mirror of
https://github.com/community-scripts/ProxmoxVE.git
synced 2026-03-13 04:09:09 +00:00
Compare commits
223 Commits
tinyauth
...
feat/remov
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b33afca1e | ||
|
|
12bdbcce5c | ||
|
|
51418f0d99 | ||
|
|
3601388abe | ||
|
|
4189137f55 | ||
|
|
043401876b | ||
|
|
543b93ced0 | ||
|
|
dd3b381813 | ||
|
|
667efeab5e | ||
|
|
4103efd10b | ||
|
|
00be37a151 | ||
|
|
c9f7453222 | ||
|
|
7f95652e80 | ||
|
|
15f6591d4c | ||
|
|
a530da5760 | ||
|
|
cc95ef2987 | ||
|
|
38c9421493 | ||
|
|
5abd9170ba | ||
|
|
375e526108 | ||
|
|
eb9013f1ce | ||
|
|
de9168f6a7 | ||
|
|
24a15d2c12 | ||
|
|
a9ebaad37e | ||
|
|
968e96e2c7 | ||
|
|
02b5c7f7a8 | ||
|
|
f1f77e5283 | ||
|
|
1e726852df | ||
|
|
ba85fad318 | ||
|
|
6ae5eefdf5 | ||
|
|
b6805bb845 | ||
|
|
3117145e6c | ||
|
|
c144915a74 | ||
|
|
ad1e207ee1 | ||
|
|
f9cb07ee4c | ||
|
|
f5ec5b0e47 | ||
|
|
ba8b85972e | ||
|
|
766f678321 | ||
|
|
c69c4afd25 | ||
|
|
516d8d7a0f | ||
|
|
a0c93900e9 | ||
|
|
a11f282a43 | ||
|
|
cac6b4ec59 | ||
|
|
eba96a55d2 | ||
|
|
37f4585110 | ||
|
|
e6931434b0 | ||
|
|
eec763bed0 | ||
|
|
fa62363628 | ||
|
|
0bbb5a1c74 | ||
|
|
064e440d00 | ||
|
|
129b85a8cf | ||
|
|
586154d4e1 | ||
|
|
b819231a01 | ||
|
|
3ec9eba736 | ||
|
|
75d4bc2b61 | ||
|
|
a5ac56be7a | ||
|
|
fe46d8c22d | ||
|
|
93cbd51d5b | ||
|
|
0b99873194 | ||
|
|
8113c7da22 | ||
|
|
85023dab51 | ||
|
|
f8ab3dc4b9 | ||
|
|
fedabe4889 | ||
|
|
eba19d8e42 | ||
|
|
e180a3bc44 | ||
|
|
a1a465708f | ||
|
|
346d6c6a0a | ||
|
|
c76813cbcb | ||
|
|
e7f551dab6 | ||
|
|
d8b2a37228 | ||
|
|
af3950fafc | ||
|
|
b20bf9c658 | ||
|
|
8c5e340ad0 | ||
|
|
f3cd063816 | ||
|
|
5adfa3cb45 | ||
|
|
3398fe9361 | ||
|
|
2afc25d51f | ||
|
|
8c5d5c6679 | ||
|
|
fca66c5b56 | ||
|
|
d38ca1a7fc | ||
|
|
b3bedd720f | ||
|
|
b0858920d5 | ||
|
|
e4e365b701 | ||
|
|
c4315713b5 | ||
|
|
047ea2c66d | ||
|
|
5e6eb400b5 | ||
|
|
db7880cab5 | ||
|
|
3909095a2c | ||
|
|
6685b88695 | ||
|
|
6076a7ecc7 | ||
|
|
cc351a4817 | ||
|
|
9217a0fb79 | ||
|
|
5abaa2e7e3 | ||
|
|
c3b8285584 | ||
|
|
bf2667827b | ||
|
|
3b7283a13f | ||
|
|
5aaca69e91 | ||
|
|
8be52ab1ad | ||
|
|
447fe2c2e3 | ||
|
|
0ecbbdf669 | ||
|
|
efce5888d7 | ||
|
|
ab93083a9d | ||
|
|
f757401d65 | ||
|
|
1e5d52098c | ||
|
|
152b703389 | ||
|
|
b0cf05fa5f | ||
|
|
e07f2ff160 | ||
|
|
2bb60e2cec | ||
|
|
f0d070b857 | ||
|
|
732de1a5bc | ||
|
|
5d7701275b | ||
|
|
8f557e460d | ||
|
|
148f0121df | ||
|
|
8eb2e58891 | ||
|
|
3ac7216e63 | ||
|
|
80f58e1e07 | ||
|
|
5446706286 | ||
|
|
79ed7e4b73 | ||
|
|
94d95ac5d2 | ||
|
|
a39b457888 | ||
|
|
94ff34d0df | ||
|
|
ff4648b7f3 | ||
|
|
acedb5fb55 | ||
|
|
4bd39e7bae | ||
|
|
5c2cf61455 | ||
|
|
10b47eae33 | ||
|
|
c61b0e766a | ||
|
|
9ff1d088c5 | ||
|
|
be803ced6f | ||
|
|
5949e9e32e | ||
|
|
bb3276bbbd | ||
|
|
b7a09989cb | ||
|
|
039d046649 | ||
|
|
58301651e4 | ||
|
|
b848c2d1bf | ||
|
|
9e2bb23d35 | ||
|
|
eb848fd70f | ||
|
|
176fffff0b | ||
|
|
14e13edeef | ||
|
|
4f70196444 | ||
|
|
acc715de82 | ||
|
|
5bfb5f486f | ||
|
|
6fbd785a87 | ||
|
|
75b1c63884 | ||
|
|
ad0808008a | ||
|
|
fcf75b06bf | ||
|
|
b8d47cdce3 | ||
|
|
8301f04b58 | ||
|
|
34942ac799 | ||
|
|
599f11683d | ||
|
|
55c699f841 | ||
|
|
3a7f922d90 | ||
|
|
352716319f | ||
|
|
5162f13372 | ||
|
|
cbe8cc82c6 | ||
|
|
c02010ea58 | ||
|
|
45e39b21ec | ||
|
|
7cbaf9c055 | ||
|
|
555f18d8df | ||
|
|
f915c68a4b | ||
|
|
da9ddcb382 | ||
|
|
55e6283075 | ||
|
|
e56b9dce35 | ||
|
|
363be950f0 | ||
|
|
0f1a06ca32 | ||
|
|
24957ea881 | ||
|
|
0be065bbef | ||
|
|
10118f4350 | ||
|
|
ac435455a0 | ||
|
|
929cd08718 | ||
|
|
299e7bfabe | ||
|
|
3465f053f5 | ||
|
|
18801eb4ff | ||
|
|
87e14ba12f | ||
|
|
438a519c65 | ||
|
|
572ea7c2be | ||
|
|
fc8b5af5fe | ||
|
|
554dfc5b0e | ||
|
|
4855fdb50d | ||
|
|
4dbb536922 | ||
|
|
154f46d6b8 | ||
|
|
bb8ac9696f | ||
|
|
41b5e5d8b9 | ||
|
|
e07b2325c1 | ||
|
|
8c9c85c1c7 | ||
|
|
2d38e509f0 | ||
|
|
4396108113 | ||
|
|
f82c15ca98 | ||
|
|
1a09b112b6 | ||
|
|
c70c488648 | ||
|
|
91db277446 | ||
|
|
35ce20391c | ||
|
|
783ba03e92 | ||
|
|
e344d3661c | ||
|
|
c62418069d | ||
|
|
41611d2682 | ||
|
|
cf8043a22e | ||
|
|
2521412747 | ||
|
|
199483be82 | ||
|
|
821b2b4415 | ||
|
|
866ae47dd4 | ||
|
|
fbc8133a86 | ||
|
|
7044b6e017 | ||
|
|
3ff140101d | ||
|
|
c4b6528bf0 | ||
|
|
7a85e33791 | ||
|
|
bbfffb97a5 | ||
|
|
4306b4018d | ||
|
|
3722533431 | ||
|
|
f773af17b2 | ||
|
|
56b4490554 | ||
|
|
b45842d76a | ||
|
|
ea279ace89 | ||
|
|
034061e744 | ||
|
|
dd07ba4453 | ||
|
|
380aa4bc0f | ||
|
|
aca721e9ee | ||
|
|
42e546904f | ||
|
|
4045824bf1 | ||
|
|
738cbfd1ae | ||
|
|
278c3cc2d8 | ||
|
|
14a7ac2618 | ||
|
|
a7699361c1 | ||
|
|
82a0893036 |
183
.github/changelogs/2026/03.md
generated
vendored
Normal file
183
.github/changelogs/2026/03.md
generated
vendored
Normal 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
2
.github/workflows/autolabeler.yml
generated
vendored
@@ -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));
|
||||
|
||||
|
||||
2
.github/workflows/close-new-script-prs.yml
generated
vendored
2
.github/workflows/close-new-script-prs.yml
generated
vendored
@@ -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
|
||||
|
||||
142
.github/workflows/close_issue_in_dev.yaml
generated
vendored
142
.github/workflows/close_issue_in_dev.yaml
generated
vendored
@@ -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
|
||||
|
||||
150
.github/workflows/delete-pocketbase-entry-on-removal.yml
generated
vendored
Normal file
150
.github/workflows/delete-pocketbase-entry-on-removal.yml
generated
vendored
Normal 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
147
.github/workflows/frontend-cicd.yml
generated
vendored
@@ -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
|
||||
6
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
6
.github/workflows/push-json-to-pocketbase.yml
generated
vendored
@@ -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
41
.github/workflows/trigger_github_pages_redirect.yml
generated
vendored
Normal 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
|
||||
4
.github/workflows/update-json-date.yml
generated
vendored
4
.github/workflows/update-json-date.yml
generated
vendored
@@ -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."
|
||||
|
||||
|
||||
167
.github/workflows/update-script-timestamp-on-sh-change.yml
generated
vendored
Normal file
167
.github/workflows/update-script-timestamp-on-sh-change.yml
generated
vendored
Normal 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
|
||||
4
.github/workflows/update-versions-github.yml
generated
vendored
4
.github/workflows/update-versions-github.yml
generated
vendored
@@ -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
7
.gitignore
vendored
@@ -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__/
|
||||
|
||||
456
CHANGELOG.md
456
CHANGELOG.md
@@ -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,239 @@ 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
|
||||
|
||||
- #### 🐞 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
|
||||
@@ -1190,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))
|
||||
@@ -48,9 +48,11 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Migrating update function"
|
||||
cat <<'MIGRATION_EOF' >/usr/bin/update
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'MIGRATION_EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/komodo.sh)"
|
||||
MIGRATION_EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
|
||||
ln -sf /usr/bin/update /usr/bin/update_komodo 2>/dev/null || true
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -46,9 +46,11 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Migrating update function"
|
||||
cat <<'MIGRATION_EOF' >/usr/bin/update
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'MIGRATION_EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/coolify.sh)"
|
||||
MIGRATION_EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
|
||||
ln -sf /usr/bin/update /usr/bin/update_coolify 2>/dev/null || true
|
||||
|
||||
@@ -25,6 +25,7 @@ function update_script() {
|
||||
check_container_resources
|
||||
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
ensure_dependencies build-essential
|
||||
|
||||
if command -v cross-seed &>/dev/null; then
|
||||
current_version=$(cross-seed --version)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)
|
||||
# Copyright (c) 2021-2026 tteck
|
||||
# Author: tteck (tteckster)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://ui.com/download/unifi
|
||||
|
||||
APP="Unifi"
|
||||
var_tags="${var_tags:-network;unifi}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-8}"
|
||||
# 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:-12}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
header_info "$APP"
|
||||
@@ -23,16 +24,16 @@ function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
if [[ ! -d /usr/lib/unifi ]]; then
|
||||
if [[ ! -f /etc/ddclient.conf ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
JAVA_VERSION="21" setup_java
|
||||
|
||||
msg_info "Updating ${APP}"
|
||||
$STD apt update --allow-releaseinfo-change
|
||||
ensure_dependencies unifi
|
||||
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
|
||||
}
|
||||
@@ -43,5 +44,3 @@ 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}https://${IP}:8443${CL}"
|
||||
@@ -48,9 +48,11 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Migrating update function"
|
||||
cat <<'MIGRATION_EOF' >/usr/bin/update
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'MIGRATION_EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/dockge.sh)"
|
||||
MIGRATION_EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
|
||||
ln -sf /usr/bin/update /usr/bin/update_dockge 2>/dev/null || true
|
||||
|
||||
@@ -48,6 +48,17 @@ function update_script() {
|
||||
cd /opt/docmost
|
||||
mv /opt/.env /opt/docmost/.env
|
||||
mv /opt/data /opt/docmost/data
|
||||
|
||||
# Fix: Docmost EE (audit logs etc.) lives in a git submodule that is NOT
|
||||
# included in GitHub tarballs. The community NoopAuditService exists but
|
||||
# is only exported by CoreModule – child modules such as UserModule cannot
|
||||
# resolve it. Making CoreModule @Global() exposes the token app-wide.
|
||||
if [[ ! -f /opt/docmost/apps/server/src/ee/ee.module.ts ]] \
|
||||
&& ! grep -q '@Global()' /opt/docmost/apps/server/src/core/core.module.ts 2>/dev/null; then
|
||||
sed -i '/^ Module,$/a\ Global,' /opt/docmost/apps/server/src/core/core.module.ts
|
||||
sed -i '/^@Module({$/i @Global()' /opt/docmost/apps/server/src/core/core.module.ts
|
||||
fi
|
||||
|
||||
$STD pnpm install --force
|
||||
$STD pnpm build
|
||||
msg_ok "Updated ${APP}"
|
||||
|
||||
@@ -46,9 +46,11 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Migrating update function"
|
||||
cat <<'MIGRATION_EOF' >/usr/bin/update
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'MIGRATION_EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/dokploy.sh)"
|
||||
MIGRATION_EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
|
||||
ln -sf /usr/bin/update /usr/bin/update_dokploy 2>/dev/null || true
|
||||
|
||||
@@ -28,6 +28,10 @@ function update_script() {
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
ensure_dependencies build-essential
|
||||
NODE_VERSION="24" setup_nodejs
|
||||
|
||||
if check_for_gh_release "fluid-calendar" "dotnetfactory/fluid-calendar"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop fluid-calendar
|
||||
@@ -45,7 +49,7 @@ function update_script() {
|
||||
$STD npx prisma migrate deploy
|
||||
$STD npm run build:os
|
||||
msg_ok "Updated Fluid Calendar"
|
||||
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start fluid-calendar
|
||||
msg_ok "Started Service"
|
||||
|
||||
@@ -87,6 +87,8 @@ EOF
|
||||
msg_ok "Old Enviroment fixed"
|
||||
fi
|
||||
|
||||
ensure_dependencies git
|
||||
|
||||
if check_for_gh_release "gitea-mirror" "RayLabsHQ/gitea-mirror"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop gitea-mirror
|
||||
@@ -94,7 +96,7 @@ EOF
|
||||
|
||||
msg_info "Backup Data"
|
||||
mkdir -p /opt/gitea-mirror-backup/data
|
||||
cp /opt/gitea-mirror/data/* /opt/gitea-mirror-backup/data/
|
||||
cp -r /opt/gitea-mirror/data/* /opt/gitea-mirror-backup/data/
|
||||
msg_ok "Backup Data"
|
||||
|
||||
msg_info "Installing Bun"
|
||||
@@ -111,12 +113,11 @@ EOF
|
||||
$STD bun run setup
|
||||
$STD bun run build
|
||||
APP_VERSION=$(grep -o '"version": *"[^"]*"' package.json | cut -d'"' -f4)
|
||||
|
||||
sudo sed -i.bak "s|^npm_package_version=.*|npm_package_version=${APP_VERSION}|" /opt/gitea-mirror.env
|
||||
sed -i.bak "s|^npm_package_version=.*|npm_package_version=${APP_VERSION}|" /opt/gitea-mirror.env
|
||||
msg_ok "Updated and rebuilt ${APP}"
|
||||
|
||||
msg_info "Restoring Data"
|
||||
cp /opt/gitea-mirror-backup/data/* /opt/gitea-mirror/data
|
||||
cp -r /opt/gitea-mirror-backup/data/* /opt/gitea-mirror/data
|
||||
msg_ok "Restored Data"
|
||||
|
||||
msg_info "Starting Service"
|
||||
|
||||
@@ -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
6
ct/headers/ddclient
Normal file
@@ -0,0 +1,6 @@
|
||||
__ __ ___ __
|
||||
____/ /___/ /____/ (_)__ ____ / /_
|
||||
/ __ / __ / ___/ / / _ \/ __ \/ __/
|
||||
/ /_/ / /_/ / /__/ / / __/ / / / /_
|
||||
\__,_/\__,_/\___/_/_/\___/_/ /_/\__/
|
||||
|
||||
6
ct/headers/immichframe
Normal file
6
ct/headers/immichframe
Normal file
@@ -0,0 +1,6 @@
|
||||
____ _ __ ______
|
||||
/ _/___ ___ ____ ___ (_)____/ /_ / ____/________ _____ ___ ___
|
||||
/ // __ `__ \/ __ `__ \/ / ___/ __ \/ /_ / ___/ __ `/ __ `__ \/ _ \
|
||||
_/ // / / / / / / / / / / / /__/ / / / __/ / / / /_/ / / / / / / __/
|
||||
/___/_/ /_/ /_/_/ /_/ /_/_/\___/_/ /_/_/ /_/ \__,_/_/ /_/ /_/\___/
|
||||
|
||||
6
ct/headers/netbird
Normal file
6
ct/headers/netbird
Normal file
@@ -0,0 +1,6 @@
|
||||
_ __ __ ____ _ __
|
||||
/ | / /__ / /_/ __ )(_)________/ /
|
||||
/ |/ / _ \/ __/ __ / / ___/ __ /
|
||||
/ /| / __/ /_/ /_/ / / / / /_/ /
|
||||
/_/ |_/\___/\__/_____/_/_/ \__,_/
|
||||
|
||||
6
ct/headers/papra
Normal file
6
ct/headers/papra
Normal file
@@ -0,0 +1,6 @@
|
||||
____
|
||||
/ __ \____ _____ _________ _
|
||||
/ /_/ / __ `/ __ \/ ___/ __ `/
|
||||
/ ____/ /_/ / /_/ / / / /_/ /
|
||||
/_/ \__,_/ .___/_/ \__,_/
|
||||
/_/
|
||||
@@ -1,6 +0,0 @@
|
||||
_____ _ _____
|
||||
/ ___/__ ___ ______ ___ ______ ____ ___ (_) ___/___ ______ _____ _____
|
||||
\__ \/ / / / | /| / / __ `/ / / / __ \/ __ `__ \/ /\__ \/ _ \/ ___/ | / / _ \/ ___/
|
||||
___/ / /_/ /| |/ |/ / /_/ / /_/ / /_/ / / / / / / /___/ / __/ / | |/ / __/ /
|
||||
/____/\__,_/ |__/|__/\__,_/\__, /\____/_/ /_/ /_/_//____/\___/_/ |___/\___/_/
|
||||
/____/
|
||||
6
ct/headers/tinyauth
Normal file
6
ct/headers/tinyauth
Normal file
@@ -0,0 +1,6 @@
|
||||
_______ __ __
|
||||
/_ __(_)___ __ ______ ___ __/ /_/ /_
|
||||
/ / / / __ \/ / / / __ `/ / / / __/ __ \
|
||||
/ / / / / / / /_/ / /_/ / /_/ / /_/ / / /
|
||||
/_/ /_/_/ /_/\__, /\__,_/\__,_/\__/_/ /_/
|
||||
/____/
|
||||
@@ -1,6 +0,0 @@
|
||||
__ __ _ _____
|
||||
/ / / /___ (_) __(_)
|
||||
/ / / / __ \/ / /_/ /
|
||||
/ /_/ / / / / / __/ /
|
||||
\____/_/ /_/_/_/ /_/
|
||||
|
||||
15
ct/immich.sh
15
ct/immich.sh
@@ -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
|
||||
@@ -337,7 +342,7 @@ function compile_libraw() {
|
||||
if [[ "$LIBRAW_REVISION" != "$(grep 'libraw' ~/.immich_library_revisions | awk '{print $2}')" ]]; then
|
||||
msg_info "Recompiling libraw"
|
||||
[[ -d "$SOURCE" ]] && rm -rf "$SOURCE"
|
||||
$STD git clone https://github.com/libraw/libraw.git "$SOURCE"
|
||||
$STD git clone https://github.com/LibRaw/LibRaw.git "$SOURCE"
|
||||
cd "$SOURCE"
|
||||
$STD git reset --hard "$LIBRAW_REVISION"
|
||||
$STD autoreconf --install
|
||||
@@ -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
81
ct/immichframe.sh
Normal 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}"
|
||||
@@ -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
|
||||
|
||||
@@ -45,14 +45,15 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Switching update script to Seerr"
|
||||
cat <<'EOF' >/usr/bin/update
|
||||
#!/usr/bin/env bash
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/seerr.sh)"
|
||||
EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
msg_ok "Switched update script to Seerr"
|
||||
msg_warn "Please type 'update' again to complete the migration"
|
||||
exit
|
||||
exit 0
|
||||
fi
|
||||
|
||||
msg_info "Updating Jellyseerr"
|
||||
|
||||
@@ -52,9 +52,11 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Migrating update function"
|
||||
cat <<'MIGRATION_EOF' >/usr/bin/update
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'MIGRATION_EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/komodo.sh)"
|
||||
MIGRATION_EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
|
||||
ln -sf /usr/bin/update /usr/bin/update_komodo 2>/dev/null || true
|
||||
|
||||
@@ -34,7 +34,7 @@ function update_script() {
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
VENV_PATH="/opt/litellm/.venv"
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
PYTHON_VERSION="3.13" USE_UVX="YES" setup_uv
|
||||
|
||||
msg_info "Updating LiteLLM"
|
||||
$STD "$VENV_PATH/bin/python" -m pip install --upgrade litellm[proxy] prisma
|
||||
|
||||
@@ -9,7 +9,7 @@ APP="MeshCentral"
|
||||
var_tags="${var_tags:-remote-management}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-512}"
|
||||
var_disk="${var_disk:-2}"
|
||||
var_disk="${var_disk:-4}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
@@ -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
47
ct/netbird.sh
Normal 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}"
|
||||
10
ct/nzbget.sh
10
ct/nzbget.sh
@@ -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
|
||||
|
||||
@@ -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\
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -44,10 +44,11 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Switching update script to Seerr"
|
||||
cat <<'EOF' >/usr/bin/update
|
||||
#!/usr/bin/env bash
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/seerr.sh)"
|
||||
EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
msg_ok "Switched update script to Seerr"
|
||||
msg_warn "Please type 'update' again to complete the migration"
|
||||
|
||||
68
ct/papra.sh
Normal file
68
ct/papra.sh
Normal 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}"
|
||||
@@ -47,7 +47,10 @@ function update_script() {
|
||||
cp /opt/poweradmin_powerdns.db.bak /opt/poweradmin/powerdns.db
|
||||
rm -rf /opt/poweradmin/install
|
||||
rm -f /opt/poweradmin_settings.php.bak /opt/poweradmin_powerdns.db.bak
|
||||
chown -R www-data:www-data /opt/poweradmin
|
||||
chown -R www-data:pdns /opt/poweradmin
|
||||
chmod 775 /opt/poweradmin
|
||||
chown pdns:pdns /opt/poweradmin/powerdns.db
|
||||
chmod 664 /opt/poweradmin/powerdns.db
|
||||
msg_ok "Updated Poweradmin"
|
||||
|
||||
msg_info "Restarting Services"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,9 +46,11 @@ function update_script() {
|
||||
fi
|
||||
|
||||
msg_info "Migrating update function"
|
||||
cat <<'MIGRATION_EOF' >/usr/bin/update
|
||||
TMP_UPDATE=$(mktemp)
|
||||
cat <<'MIGRATION_EOF' >"$TMP_UPDATE"
|
||||
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/tools/addon/runtipi.sh)"
|
||||
MIGRATION_EOF
|
||||
mv "$TMP_UPDATE" /usr/bin/update
|
||||
chmod +x /usr/bin/update
|
||||
|
||||
ln -sf /usr/bin/update /usr/bin/update_runtipi 2>/dev/null || true
|
||||
|
||||
@@ -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!"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -35,7 +35,7 @@ function update_script() {
|
||||
fi
|
||||
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
JAVA_VERSION="21" setup_java
|
||||
JAVA_VERSION="25" setup_java
|
||||
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop stirlingpdf libreoffice-listener unoserver
|
||||
|
||||
@@ -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}"
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
39
frontend/.gitignore
vendored
@@ -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
51
frontend/.vscode/settings.json
generated
vendored
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 🌟 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
2031
frontend/bun.lock
generated
File diff suppressed because it is too large
Load Diff
20
frontend/components.json
generated
20
frontend/components.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -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
87
frontend/package.json
generated
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
{
|
||||
"name": "Tinyauth",
|
||||
"slug": "tinyauth",
|
||||
"categories": [
|
||||
6
|
||||
],
|
||||
"date_created": "2025-05-06",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 3000,
|
||||
"documentation": "https://tinyauth.app",
|
||||
"config_path": "/opt/tinyauth/.env",
|
||||
"website": "https://tinyauth.app",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/tinyauth.webp",
|
||||
"description": "Tinyauth is a simple authentication middleware that adds simple username/password login or OAuth with Google, Github and any generic provider to all of your docker apps.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/tinyauth.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 512,
|
||||
"hdd": 4,
|
||||
"os": "debian",
|
||||
"version": "13"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "alpine",
|
||||
"script": "ct/alpine-tinyauth.sh",
|
||||
"resources": {
|
||||
"cpu": 1,
|
||||
"ram": 256,
|
||||
"hdd": 2,
|
||||
"os": "alpine",
|
||||
"version": "3.23"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "The default credentials are located in `/opt/tinyauth/credentials.txt`.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
{
|
||||
"name": "UniFi Network Server",
|
||||
"slug": "unifi",
|
||||
"categories": [
|
||||
4
|
||||
],
|
||||
"date_created": "2024-05-02",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8443,
|
||||
"documentation": "https://help.ui.com/hc/en-us/articles/360012282453-Self-Hosting-a-UniFi-Network-Server",
|
||||
"website": "https://www.ui.com/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ubiquiti-unifi.webp",
|
||||
"config_path": "",
|
||||
"description": "UniFi Network Server is a software that helps manage and monitor UniFi networks (Wi-Fi, Ethernet, etc.) by providing an intuitive user interface and advanced features. It allows network administrators to configure, monitor, and upgrade network devices, as well as view network statistics, client devices, and historical events. The aim of the application is to make the management of UniFi networks easier and more efficient.",
|
||||
"disable": true,
|
||||
"disable_description": "This script is disabled because UniFi no longer delivers APT packages for Debian systems. The installation relies on APT repositories that are no longer maintained or available. For more details, see: https://github.com/community-scripts/ProxmoxVE/issues/11876",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/unifi.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "debian",
|
||||
"version": "12"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "For non-AVX CPUs, MongoDB 4.4 is installed. Please note this is a legacy solution that may present security risks and could become unsupported in future updates.",
|
||||
"type": "warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 63 KiB |
@@ -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 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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 |
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'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're a seasoned user or a
|
||||
newcomer, we'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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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`,
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user