mirror of
https://github.com/Heretek-AI/Heretek-ProxmoxVE.git
synced 2026-07-01 12:25:27 -04:00
Sync from Heretek-AI/ProxmoxVE - 2026-03-18
This commit is contained in:
@@ -422,6 +422,10 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
## 2026-03-18
|
||||
|
||||
### 🆕 New Scripts
|
||||
|
||||
- 🔄 Upstream Sync - 2026-03-18 [@BillyOutlast](https://github.com/BillyOutlast) ([#62](https://github.com/Heretek-AI/ProxmoxVE/pull/62))
|
||||
|
||||
## 2026-03-16
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
@@ -91,17 +91,18 @@ msg_info "Running Unsloth Studio Setup"
|
||||
# This requires GPU access - set up environment for ROCm if installed
|
||||
|
||||
# Set up ROCm environment if available
|
||||
# Use ${VAR:-} to handle unset variables (set -u causes errors otherwise)
|
||||
if [ -d "/opt/rocm" ]; then
|
||||
export PATH="/opt/rocm/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib:$LD_LIBRARY_PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export ROCM_PATH="/opt/rocm"
|
||||
elif [ -d "/opt/rocm-7.2" ]; then
|
||||
export PATH="/opt/rocm-7.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:$LD_LIBRARY_PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export ROCM_PATH="/opt/rocm-7.2"
|
||||
elif [ -d "/opt/rocm-6.2" ]; then
|
||||
export PATH="/opt/rocm-6.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:$LD_LIBRARY_PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
export ROCM_PATH="/opt/rocm-6.2"
|
||||
fi
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
__ __ __ __ __
|
||||
/ / / /___ _____/ /___ / /_/ /_
|
||||
/ / / / __ \/ ___/ / __ \/ __/ __ \
|
||||
/ /_/ / / / (__ ) / /_/ / /_/ / / /
|
||||
\____/_/ /_/____/_/\____/\__/_/ /_/
|
||||
|
||||
____ __ __ ___
|
||||
__ ______ _________ / / /_/ /_ _____/ /___ ______/ (_)___
|
||||
/ / / / __ \/ ___/ __ \/ / __/ __ \______/ ___/ __/ / / / __ / / __ \
|
||||
/ /_/ / / / (__ ) /_/ / / /_/ / / /_____(__ ) /_/ /_/ / /_/ / / /_/ /
|
||||
\__,_/_/ /_/____/\____/_/\__/_/ /_/ /____/\__/\__,_/\__,_/_/\____/
|
||||
|
||||
|
||||
+3
-4
@@ -30,12 +30,12 @@ msg_ok "Installed Dependencies"
|
||||
setup_hwaccel
|
||||
|
||||
# Setup Python virtual environment with uv (fast Python package manager)
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
PYTHON_VERSION="3.13" setup_uv
|
||||
|
||||
msg_info "Creating Virtual Environment"
|
||||
mkdir -p /opt/unsolth-studio
|
||||
cd /opt/unsolth-studio || exit
|
||||
$STD uv venv --python 3.12
|
||||
$STD uv venv --python 3.13
|
||||
source .venv/bin/activate
|
||||
msg_ok "Created Virtual Environment"
|
||||
|
||||
@@ -70,8 +70,7 @@ if [ "$GPU_TYPE" = "nvidia" ]; then
|
||||
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
|
||||
msg_ok "Installed PyTorch with CUDA Support"
|
||||
elif [ "$GPU_TYPE" = "amd" ]; then
|
||||
# AMD GPU - install PyTorch with ROCm 6.2 support
|
||||
# ROCm 6.2 is compatible with ROCm 7.x runtime
|
||||
# AMD GPU - install PyTorch with ROCm 7.2 support
|
||||
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/test/rocm7.2
|
||||
msg_ok "Installed PyTorch with ROCm Support"
|
||||
else
|
||||
|
||||
+108
-9
@@ -39,11 +39,47 @@ $STD uv venv --python 3.12
|
||||
source .venv/bin/activate
|
||||
msg_ok "Created Virtual Environment"
|
||||
|
||||
msg_info "Installing PyTorch"
|
||||
# Install PyTorch first (required by unsloth)
|
||||
# Use CPU version for broader compatibility; GPU version can be installed manually if needed
|
||||
$STD uv pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||
msg_ok "Installed PyTorch"
|
||||
msg_info "Detecting GPU Type for PyTorch Installation"
|
||||
# Detect GPU type based on what setup_hwaccel installed
|
||||
# setup_hwaccel runs before this and installs NVIDIA drivers or ROCm
|
||||
|
||||
GPU_TYPE="cpu"
|
||||
|
||||
# Check for NVIDIA GPU (nvidia-smi installed by setup_hwaccel)
|
||||
if command -v nvidia-smi &>/dev/null && nvidia-smi &>/dev/null; then
|
||||
GPU_TYPE="nvidia"
|
||||
msg_info "NVIDIA GPU detected - installing PyTorch with CUDA support"
|
||||
# Check for AMD GPU (ROCm installed by setup_hwaccel at /opt/rocm)
|
||||
elif [ -d "/opt/rocm" ] || [ -d "/opt/rocm-7.2" ] || [ -d "/opt/rocm-6.2" ]; then
|
||||
GPU_TYPE="amd"
|
||||
msg_info "AMD GPU detected (ROCm installed) - installing PyTorch with ROCm support"
|
||||
# Check for AMD render devices (GPU passthrough configured)
|
||||
elif ls /dev/dri/renderD* &>/dev/null 2>&1; then
|
||||
# Check if any render device is AMD
|
||||
for render_dev in /dev/dri/renderD*; do
|
||||
if [ -e "$render_dev" ]; then
|
||||
GPU_TYPE="amd"
|
||||
msg_info "AMD GPU detected (render device) - installing PyTorch with ROCm support"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if [ "$GPU_TYPE" = "nvidia" ]; then
|
||||
# NVIDIA GPU - install PyTorch with CUDA 12.4 support
|
||||
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124
|
||||
msg_ok "Installed PyTorch with CUDA Support"
|
||||
elif [ "$GPU_TYPE" = "amd" ]; then
|
||||
# AMD GPU - install PyTorch with ROCm 6.2 support
|
||||
# ROCm 6.2 is compatible with ROCm 7.x runtime
|
||||
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/test/rocm7.2
|
||||
msg_ok "Installed PyTorch with ROCm Support"
|
||||
else
|
||||
# No GPU detected - install CPU version
|
||||
msg_info "No GPU detected - installing PyTorch CPU version"
|
||||
$STD uv pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
|
||||
msg_ok "Installed PyTorch (CPU version)"
|
||||
fi
|
||||
|
||||
msg_info "Installing Unsloth"
|
||||
# Install unsloth and its dependencies
|
||||
@@ -53,9 +89,36 @@ msg_ok "Installed Unsloth"
|
||||
|
||||
msg_info "Running Unsloth Studio Setup"
|
||||
# Run the unsloth studio setup command to compile llama.cpp
|
||||
# Use Python module invocation since uv pip install doesn't create entry points
|
||||
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
|
||||
msg_ok "Completed Unsloth Studio Setup"
|
||||
# This requires GPU access - set up environment for ROCm if installed
|
||||
|
||||
# Set up ROCm environment if available
|
||||
if [ -d "/opt/rocm" ]; then
|
||||
export PATH="/opt/rocm/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib:$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm"
|
||||
elif [ -d "/opt/rocm-7.2" ]; then
|
||||
export PATH="/opt/rocm-7.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm-7.2"
|
||||
elif [ -d "/opt/rocm-6.2" ]; then
|
||||
export PATH="/opt/rocm-6.2/bin:$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm-6.2"
|
||||
fi
|
||||
|
||||
# Check if GPU is available (works for both CUDA and ROCm)
|
||||
if /opt/unsolth-studio/.venv/bin/python -c "import torch; exit(0 if torch.cuda.is_available() else 1)" 2>/dev/null; then
|
||||
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
|
||||
msg_ok "Completed Unsloth Studio Setup"
|
||||
else
|
||||
msg_info "GPU not detected via torch.cuda - skipping Unsloth Studio setup"
|
||||
msg_info "This may be normal if ROCm libraries need system restart to take effect"
|
||||
echo ""
|
||||
echo -e "${GN}Note: If you have GPU passthrough configured, try:${CL}"
|
||||
echo -e "${GN} 1. Restart the container: pct stop <CTID> && pct start <CTID>${CL}"
|
||||
echo -e "${GN} 2. Then run: source /opt/unsolth-studio/.venv/bin/activate && unsloth studio setup${CL}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
msg_info "Creating Directories"
|
||||
mkdir -p /opt/unsolth-studio/models
|
||||
@@ -65,6 +128,34 @@ chmod 755 /var/log/unsolth-studio
|
||||
msg_ok "Created Directories"
|
||||
|
||||
msg_info "Creating Service"
|
||||
# Create environment file for ROCm/CUDA paths
|
||||
cat <<EOF >/opt/unsolth-studio/environment.sh
|
||||
#!/bin/bash
|
||||
# Set up GPU environment for Unsloth Studio
|
||||
|
||||
# ROCm environment (AMD GPUs)
|
||||
if [ -d "/opt/rocm" ]; then
|
||||
export PATH="/opt/rocm/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm/lib:\$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm"
|
||||
elif [ -d "/opt/rocm-7.2" ]; then
|
||||
export PATH="/opt/rocm-7.2/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-7.2/lib:\$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm-7.2"
|
||||
elif [ -d "/opt/rocm-6.2" ]; then
|
||||
export PATH="/opt/rocm-6.2/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/opt/rocm-6.2/lib:\$LD_LIBRARY_PATH"
|
||||
export ROCM_PATH="/opt/rocm-6.2"
|
||||
fi
|
||||
|
||||
# NVIDIA CUDA environment
|
||||
if [ -d "/usr/local/cuda" ]; then
|
||||
export PATH="/usr/local/cuda/bin:\$PATH"
|
||||
export LD_LIBRARY_PATH="/usr/local/cuda/lib64:\$LD_LIBRARY_PATH"
|
||||
fi
|
||||
EOF
|
||||
chmod +x /opt/unsolth-studio/environment.sh
|
||||
|
||||
cat <<EOF >/etc/systemd/system/unsolth-studio.service
|
||||
[Unit]
|
||||
Description=Unsloth Studio - Local LLM Fine-tuning Web UI
|
||||
@@ -75,6 +166,7 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/unsolth-studio
|
||||
Environment="PATH=/opt/unsolth-studio/.venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
EnvironmentFile=/opt/unsolth-studio/environment.sh
|
||||
ExecStart=/opt/unsolth-studio/.venv/bin/python -m unsloth studio -H 0.0.0.0 -p 8888
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
@@ -90,8 +182,15 @@ TimeoutStopSec=60
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now unsolth-studio
|
||||
# Don't auto-start the service since GPU passthrough may not be configured yet
|
||||
# User needs to configure GPU passthrough first, then start the service manually
|
||||
systemctl enable -q unsolth-studio
|
||||
msg_ok "Created Service"
|
||||
echo ""
|
||||
echo -e "${GN}Note: The unsolth-studio service is enabled but not started.${CL}"
|
||||
echo -e "${GN}Configure GPU passthrough first, then start with:${CL}"
|
||||
echo -e "${GN} systemctl start unsolth-studio${CL}"
|
||||
echo ""
|
||||
|
||||
# Create GPU passthrough info file
|
||||
cat <<EOF >/opt/unsolth-studio/GPU_PASSTHROUGH.md
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
#!/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: cobalt (cobaltgit)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||
# Source: https://ntfy.sh/
|
||||
|
||||
APP="Alpine-ntfy"
|
||||
var_tags="${var_tags:-notification}"
|
||||
var_cpu="${var_cpu:-1}"
|
||||
var_ram="${var_ram:-256}"
|
||||
var_disk="${var_disk:-2}"
|
||||
var_os="${var_os:-alpine}"
|
||||
var_version="${var_version:-3.23}"
|
||||
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 /etc/ntfy ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
msg_info "Updating ntfy LXC"
|
||||
$STD apk -U upgrade
|
||||
setcap 'cap_net_bind_service=+ep' /usr/bin/ntfy
|
||||
msg_ok "Updated ntfy LXC"
|
||||
|
||||
msg_info "Restarting ntfy"
|
||||
rc-service ntfy restart
|
||||
msg_ok "Restarted ntfy"
|
||||
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 it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}${CL}"
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
#!/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://anytype.io
|
||||
|
||||
APP="Anytype-Server"
|
||||
var_tags="${var_tags:-notes;productivity;sync}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-4096}"
|
||||
var_disk="${var_disk:-16}"
|
||||
var_os="${var_os:-ubuntu}"
|
||||
var_version="${var_version:-24.04}"
|
||||
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 /opt/anytype/any-sync-bundle ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "anytype" "grishy/any-sync-bundle"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop anytype
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp -r /opt/anytype/data /opt/anytype_data_backup
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz"
|
||||
chmod +x /opt/anytype/any-sync-bundle
|
||||
|
||||
msg_info "Restoring Data"
|
||||
cp -r /opt/anytype_data_backup/. /opt/anytype/data
|
||||
rm -rf /opt/anytype_data_backup
|
||||
msg_ok "Restored Data"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start anytype
|
||||
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}:33010${CL}"
|
||||
echo -e "${INFO}${YW} Client config file:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}/opt/anytype/data/client-config.yml${CL}"
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#!/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/qdm12/gluetun
|
||||
|
||||
APP="Gluetun"
|
||||
var_tags="${var_tags:-vpn;wireguard;openvpn}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
var_disk="${var_disk:-8}"
|
||||
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 /usr/local/bin/gluetun ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "gluetun" "qdm12/gluetun"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop gluetun
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball"
|
||||
|
||||
msg_info "Building Gluetun"
|
||||
cd /opt/gluetun
|
||||
$STD go mod download
|
||||
CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/
|
||||
msg_ok "Built Gluetun"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start gluetun
|
||||
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}:8000${CL}"
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
___ __ _ __ ____
|
||||
/ | / /___ (_)___ ___ ____ / /_/ __/_ __
|
||||
/ /| | / / __ \/ / __ \/ _ \______/ __ \/ __/ /_/ / / /
|
||||
/ ___ |/ / /_/ / / / / / __/_____/ / / / /_/ __/ /_/ /
|
||||
/_/ |_/_/ .___/_/_/ /_/\___/ /_/ /_/\__/_/ \__, /
|
||||
/_/ /____/
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
___ __ _____
|
||||
/ | ____ __ __/ /___ ______ ___ / ___/___ ______ _____ _____
|
||||
/ /| | / __ \/ / / / __/ / / / __ \/ _ \______\__ \/ _ \/ ___/ | / / _ \/ ___/
|
||||
/ ___ |/ / / / /_/ / /_/ /_/ / /_/ / __/_____/__/ / __/ / | |/ / __/ /
|
||||
/_/ |_/_/ /_/\__, /\__/\__, / .___/\___/ /____/\___/_/ |___/\___/_/
|
||||
/____/ /____/_/
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
________ __
|
||||
/ ____/ /_ _____ / /___ ______
|
||||
/ / __/ / / / / _ \/ __/ / / / __ \
|
||||
/ /_/ / / /_/ / __/ /_/ /_/ / / / /
|
||||
\____/_/\__,_/\___/\__/\__,_/_/ /_/
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
_____ ___ __ ____
|
||||
/ ___/____ / (_) /_ / __ \_________
|
||||
\__ \/ __ \/ / / __/_____/ /_/ / ___/ __ \
|
||||
___/ / /_/ / / / /_/_____/ ____/ / / /_/ /
|
||||
/____/ .___/_/_/\__/ /_/ /_/ \____/
|
||||
/_/
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
__ __ __ __
|
||||
\ \/ /___ _____ ___ / /__________ ______/ /__
|
||||
\ / __ `/ __ `__ \/ __/ ___/ __ `/ ___/ //_/
|
||||
/ / /_/ / / / / / / /_/ / / /_/ / /__/ ,<
|
||||
/_/\__,_/_/ /_/ /_/\__/_/ \__,_/\___/_/|_|
|
||||
|
||||
+68
@@ -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: johanngrobe
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/oss-apps/split-pro
|
||||
|
||||
APP="Split-Pro"
|
||||
var_tags="${var_tags:-finance;expense-sharing}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-4096}"
|
||||
var_disk="${var_disk:-6}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/split-pro ]]; then
|
||||
msg_error "No Split Pro Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "split-pro" "oss-apps/split-pro"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop split-pro
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp /opt/split-pro/.env /opt/split-pro.env
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "split-pro" "oss-apps/split-pro" "tarball"
|
||||
|
||||
msg_info "Building Application"
|
||||
cd /opt/split-pro
|
||||
$STD pnpm install --frozen-lockfile
|
||||
$STD pnpm build
|
||||
cp /opt/split-pro.env /opt/split-pro/.env
|
||||
rm -f /opt/split-pro.env
|
||||
ln -sf /opt/split-pro_data/uploads /opt/split-pro/uploads
|
||||
$STD pnpm exec prisma migrate deploy
|
||||
msg_ok "Built Application"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start split-pro
|
||||
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}:3000${CL}"
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
#!/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/FuzzyGrim/Yamtrack
|
||||
|
||||
APP="Yamtrack"
|
||||
var_tags="${var_tags:-media;tracker;movies;anime}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
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/yamtrack ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "yamtrack" "FuzzyGrim/Yamtrack"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop yamtrack yamtrack-celery
|
||||
msg_ok "Stopped Services"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp /opt/yamtrack/src/.env /opt/yamtrack_env.bak
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "yamtrack" "FuzzyGrim/Yamtrack" "tarball"
|
||||
|
||||
msg_info "Installing Python Dependencies"
|
||||
cd /opt/yamtrack
|
||||
$STD uv venv .venv
|
||||
$STD uv pip install --no-cache-dir -r requirements.txt
|
||||
msg_ok "Installed Python Dependencies"
|
||||
|
||||
msg_info "Restoring Data"
|
||||
cp /opt/yamtrack_env.bak /opt/yamtrack/src/.env
|
||||
rm -f /opt/yamtrack_env.bak
|
||||
msg_ok "Restored Data"
|
||||
|
||||
msg_info "Updating Yamtrack"
|
||||
cd /opt/yamtrack/src
|
||||
$STD /opt/yamtrack/.venv/bin/python manage.py migrate
|
||||
$STD /opt/yamtrack/.venv/bin/python manage.py collectstatic --noinput
|
||||
msg_ok "Updated Yamtrack"
|
||||
|
||||
msg_info "Updating Nginx Configuration"
|
||||
cp /opt/yamtrack/nginx.conf /etc/nginx/nginx.conf
|
||||
sed -i 's|user abc;|user www-data;|' /etc/nginx/nginx.conf
|
||||
sed -i 's|/yamtrack/staticfiles/|/opt/yamtrack/src/staticfiles/|' /etc/nginx/nginx.conf
|
||||
$STD systemctl reload nginx
|
||||
msg_ok "Updated Nginx Configuration"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start yamtrack yamtrack-celery
|
||||
msg_ok "Started Services"
|
||||
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}:8000${CL}"
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: cobalt (cobaltgit)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVED/raw/main/LICENSE
|
||||
# Source: https://ntfy.sh/
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing ntfy"
|
||||
$STD apk add --no-cache ntfy ntfy-openrc libcap
|
||||
sed -i '/^listen-http/s/^\(.*\)$/#\1\n/' /etc/ntfy/server.yml
|
||||
setcap 'cap_net_bind_service=+ep' /usr/bin/ntfy
|
||||
$STD rc-update add ntfy default
|
||||
$STD service ntfy start
|
||||
msg_ok "Installed ntfy"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: MickLesk (CanbiZ)
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://anytype.io
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
setup_mongodb
|
||||
|
||||
msg_info "Configuring MongoDB Replica Set"
|
||||
cat <<EOF >>/etc/mongod.conf
|
||||
|
||||
replication:
|
||||
replSetName: "rs0"
|
||||
EOF
|
||||
systemctl restart mongod
|
||||
sleep 3
|
||||
$STD mongosh --eval 'rs.initiate({_id: "rs0", members: [{_id: 0, host: "127.0.0.1:27017"}]})'
|
||||
msg_ok "Configured MongoDB Replica Set"
|
||||
|
||||
msg_info "Installing Redis Stack"
|
||||
setup_deb822_repo \
|
||||
"redis-stack" \
|
||||
"https://packages.redis.io/gpg" \
|
||||
"https://packages.redis.io/deb" \
|
||||
"jammy" \
|
||||
"main"
|
||||
$STD apt install -y redis-stack-server
|
||||
systemctl enable -q --now redis-stack-server
|
||||
msg_ok "Installed Redis Stack"
|
||||
|
||||
fetch_and_deploy_gh_release "anytype" "grishy/any-sync-bundle" "prebuild" "latest" "/opt/anytype" "any-sync-bundle_*_linux_amd64.tar.gz"
|
||||
chmod +x /opt/anytype/any-sync-bundle
|
||||
|
||||
msg_info "Configuring Anytype"
|
||||
mkdir -p /opt/anytype/data/storage
|
||||
cat <<EOF >/opt/anytype/.env
|
||||
ANY_SYNC_BUNDLE_CONFIG=/opt/anytype/data/bundle-config.yml
|
||||
ANY_SYNC_BUNDLE_CLIENT_CONFIG=/opt/anytype/data/client-config.yml
|
||||
ANY_SYNC_BUNDLE_INIT_STORAGE=/opt/anytype/data/storage/
|
||||
ANY_SYNC_BUNDLE_INIT_EXTERNAL_ADDRS=${LOCAL_IP}
|
||||
ANY_SYNC_BUNDLE_INIT_MONGO_URI=mongodb://127.0.0.1:27017/
|
||||
ANY_SYNC_BUNDLE_INIT_REDIS_URI=redis://127.0.0.1:6379/
|
||||
ANY_SYNC_BUNDLE_LOG_LEVEL=info
|
||||
EOF
|
||||
msg_ok "Configured Anytype"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/anytype.service
|
||||
[Unit]
|
||||
Description=Anytype Sync Server (any-sync-bundle)
|
||||
After=network-online.target mongod.service redis-stack-server.service
|
||||
Wants=network-online.target
|
||||
Requires=mongod.service redis-stack-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/anytype
|
||||
EnvironmentFile=/opt/anytype/.env
|
||||
ExecStart=/opt/anytype/any-sync-bundle start-bundle
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now anytype
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 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/qdm12/gluetun
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
openvpn \
|
||||
wireguard-tools \
|
||||
iptables
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
msg_info "Configuring iptables"
|
||||
$STD update-alternatives --set iptables /usr/sbin/iptables-legacy
|
||||
$STD update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
|
||||
ln -sf /usr/sbin/openvpn /usr/sbin/openvpn2.6
|
||||
msg_ok "Configured iptables"
|
||||
|
||||
setup_go
|
||||
|
||||
fetch_and_deploy_gh_release "gluetun" "qdm12/gluetun" "tarball"
|
||||
|
||||
msg_info "Building Gluetun"
|
||||
cd /opt/gluetun
|
||||
$STD go mod download
|
||||
CGO_ENABLED=0 $STD go build -trimpath -ldflags="-s -w" -o /usr/local/bin/gluetun ./cmd/gluetun/
|
||||
msg_ok "Built Gluetun"
|
||||
|
||||
msg_info "Configuring Gluetun"
|
||||
mkdir -p /opt/gluetun-data
|
||||
touch /etc/alpine-release
|
||||
ln -sf /opt/gluetun-data /gluetun
|
||||
cat <<EOF >/opt/gluetun-data/.env
|
||||
VPN_SERVICE_PROVIDER=custom
|
||||
VPN_TYPE=openvpn
|
||||
OPENVPN_CUSTOM_CONFIG=/opt/gluetun-data/custom.ovpn
|
||||
OPENVPN_USER=
|
||||
OPENVPN_PASSWORD=
|
||||
OPENVPN_PROCESS_USER=root
|
||||
PUID=0
|
||||
PGID=0
|
||||
HTTP_CONTROL_SERVER_ADDRESS=:8000
|
||||
HTTPPROXY=off
|
||||
SHADOWSOCKS=off
|
||||
PPROF_ENABLED=no
|
||||
PPROF_BLOCK_PROFILE_RATE=0
|
||||
PPROF_MUTEX_PROFILE_RATE=0
|
||||
PPROF_HTTP_SERVER_ADDRESS=:6060
|
||||
FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=on
|
||||
HEALTH_SERVER_ADDRESS=127.0.0.1:9999
|
||||
DNS_UPSTREAM_RESOLVERS=cloudflare
|
||||
LOG_LEVEL=info
|
||||
STORAGE_FILEPATH=/gluetun/servers.json
|
||||
PUBLICIP_FILE=/gluetun/ip
|
||||
VPN_PORT_FORWARDING_STATUS_FILE=/gluetun/forwarded_port
|
||||
TZ=UTC
|
||||
EOF
|
||||
msg_ok "Configured Gluetun"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/gluetun.service
|
||||
[Unit]
|
||||
Description=Gluetun VPN Client
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/gluetun-data
|
||||
EnvironmentFile=/opt/gluetun-data/.env
|
||||
UnsetEnvironment=USER
|
||||
ExecStartPre=/bin/sh -c 'rm -f /etc/openvpn/target.ovpn'
|
||||
ExecStart=/usr/local/bin/gluetun
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
AmbientCapabilities=CAP_NET_ADMIN
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now gluetun
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright (c) 2021-2026 community-scripts ORG
|
||||
# Author: johanngrobe
|
||||
# License: MIT | https://github.com/community-scripts/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/oss-apps/split-pro
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
NODE_VERSION="22" NODE_MODULE="pnpm" setup_nodejs
|
||||
PG_VERSION="17" PG_MODULES="cron" setup_postgresql
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y openssl
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
PG_DB_NAME="splitpro" PG_DB_USER="splitpro" PG_DB_EXTENSIONS="pg_cron" setup_postgresql_db
|
||||
fetch_and_deploy_gh_release "split-pro" "oss-apps/split-pro" "tarball"
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
cd /opt/split-pro
|
||||
$STD pnpm install --frozen-lockfile
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
msg_info "Building Split Pro"
|
||||
cd /opt/split-pro
|
||||
mkdir -p /opt/split-pro_data/uploads
|
||||
ln -sf /opt/split-pro_data/uploads /opt/split-pro/uploads
|
||||
NEXTAUTH_SECRET=$(openssl rand -base64 32)
|
||||
cp .env.example .env
|
||||
sed -i "s|^DATABASE_URL=.*|DATABASE_URL=\"postgresql://${PG_DB_USER}:${PG_DB_PASS}@localhost:5432/${PG_DB_NAME}\"|" .env
|
||||
sed -i "s|^NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=\"${NEXTAUTH_SECRET}\"|" .env
|
||||
sed -i "s|^NEXTAUTH_URL=.*|NEXTAUTH_URL=\"http://${LOCAL_IP}:3000\"|" .env
|
||||
sed -i "s|^NEXTAUTH_URL_INTERNAL=.*|NEXTAUTH_URL_INTERNAL=\"http://localhost:3000\"|" .env
|
||||
sed -i "/^POSTGRES_CONTAINER_NAME=/d" .env
|
||||
sed -i "/^POSTGRES_USER=/d" .env
|
||||
sed -i "/^POSTGRES_PASSWORD=/d" .env
|
||||
sed -i "/^POSTGRES_DB=/d" .env
|
||||
sed -i "/^POSTGRES_PORT=/d" .env
|
||||
$STD pnpm build
|
||||
$STD pnpm exec prisma migrate deploy
|
||||
msg_ok "Built Split Pro"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/split-pro.service
|
||||
[Unit]
|
||||
Description=Split Pro
|
||||
After=network.target postgresql.service
|
||||
Requires=postgresql.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/split-pro
|
||||
EnvironmentFile=/opt/split-pro/.env
|
||||
ExecStart=/usr/bin/pnpm start
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now split-pro
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+9
-2
@@ -39,9 +39,16 @@ $STD uv venv --python 3.12
|
||||
source .venv/bin/activate
|
||||
msg_ok "Created Virtual Environment"
|
||||
|
||||
msg_info "Installing PyTorch"
|
||||
# Install PyTorch first (required by unsloth)
|
||||
# Use CPU version for broader compatibility; GPU version can be installed manually if needed
|
||||
$STD uv pip install torch --index-url https://download.pytorch.org/whl/cpu
|
||||
msg_ok "Installed PyTorch"
|
||||
|
||||
msg_info "Installing Unsloth"
|
||||
# Install unsloth with torch backend auto-detection (GPU drivers must be installed first)
|
||||
$STD uv pip install unsloth --torch-backend=auto
|
||||
# Install unsloth and its dependencies
|
||||
# packaging module is required but not declared as dependency
|
||||
$STD uv pip install unsloth packaging
|
||||
msg_ok "Installed Unsloth"
|
||||
|
||||
msg_info "Running Unsloth Studio Setup"
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# 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/FuzzyGrim/Yamtrack
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt install -y \
|
||||
nginx \
|
||||
redis-server
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
PG_VERSION="16" setup_postgresql
|
||||
PG_DB_NAME="yamtrack" PG_DB_USER="yamtrack" setup_postgresql_db
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
|
||||
fetch_and_deploy_gh_release "yamtrack" "FuzzyGrim/Yamtrack" "tarball"
|
||||
|
||||
msg_info "Installing Python Dependencies"
|
||||
cd /opt/yamtrack
|
||||
$STD uv venv .venv
|
||||
$STD uv pip install --no-cache-dir -r requirements.txt
|
||||
msg_ok "Installed Python Dependencies"
|
||||
|
||||
msg_info "Configuring Yamtrack"
|
||||
SECRET=$(openssl rand -hex 32)
|
||||
cat <<EOF >/opt/yamtrack/src/.env
|
||||
SECRET=${SECRET}
|
||||
DB_HOST=localhost
|
||||
DB_NAME=${PG_DB_NAME}
|
||||
DB_USER=${PG_DB_USER}
|
||||
DB_PASSWORD=${PG_DB_PASS}
|
||||
DB_PORT=5432
|
||||
REDIS_URL=redis://localhost:6379
|
||||
URLS=http://${LOCAL_IP}:8000
|
||||
EOF
|
||||
|
||||
cd /opt/yamtrack/src
|
||||
$STD /opt/yamtrack/.venv/bin/python manage.py migrate
|
||||
$STD /opt/yamtrack/.venv/bin/python manage.py collectstatic --noinput
|
||||
msg_ok "Configured Yamtrack"
|
||||
|
||||
msg_info "Configuring Nginx"
|
||||
rm -f /etc/nginx/sites-enabled/default /etc/nginx/sites-available/default
|
||||
cp /opt/yamtrack/nginx.conf /etc/nginx/nginx.conf
|
||||
sed -i 's|user abc;|user www-data;|' /etc/nginx/nginx.conf
|
||||
sed -i 's|pid /tmp/nginx.pid;|pid /run/nginx.pid;|' /etc/nginx/nginx.conf
|
||||
sed -i 's|/yamtrack/staticfiles/|/opt/yamtrack/src/staticfiles/|' /etc/nginx/nginx.conf
|
||||
sed -i 's|error_log /dev/stderr|error_log /var/log/nginx/error.log|' /etc/nginx/nginx.conf
|
||||
sed -i 's|access_log /dev/stdout|access_log /var/log/nginx/access.log|' /etc/nginx/nginx.conf
|
||||
$STD nginx -t
|
||||
systemctl enable -q nginx
|
||||
$STD systemctl restart nginx
|
||||
msg_ok "Configured Nginx"
|
||||
|
||||
msg_info "Creating Services"
|
||||
cat <<EOF >/etc/systemd/system/yamtrack.service
|
||||
[Unit]
|
||||
Description=Yamtrack Gunicorn
|
||||
After=network.target postgresql.service redis-server.service
|
||||
Requires=postgresql.service redis-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/yamtrack/src
|
||||
ExecStart=/opt/yamtrack/.venv/bin/gunicorn config.wsgi:application -b 127.0.0.1:8001 -w 2 --timeout 120
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
cat <<EOF >/etc/systemd/system/yamtrack-celery.service
|
||||
[Unit]
|
||||
Description=Yamtrack Celery Worker
|
||||
After=network.target postgresql.service redis-server.service yamtrack.service
|
||||
Requires=postgresql.service redis-server.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/yamtrack/src
|
||||
ExecStart=/opt/yamtrack/.venv/bin/celery -A config worker --beat --scheduler django --loglevel INFO
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl enable -q --now redis-server yamtrack yamtrack-celery
|
||||
msg_ok "Created Services"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+16
-8
@@ -48,7 +48,8 @@ jobs:
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const url = require('url');
|
||||
function request(fullUrl, opts) {
|
||||
function request(fullUrl, opts, redirectCount) {
|
||||
redirectCount = redirectCount || 0;
|
||||
return new Promise(function(resolve, reject) {
|
||||
const u = url.parse(fullUrl);
|
||||
const isHttps = u.protocol === 'https:';
|
||||
@@ -63,6 +64,13 @@ jobs:
|
||||
if (body) options.headers['Content-Length'] = Buffer.byteLength(body);
|
||||
const lib = isHttps ? https : http;
|
||||
const req = lib.request(options, function(res) {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
if (redirectCount >= 5) return reject(new Error('Too many redirects from ' + fullUrl));
|
||||
const redirectUrl = url.resolve(fullUrl, res.headers.location);
|
||||
res.resume();
|
||||
resolve(request(redirectUrl, opts, redirectCount + 1));
|
||||
return;
|
||||
}
|
||||
let data = '';
|
||||
res.on('data', function(chunk) { data += chunk; });
|
||||
res.on('end', function() {
|
||||
@@ -125,15 +133,15 @@ jobs:
|
||||
var osVersionToId = {};
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_note_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) noteTypeToId[item.type] = item.id; });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) { noteTypeToId[item.type] = item.id; noteTypeToId[item.type.toLowerCase()] = item.id; } });
|
||||
} catch (e) { console.warn('z_ref_note_types:', e.message); }
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_install_method_types/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) installMethodTypeToId[item.type] = item.id; });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.type != null) { installMethodTypeToId[item.type] = item.id; installMethodTypeToId[item.type.toLowerCase()] = item.id; } });
|
||||
} catch (e) { console.warn('z_ref_install_method_types:', e.message); }
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_os/records?perPage=500', { headers: { 'Authorization': token } });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) osToId[item.os] = item.id; });
|
||||
if (res.ok) JSON.parse(res.body).items?.forEach(function(item) { if (item.os != null) { osToId[item.os] = item.id; osToId[item.os.toLowerCase()] = item.id; } });
|
||||
} catch (e) { console.warn('z_ref_os:', e.message); }
|
||||
try {
|
||||
const res = await request(apiBase + '/collections/z_ref_os_version/records?perPage=500&expand=os', { headers: { 'Authorization': token } });
|
||||
@@ -154,7 +162,7 @@ jobs:
|
||||
name: data.name,
|
||||
slug: data.slug,
|
||||
script_created: data.date_created || data.script_created,
|
||||
script_updated: data.date_created || data.script_updated,
|
||||
script_updated: new Date().toISOString().split('T')[0],
|
||||
updateable: data.updateable,
|
||||
privileged: data.privileged,
|
||||
port: data.interface_port != null ? data.interface_port : data.port,
|
||||
@@ -163,8 +171,8 @@ jobs:
|
||||
logo: data.logo,
|
||||
description: data.description,
|
||||
config_path: data.config_path,
|
||||
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user,
|
||||
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd,
|
||||
default_user: (data.default_credentials && data.default_credentials.username) || data.default_user || null,
|
||||
default_passwd: (data.default_credentials && data.default_credentials.password) || data.default_passwd || null,
|
||||
is_dev: false
|
||||
};
|
||||
var resolvedType = typeValueToId[data.type];
|
||||
@@ -190,7 +198,7 @@ jobs:
|
||||
var postRes = await request(notesCollUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text: note.text || '', type: typeId })
|
||||
body: JSON.stringify({ text: note.text || '', type: typeId, script: scriptId })
|
||||
});
|
||||
if (postRes.ok) noteIds.push(JSON.parse(postRes.body).id);
|
||||
}
|
||||
|
||||
+100
-50
@@ -3,12 +3,12 @@ name: Sync Upstream (Git Merge Strategy)
|
||||
on:
|
||||
schedule:
|
||||
# Runs automatically every day at 2:00 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
- cron: "0 2 * * *"
|
||||
workflow_dispatch:
|
||||
# Allows manual trigger from the Actions tab
|
||||
inputs:
|
||||
force_sync:
|
||||
description: 'Force sync even if no new commits detected'
|
||||
description: "Force sync even if no new commits detected"
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
@@ -48,7 +48,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
|
||||
# Configure the 'ours' merge driver for fork-specific files
|
||||
# This tells git to use our version for files marked with merge=ours in .gitattributes
|
||||
git config merge.ours.name "ours merge driver"
|
||||
@@ -64,17 +64,17 @@ jobs:
|
||||
run: |
|
||||
# Get the merge base between fork and upstream
|
||||
MERGE_BASE=$(git merge-base HEAD upstream/${{ env.UPSTREAM_BRANCH }} 2>/dev/null || echo "")
|
||||
|
||||
|
||||
if [ -z "$MERGE_BASE" ]; then
|
||||
echo "No common ancestor found - this may be a fresh sync"
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "commit_count=unknown" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
# Count commits in upstream that are not in fork
|
||||
UPSTREAM_COMMITS=$(git rev-list $MERGE_BASE..upstream/${{ env.UPSTREAM_BRANCH }} --count 2>/dev/null || echo "0")
|
||||
|
||||
|
||||
if [ "$UPSTREAM_COMMITS" -eq 0 ] && [ "${{ github.event.inputs.force_sync }}" != "true" ]; then
|
||||
echo "No new commits from upstream. Nothing to sync."
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
@@ -113,7 +113,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
|
||||
# Configure the 'ours' merge driver
|
||||
# Files with merge=ours in .gitattributes will automatically keep fork version
|
||||
git config merge.ours.name "ours merge driver"
|
||||
@@ -135,14 +135,14 @@ jobs:
|
||||
id: merge
|
||||
run: |
|
||||
echo "Starting merge from upstream/${{ env.UPSTREAM_BRANCH }}..."
|
||||
|
||||
|
||||
# Attempt merge with the 'ours' strategy for conflicts
|
||||
# The .gitattributes file configures which files use merge=ours
|
||||
git merge upstream/${{ env.UPSTREAM_BRANCH }} \
|
||||
--no-edit \
|
||||
-m "Merge upstream ${{ env.UPSTREAM_REPO }} into ${{ env.FORK_BRANCH }}" \
|
||||
2>&1 || MERGE_STATUS=$?
|
||||
|
||||
|
||||
if [ "${MERGE_STATUS:-0}" -eq 0 ]; then
|
||||
echo "Merge completed successfully with no conflicts."
|
||||
echo "merge_status=success" >> $GITHUB_OUTPUT
|
||||
@@ -152,29 +152,70 @@ jobs:
|
||||
echo "merge_status=conflicts" >> $GITHUB_OUTPUT
|
||||
echo "has_conflicts=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# List conflicted files
|
||||
# List all conflicted files
|
||||
CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
|
||||
echo "conflicted_files<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONFLICTS" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# Check if conflicts are only in fork-specific files
|
||||
# These should have been handled by merge=ours, but check anyway
|
||||
FORK_FILES=$(git diff --name-only --diff-filter=U \
|
||||
-- 'install/' 'ct/' 'frontend/' 'misc/images/' '.github/workflows/' '.github/runner/' 'tools/addon/' 'README.md' 'CHANGELOG.md' 2>/dev/null || echo "")
|
||||
echo "Resolving conflicts..."
|
||||
|
||||
if [ -n "$FORK_FILES" ]; then
|
||||
echo "Warning: Fork-specific files have unexpected conflicts:"
|
||||
echo "$FORK_FILES"
|
||||
# For fork files, always take our version
|
||||
for file in $FORK_FILES; do
|
||||
# 1. Handle content conflicts in fork-specific files - always keep our version
|
||||
# These are files that exist in both but have different content
|
||||
FORK_CONTENT_FILES=$(git diff --name-only --diff-filter=U \
|
||||
-- 'CHANGELOG.md' 'misc/tools.func' 'README.md' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$FORK_CONTENT_FILES" ]; then
|
||||
echo "Resolving content conflicts in fork-specific files:"
|
||||
echo "$FORK_CONTENT_FILES"
|
||||
for file in $FORK_CONTENT_FILES; do
|
||||
echo " Keeping fork version: $file"
|
||||
git checkout --ours "$file" 2>/dev/null || true
|
||||
git add "$file" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
# For other conflicts, we leave them for manual resolution in the PR
|
||||
# This allows reviewers to see what needs attention
|
||||
# 2. Handle modify/delete conflicts - files deleted in fork but modified upstream
|
||||
# We want to keep our deletion (remove the file)
|
||||
MODIFY_DELETE_CONFLICTS=$(git status --porcelain | grep "^DU" | awk '{print $2}' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$MODIFY_DELETE_CONFLICTS" ]; then
|
||||
echo "Resolving modify/delete conflicts (keeping fork deletions):"
|
||||
echo "$MODIFY_DELETE_CONFLICTS"
|
||||
for file in $MODIFY_DELETE_CONFLICTS; do
|
||||
echo " Removing file (fork deleted it): $file"
|
||||
git rm --ignore-unmatch "$file" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
# 3. Handle any remaining content conflicts in fork directories
|
||||
REMAINING_CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$REMAINING_CONFLICTS" ]; then
|
||||
echo "Resolving remaining conflicts in fork directories:"
|
||||
echo "$REMAINING_CONFLICTS"
|
||||
|
||||
# For files in fork-specific directories, keep our version
|
||||
FORK_DIR_CONFLICTS=$(echo "$REMAINING_CONFLICTS" | grep -E '^(install/|ct/|frontend/|misc/images/|tools/addon/)' || true)
|
||||
|
||||
if [ -n "$FORK_DIR_CONFLICTS" ]; then
|
||||
for file in $FORK_DIR_CONFLICTS; do
|
||||
echo " Keeping fork version: $file"
|
||||
git checkout --ours "$file" 2>/dev/null || true
|
||||
git add "$file" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check if there are still unresolved conflicts
|
||||
REMAINING=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
|
||||
if [ -n "$REMAINING" ]; then
|
||||
echo "Warning: Some conflicts could not be auto-resolved:"
|
||||
echo "$REMAINING"
|
||||
echo "These will need manual resolution in the PR."
|
||||
else
|
||||
echo "All conflicts resolved successfully."
|
||||
fi
|
||||
else
|
||||
echo "Merge failed with status: ${MERGE_STATUS}"
|
||||
echo "merge_status=failed" >> $GITHUB_OUTPUT
|
||||
@@ -188,22 +229,28 @@ jobs:
|
||||
echo "upstream_commits<<EOF" >> $GITHUB_OUTPUT
|
||||
git log HEAD..upstream/${{ env.UPSTREAM_BRANCH }} --oneline --no-merges | head -30 >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Get changed files summary
|
||||
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
|
||||
git diff --name-only HEAD upstream/${{ env.UPSTREAM_BRANCH }} | head -50 >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Count files by category
|
||||
TOTAL_CHANGED=$(git diff --name-only HEAD upstream/${{ env.UPSTREAM_BRANCH }} | wc -l)
|
||||
echo "total_changed=$TOTAL_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Commit merge result
|
||||
if: steps.merge.outputs.merge_status == 'conflicts'
|
||||
run: |
|
||||
# Commit the merge with resolved fork-specific conflicts
|
||||
# Remaining conflicts will be visible in the PR for review
|
||||
git commit -m "Merge upstream with partial conflict resolution" -m "Fork-specific files preserved, other conflicts need review" || true
|
||||
# Check if there are staged changes to commit
|
||||
if git diff --cached --quiet 2>/dev/null && git diff --quiet 2>/dev/null; then
|
||||
echo "No changes to commit."
|
||||
else
|
||||
# Commit the merge with resolved conflicts
|
||||
git commit -m "Merge upstream ${{ env.UPSTREAM_REPO }} with conflict resolution" \
|
||||
-m "- Fork-specific files preserved (CHANGELOG.md, misc/tools.func)" \
|
||||
-m "- Deleted files kept as deleted (fork deletions preserved)" \
|
||||
-m "- New upstream scripts added" || echo "Commit may have already been made"
|
||||
fi
|
||||
|
||||
- name: Push sync branch
|
||||
run: |
|
||||
@@ -216,56 +263,60 @@ jobs:
|
||||
BRANCH="${{ steps.branch.outputs.branch_name }}"
|
||||
HAS_CONFLICTS="${{ steps.merge.outputs.has_conflicts }}"
|
||||
CONFLICTED_FILES="${{ steps.merge.outputs.conflicted_files }}"
|
||||
|
||||
|
||||
# Build PR body
|
||||
PR_BODY="## 🔄 Upstream Sync Summary
|
||||
|
||||
|
||||
This PR syncs the latest changes from upstream [\`${{ env.UPSTREAM_REPO }}\`](https://github.com/${{ env.UPSTREAM_REPO }}) while preserving fork-specific customizations.
|
||||
|
||||
### 📊 Sync Statistics
|
||||
- **Upstream commits:** ${{ needs.check-and-sync.outputs.commit_count }}
|
||||
- **Files changed:** ${{ steps.summary.outputs.total_changed }}
|
||||
- **Merge conflicts:** ${HAS_CONFLICTS:-none}
|
||||
|
||||
|
||||
### 📋 Upstream Commits (last 30)
|
||||
\`\`\`
|
||||
${{ steps.summary.outputs.upstream_commits }}
|
||||
\`\`\`
|
||||
|
||||
|
||||
### 📁 Changed Files
|
||||
<details>
|
||||
<summary>Click to view changed files</summary>
|
||||
|
||||
|
||||
\`\`\`
|
||||
${{ steps.summary.outputs.changed_files }}
|
||||
\`\`\`
|
||||
</details>
|
||||
"
|
||||
|
||||
# Add conflict warning if needed
|
||||
|
||||
# Add conflict resolution section
|
||||
if [ "$HAS_CONFLICTS" = "true" ]; then
|
||||
PR_BODY="$PR_BODY
|
||||
### ⚠️ Merge Conflicts Detected
|
||||
|
||||
The following files have conflicts that need manual review:
|
||||
### ⚠️ Conflicts Auto-Resolved
|
||||
|
||||
The following conflict resolution strategy was applied:
|
||||
|
||||
1. **Fork-specific content files** (CHANGELOG.md, misc/tools.func, README.md) - Kept fork version
|
||||
2. **Modify/delete conflicts** - Kept fork's deletions (files removed in fork stay removed)
|
||||
3. **Fork directory conflicts** (install/, ct/, frontend/) - Kept fork version
|
||||
|
||||
Original conflicted files:
|
||||
\`\`\`
|
||||
$CONFLICTED_FILES
|
||||
\`\`\`
|
||||
|
||||
**Note:** Fork-specific files have been automatically preserved using the \`merge=ours\` strategy configured in \`.gitattributes\`.
|
||||
"
|
||||
else
|
||||
PR_BODY="$PR_BODY
|
||||
### ✅ Clean Merge
|
||||
|
||||
No conflicts detected. Fork-specific files were automatically preserved using the \`merge=ours\` strategy.
|
||||
|
||||
No conflicts detected. All changes merged cleanly.
|
||||
"
|
||||
fi
|
||||
|
||||
|
||||
# Add preserved files section
|
||||
PR_BODY="$PR_BODY
|
||||
### 🔒 Fork-Specific Files Preserved
|
||||
|
||||
|
||||
The following files/directories are configured in \`.gitattributes\` to always keep the fork version:
|
||||
- \`install/\` - Custom install scripts
|
||||
- \`ct/\` - Custom container scripts
|
||||
@@ -276,25 +327,24 @@ jobs:
|
||||
- \`tools/addon/\` - Custom addon tools
|
||||
- \`README.md\` - Fork-specific documentation
|
||||
- \`CHANGELOG.md\` - Fork-specific changelog
|
||||
|
||||
|
||||
### ✅ Pre-Merge Checklist
|
||||
- [ ] Review all changed files for unexpected modifications
|
||||
- [ ] Verify fork-specific files are intact
|
||||
- [ ] Resolve any remaining merge conflicts
|
||||
- [ ] Test any new scripts or features from upstream
|
||||
|
||||
|
||||
---
|
||||
*This PR was automatically created by the [upstream-sync workflow](https://github.com/${{ github.repository }}/blob/main/.github/workflows/upstream-sync.yml).*
|
||||
"
|
||||
|
||||
|
||||
# Create PR title with conflict indicator
|
||||
if [ "$HAS_CONFLICTS" = "true" ]; then
|
||||
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d) ⚠️ CONFLICTS"
|
||||
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d) ✅ Conflicts Resolved"
|
||||
else
|
||||
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d)"
|
||||
fi
|
||||
|
||||
# Create the PR (labels are optional - will fail silently if labels don't exist)
|
||||
|
||||
# Create the PR
|
||||
gh pr create \
|
||||
--title "$TITLE" \
|
||||
--body "$PR_BODY" \
|
||||
|
||||
+2
@@ -420,6 +420,8 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-03-18
|
||||
|
||||
## 2026-03-16
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
+6
-6
@@ -1,6 +1,6 @@
|
||||
__ __ __
|
||||
/ /____ __ __________/ /___ _/ /___ _______ __
|
||||
/ __/ _ \/ / / / ___/ __ / __ `/ __/ / / / ___/ / /
|
||||
/ /_/ __/ /_/ / / / /_/ / /_/ / /_/ /_/ (__ )_/ /
|
||||
\__/\___/\__,_/_/ \__,_/\__,_/\__/\__,_/____(_)
|
||||
|
||||
__ _ ____
|
||||
_____/ /__(_) / /_______ ______ _____ _____
|
||||
/ ___/ //_/ / / / ___/ _ \/ ___/ | / / _ \/ ___/
|
||||
(__ ) ,< / / / (__ ) __/ / | |/ / __/ /
|
||||
/____/_/|_/_/_/_/____/\___/_/ |___/\___/_/
|
||||
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-03-15T12:22:39Z",
|
||||
"generated": "2026-03-16T18:42:54Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "agregarr",
|
||||
@@ -18,9 +18,9 @@
|
||||
{
|
||||
"slug": "llamacpp",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"version": "b8354",
|
||||
"version": "b8373",
|
||||
"pinned": false,
|
||||
"date": "2026-03-15T10:06:38Z"
|
||||
"date": "2026-03-16T10:55:12Z"
|
||||
},
|
||||
{
|
||||
"slug": "localai",
|
||||
@@ -46,9 +46,9 @@
|
||||
{
|
||||
"slug": "pegaprox",
|
||||
"repo": "PegaProx/project-pegaprox",
|
||||
"version": "v0.9.1.3",
|
||||
"version": "v0.9.2",
|
||||
"pinned": false,
|
||||
"date": "2026-03-11T20:23:52Z"
|
||||
"date": "2026-03-16T08:00:29Z"
|
||||
},
|
||||
{
|
||||
"slug": "ragflow",
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ update_os
|
||||
|
||||
# Setup GPU hardware acceleration (detects GPU, installs drivers, configures permissions)
|
||||
# This handles NVIDIA, AMD/ROCm, and Intel GPU detection and driver installation
|
||||
setup_hardware_acceleration
|
||||
setup_hwaccel
|
||||
|
||||
fetch_and_deploy_gh_release "lemonade" "lemonade-sdk/lemonade" "binary"
|
||||
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ msg_ok "Created Service"
|
||||
|
||||
# Setup GPU hardware acceleration (detects GPU, installs drivers, configures permissions)
|
||||
# This handles NVIDIA, AMD/ROCm, and Intel GPU detection and driver installation
|
||||
setup_hardware_acceleration
|
||||
setup_hwaccel
|
||||
|
||||
# Create GPU passthrough info file
|
||||
cat <<EOF >/opt/llamacpp/GPU_PASSTHROUGH.md
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ update_os
|
||||
|
||||
# Setup GPU hardware acceleration (detects GPU, installs drivers, configures permissions)
|
||||
# This handles NVIDIA, AMD/ROCm, and Intel GPU detection and driver installation
|
||||
setup_hardware_acceleration
|
||||
setup_hwaccel
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt-get install -y curl
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ update_os
|
||||
|
||||
# Setup GPU hardware acceleration (detects GPU, installs drivers, configures permissions)
|
||||
# This handles NVIDIA, AMD/ROCm, and Intel GPU detection and driver installation
|
||||
setup_hardware_acceleration
|
||||
setup_hwaccel
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
setup_deb822_repo \
|
||||
|
||||
+4
-3
@@ -27,7 +27,7 @@ msg_ok "Installed Dependencies"
|
||||
|
||||
# Setup GPU hardware acceleration FIRST (detects GPU, installs drivers, configures permissions)
|
||||
# This must run before installing unsloth/torch so PyTorch can detect the GPU
|
||||
setup_hardware_acceleration
|
||||
setup_hwaccel
|
||||
|
||||
# Setup Python virtual environment with uv (fast Python package manager)
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
@@ -46,7 +46,8 @@ msg_ok "Installed Unsloth"
|
||||
|
||||
msg_info "Running Unsloth Studio Setup"
|
||||
# Run the unsloth studio setup command to compile llama.cpp
|
||||
$STD unsloth studio setup
|
||||
# Use Python module invocation since uv pip install doesn't create entry points
|
||||
$STD /opt/unsolth-studio/.venv/bin/python -m unsloth studio setup
|
||||
msg_ok "Completed Unsloth Studio Setup"
|
||||
|
||||
msg_info "Creating Directories"
|
||||
@@ -67,7 +68,7 @@ Wants=network-online.target
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/unsolth-studio
|
||||
Environment="PATH=/opt/unsolth-studio/.venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
ExecStart=/bin/sh -c 'source /opt/unsolth-studio/.venv/bin/activate && unsloth studio -H 0.0.0.0 -p 8888'
|
||||
ExecStart=/opt/unsolth-studio/.venv/bin/python -m unsloth studio -H 0.0.0.0 -p 8888
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
__ __ __ __ __
|
||||
/ / / /___ _____/ /___ / /_/ /_
|
||||
/ / / / __ \/ ___/ / __ \/ __/ __ \
|
||||
/ /_/ / / / (__ ) / /_/ / /_/ / / /
|
||||
\____/_/ /_/____/_/\____/\__/_/ /_/
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: Heretek-AI
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/unslothai/unsloth
|
||||
|
||||
APP="unsolth-studio"
|
||||
var_tags="${var_tags:-ai;llm;fine-tuning;training}"
|
||||
var_cpu="${var_cpu:-4}"
|
||||
var_ram="${var_ram:-16384}"
|
||||
var_disk="${var_disk:-50}"
|
||||
var_os="${var_os:-debian}"
|
||||
var_version="${var_version:-13}"
|
||||
var_unprivileged="${var_unprivileged:-1}"
|
||||
var_gpu="${var_gpu:-yes}"
|
||||
|
||||
header_info "$APP"
|
||||
variables
|
||||
color
|
||||
catch_errors
|
||||
|
||||
function update_script() {
|
||||
header_info
|
||||
check_container_storage
|
||||
check_container_resources
|
||||
|
||||
if [[ ! -d /opt/unsolth-studio ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if command -v unsloth &>/dev/null; then
|
||||
CURRENT_VERSION=$(pip show unsloth 2>/dev/null | grep -i version | awk '{print $2}' || echo "unknown")
|
||||
msg_info "Current version: ${CURRENT_VERSION}"
|
||||
|
||||
msg_info "Checking for updates"
|
||||
$STD pip install --upgrade unsloth 2>/dev/null
|
||||
|
||||
NEW_VERSION=$(pip show unsloth 2>/dev/null | grep -i version | awk '{print $2}' || echo "unknown")
|
||||
|
||||
if [[ "$CURRENT_VERSION" != "$NEW_VERSION" ]]; then
|
||||
msg_ok "Updated from ${CURRENT_VERSION} to ${NEW_VERSION}"
|
||||
else
|
||||
msg_ok "Already at latest version: ${NEW_VERSION}"
|
||||
fi
|
||||
else
|
||||
msg_error "Unsloth not installed properly"
|
||||
exit 1
|
||||
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}:8888${CL}"
|
||||
echo -e "${INFO}${YW} Note: First launch may take 5-10 minutes to compile llama.cpp${CL}"
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "Unsloth Studio (In Beta)",
|
||||
"slug": "unsolth-studio",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-18",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8888,
|
||||
"documentation": "https://unsloth.ai/docs/new/studio/start",
|
||||
"website": "https://unsloth.ai/",
|
||||
"logo": "https://unsloth.ai/favicon.ico",
|
||||
"config_path": "/opt/unsolth-studio/.env",
|
||||
"description": "Local, browser-based GUI for fine-tuning LLMs without writing code. Supports QLoRA, LoRA, and full fine-tuning with live training monitoring, GPU stats, and model export to GGUF/Safetensors.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/unsolth-studio.sh",
|
||||
"resources": {
|
||||
"cpu": 4,
|
||||
"ram": 16384,
|
||||
"hdd": 50,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Requires GPU passthrough for training. NVIDIA, AMD (ROCm), and Intel GPUs are supported.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "First launch takes 5-10 minutes to compile llama.cpp binaries.",
|
||||
"type": "warning"
|
||||
},
|
||||
{
|
||||
"text": "Minimum 16GB RAM recommended for training models up to 7B parameters.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "For AMD GPUs, ensure ROCm is properly configured on the host.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
+4
@@ -12,6 +12,10 @@ setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
# Setup GPU hardware acceleration (detects GPU, installs drivers, configures permissions)
|
||||
# This handles NVIDIA, AMD/ROCm, and Intel GPU detection and driver installation
|
||||
setup_hardware_acceleration
|
||||
|
||||
fetch_and_deploy_gh_release "lemonade" "lemonade-sdk/lemonade" "binary"
|
||||
|
||||
msg_info "Configuring Service"
|
||||
|
||||
+3
-20
@@ -101,26 +101,9 @@ EOF
|
||||
systemctl enable -q --now llamacpp
|
||||
msg_ok "Created Service"
|
||||
|
||||
msg_info "Configuring GPU Permissions"
|
||||
# Add render and video groups for GPU access
|
||||
usermod -aG render,video root 2>/dev/null || true
|
||||
|
||||
# Configure /dev/kfd and /dev/dri permissions for AMD
|
||||
if [[ -e /dev/kfd ]]; then
|
||||
chgrp render /dev/kfd 2>/dev/null || true
|
||||
chmod 660 /dev/kfd 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [[ -d /dev/dri ]]; then
|
||||
chmod 755 /dev/dri 2>/dev/null || true
|
||||
for render_dev in /dev/dri/renderD*; do
|
||||
if [[ -e "$render_dev" ]]; then
|
||||
chgrp render "$render_dev" 2>/dev/null || true
|
||||
chmod 660 "$render_dev" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
msg_ok "Configured GPU Permissions"
|
||||
# Setup GPU hardware acceleration (detects GPU, installs drivers, configures permissions)
|
||||
# This handles NVIDIA, AMD/ROCm, and Intel GPU detection and driver installation
|
||||
setup_hardware_acceleration
|
||||
|
||||
# Create GPU passthrough info file
|
||||
cat <<EOF >/opt/llamacpp/GPU_PASSTHROUGH.md
|
||||
|
||||
+4
@@ -13,6 +13,10 @@ setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
# Setup GPU hardware acceleration (detects GPU, installs drivers, configures permissions)
|
||||
# This handles NVIDIA, AMD/ROCm, and Intel GPU detection and driver installation
|
||||
setup_hardware_acceleration
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt-get install -y curl
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
+4
@@ -12,6 +12,10 @@ setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
# Setup GPU hardware acceleration (detects GPU, installs drivers, configures permissions)
|
||||
# This handles NVIDIA, AMD/ROCm, and Intel GPU detection and driver installation
|
||||
setup_hardware_acceleration
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
setup_deb822_repo \
|
||||
"microsoft" \
|
||||
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Author: Heretek-AI
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/unslothai/unsloth
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
cmake \
|
||||
build-essential \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
pciutils
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
# Setup GPU hardware acceleration FIRST (detects GPU, installs drivers, configures permissions)
|
||||
# This must run before installing unsloth/torch so PyTorch can detect the GPU
|
||||
setup_hardware_acceleration
|
||||
|
||||
# Setup Python virtual environment with uv (fast Python package manager)
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
|
||||
msg_info "Creating Virtual Environment"
|
||||
mkdir -p /opt/unsolth-studio
|
||||
cd /opt/unsolth-studio || exit
|
||||
$STD uv venv --python 3.12
|
||||
source .venv/bin/activate
|
||||
msg_ok "Created Virtual Environment"
|
||||
|
||||
msg_info "Installing Unsloth"
|
||||
# Install unsloth with torch backend auto-detection (GPU drivers must be installed first)
|
||||
$STD uv pip install unsloth --torch-backend=auto
|
||||
msg_ok "Installed Unsloth"
|
||||
|
||||
msg_info "Running Unsloth Studio Setup"
|
||||
# Run the unsloth studio setup command to compile llama.cpp
|
||||
$STD unsloth studio setup
|
||||
msg_ok "Completed Unsloth Studio Setup"
|
||||
|
||||
msg_info "Creating Directories"
|
||||
mkdir -p /opt/unsolth-studio/models
|
||||
mkdir -p /opt/unsolth-studio/datasets
|
||||
mkdir -p /var/log/unsolth-studio
|
||||
chmod 755 /var/log/unsolth-studio
|
||||
msg_ok "Created Directories"
|
||||
|
||||
msg_info "Creating Service"
|
||||
cat <<EOF >/etc/systemd/system/unsolth-studio.service
|
||||
[Unit]
|
||||
Description=Unsloth Studio - Local LLM Fine-tuning Web UI
|
||||
After=network.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/opt/unsolth-studio
|
||||
Environment="PATH=/opt/unsolth-studio/.venv/bin:/usr/local/bin:/usr/bin:/bin"
|
||||
ExecStart=/bin/sh -c 'source /opt/unsolth-studio/.venv/bin/activate && unsloth studio -H 0.0.0.0 -p 8888'
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=unsolth-studio
|
||||
|
||||
# Resource limits
|
||||
LimitNOFILE=65535
|
||||
TimeoutStartSec=600
|
||||
TimeoutStopSec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now unsolth-studio
|
||||
msg_ok "Created Service"
|
||||
|
||||
# Create GPU passthrough info file
|
||||
cat <<EOF >/opt/unsolth-studio/GPU_PASSTHROUGH.md
|
||||
# GPU Passthrough Configuration for Unsloth Studio
|
||||
|
||||
This container has been configured for GPU acceleration for LLM fine-tuning.
|
||||
|
||||
## Required Proxmox Configuration
|
||||
|
||||
Add the following lines to your container config file:
|
||||
/etc/pve/lxc/<CTID>.conf
|
||||
|
||||
### For NVIDIA GPUs:
|
||||
\`\`\`
|
||||
# Requires nvidia-container-toolkit on host
|
||||
lxc.cgroup2.devices.allow: c 195:* rwm
|
||||
lxc.cgroup2.devices.allow: c 509:* rwm
|
||||
dev0: /dev/nvidia0,gid=104
|
||||
dev1: /dev/nvidiactl,gid=104
|
||||
dev2: /dev/nvidia-uvm,gid=104
|
||||
dev3: /dev/nvidia-uvm-tools,gid=104
|
||||
\`\`\`
|
||||
|
||||
### For AMD GPUs (ROCm):
|
||||
\`\`\`
|
||||
dev0: /dev/kfd,gid=104
|
||||
dev1: /dev/dri/renderD128,gid=104
|
||||
lxc.cgroup2.devices.allow: c 226:0 rwm
|
||||
lxc.cgroup2.devices.allow: c 226:128 rwm
|
||||
\`\`\`
|
||||
|
||||
### For Intel GPUs:
|
||||
\`\`\`
|
||||
dev0: /dev/dri/renderD128,gid=104
|
||||
lxc.cgroup2.devices.allow: c 226:128 rwm
|
||||
\`\`\`
|
||||
|
||||
## Verify GPU Access
|
||||
|
||||
Run these commands inside the container:
|
||||
- nvidia-smi (NVIDIA GPUs)
|
||||
- rocminfo (AMD GPUs)
|
||||
- python -c "import torch; print(torch.cuda.is_available())"
|
||||
|
||||
## Usage
|
||||
|
||||
Access the web UI at: http://<IP>:8888
|
||||
|
||||
On first launch:
|
||||
1. Create a password to secure your account
|
||||
2. Follow the onboarding wizard to select a model and dataset
|
||||
3. Configure training parameters
|
||||
4. Start fine-tuning!
|
||||
|
||||
## Supported Models
|
||||
|
||||
Unsloth Studio supports fine-tuning many LLM models including:
|
||||
- Llama 3.x
|
||||
- Qwen 2.x / 3.x
|
||||
- Mistral
|
||||
- Gemma
|
||||
- Phi-3
|
||||
- And many more...
|
||||
|
||||
## Documentation
|
||||
|
||||
- Official Docs: https://unsloth.ai/docs/new/studio/start
|
||||
- GitHub: https://github.com/unslothai/unsloth
|
||||
EOF
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+1
@@ -3586,6 +3586,7 @@ build_container() {
|
||||
fi
|
||||
|
||||
# Core exports for install.func
|
||||
export COMMUNITY_SCRIPTS_URL="$COMMUNITY_SCRIPTS_URL"
|
||||
export DIAGNOSTICS="$DIAGNOSTICS"
|
||||
export RANDOM_UUID="$RANDOM_UUID"
|
||||
export EXECUTION_ID="$EXECUTION_ID"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
|
||||
source <(curl -fsSL ""${COMMUNITY_SCRIPTS_URL"}"/misc/build.func)
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: BillyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/mcmonkeyprojects/SwarmUI
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
__ __ __
|
||||
/ /____ __ __________/ /___ _/ /___ _______ __
|
||||
/ __/ _ \/ / / / ___/ __ / __ `/ __/ / / / ___/ / /
|
||||
/ /_/ __/ /_/ / / / /_/ / /_/ / /_/ /_/ (__ )_/ /
|
||||
\__/\___/\__,_/_/ \__,_/\__,_/\__/\__,_/____(_)
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env bash
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: BillyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/mudler/skillserver
|
||||
|
||||
APP="skillserver"
|
||||
var_tags="${var_tags:-ai;mcp;skills;agents}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
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 [[ ! -f /usr/local/bin/skillserver ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "skillserver" "mudler/skillserver"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop skillserver
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp -r /opt/skillserver/skills /opt/skillserver_skills_backup
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
setup_go
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "skillserver" "mudler/skillserver" "tarball" "latest" "/opt/skillserver"
|
||||
|
||||
msg_info "Building Application"
|
||||
cd /opt/skillserver || exit
|
||||
$STD go build -o /usr/local/bin/skillserver ./cmd/skillserver
|
||||
msg_ok "Built Application"
|
||||
|
||||
msg_info "Restoring Data"
|
||||
cp -r /opt/skillserver_skills_backup/. /opt/skillserver/skills
|
||||
rm -rf /opt/skillserver_skills_backup
|
||||
msg_ok "Restored Data"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start skillserver
|
||||
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}"
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"name": "SkillServer (In Development)",
|
||||
"slug": "skillserver",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-17",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8080,
|
||||
"documentation": "https://github.com/mudler/skillserver",
|
||||
"website": "https://github.com/mudler/skillserver",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/skillserver.webp",
|
||||
"config_path": "",
|
||||
"description": "An MCP/REST server with WebUI serving as a centralized skills database for AI Agents. Manages 'Skills' (directory-based with SKILL.md files) stored locally, following the Agent Skills specification. Features MCP server integration, web interface for skill management, Git synchronization, and full-text search.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/skillserver.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Access the WebUI at http://SERVER_IP:8080",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Skills are stored in /opt/skillserver/skills",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Configure Git repositories to sync skills via SKILLSERVER_GIT_REPOS environment variable",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "MCP server runs over stdio for integration with AI clients",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Author: BillyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/mudler/skillserver
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt-get install -y \
|
||||
git \
|
||||
ca-certificates
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
setup_go
|
||||
|
||||
fetch_and_deploy_gh_release "skillserver" "mudler/skillserver" "tarball" "latest" "/opt/skillserver"
|
||||
|
||||
msg_info "Building Application"
|
||||
cd /opt/skillserver || exit
|
||||
$STD go build -o /usr/local/bin/skillserver ./cmd/skillserver
|
||||
msg_ok "Built Application"
|
||||
|
||||
msg_info "Creating Service"
|
||||
mkdir -p /opt/skillserver/skills
|
||||
cat <<EOF >/etc/systemd/system/skillserver.service
|
||||
[Unit]
|
||||
Description=SkillServer - MCP/REST server for AI agent skills
|
||||
After=network.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/skillserver
|
||||
# Use tail -f /dev/null to keep stdin open for the MCP stdio server
|
||||
# skillserver runs both MCP stdio (main thread) and web server (goroutine)
|
||||
# The MCP server needs stdin to stay open, otherwise it exits immediately
|
||||
ExecStart=/bin/sh -c 'tail -f /dev/null | /usr/local/bin/skillserver --enable-logging'
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
Environment=SKILLSERVER_DIR=/opt/skillserver/skills
|
||||
Environment=SKILLSERVER_PORT=8080
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable -q --now skillserver
|
||||
msg_ok "Created Service"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+2
@@ -420,6 +420,8 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-03-16
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
## 2026-03-14
|
||||
|
||||
+6
-10
@@ -1,10 +1,6 @@
|
||||
/$$$$$$$$/$$$$$$$$ /$$ /$$ /$$$$$$$$ /$$$$$$$$
|
||||
|_____ $$|__ $$__/| $$$ | $$| $$_____/|__ $$__/
|
||||
/$$/ | $$ | $$$$| $$| $$ | $$
|
||||
/$$/ | $$ | $$ $$ $$| $$$$$ | $$
|
||||
/$$/ | $$ | $$ $$$$| $$__/ | $$
|
||||
/$$/ | $$ | $$\ $$$| $$ | $$
|
||||
/$$$$$$$$ | $$ | $$ \ $$| $$$$$$$$ | $$
|
||||
|________/ |__/ |__/ \__/|________/ |__/
|
||||
|
||||
ZeroTier Network Controller Web UI
|
||||
_____ _______ __ __
|
||||
/__ //_ __/ | / /__ / /_
|
||||
/ / / / / |/ / _ \/ __/
|
||||
/ /__/ / / /| / __/ /_
|
||||
/____/_/ /_/ |_/\___/\__/
|
||||
|
||||
|
||||
+6
-10
@@ -1,10 +1,6 @@
|
||||
/$$$$$$$$/$$$$$$$$ /$$ /$$ /$$$$$$$$ /$$$$$$$$
|
||||
|_____ $$|__ $$__/| $$$ | $$| $$_____/|__ $$__/
|
||||
/$$/ | $$ | $$$$| $$| $$ | $$
|
||||
/$$/ | $$ | $$ $$ $$| $$$$$ | $$
|
||||
/$$/ | $$ | $$ $$$$| $$__/ | $$
|
||||
/$$/ | $$ | $$\ $$$| $$ | $$
|
||||
/$$$$$$$$ | $$ | $$ \ $$| $$$$$$$$ | $$
|
||||
|________/ |__/ |__/ \__/|________/ |__/
|
||||
|
||||
ZeroTier Network Controller Web UI
|
||||
_____ _______ __ __
|
||||
/__ //_ __/ | / /__ / /_
|
||||
/ / / / / |/ / _ \/ __/
|
||||
/ /__/ / / /| / __/ /_
|
||||
/____/_/ /_/ |_/\___/\__/
|
||||
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/$$$$$$$$/$$$$$$$$ /$$ /$$ /$$$$$$$$ /$$$$$$$$
|
||||
|_____ $$|__ $$__/| $$$ | $$| $$_____/|__ $$__/
|
||||
/$$/ | $$ | $$$$| $$| $$ | $$
|
||||
/$$/ | $$ | $$ $$ $$| $$$$$ | $$
|
||||
/$$/ | $$ | $$ $$$$| $$__/ | $$
|
||||
/$$/ | $$ | $$\ $$$| $$ | $$
|
||||
/$$$$$$$$ | $$ | $$ \ $$| $$$$$$$$ | $$
|
||||
|________/ |__/ |__/ \__/|________/ |__/
|
||||
|
||||
ZeroTier Network Controller Web UI
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: BillyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://ztnet.network
|
||||
|
||||
APP="ZTNet"
|
||||
var_tags="${var_tags:-network;vpn;zerotier}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
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/ztnet ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop ztnet
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp -r /opt/ztnet/data /opt/ztnet_data_backup 2>/dev/null || true
|
||||
cp /opt/ztnet/.env /opt/ztnet_env_backup 2>/dev/null || true
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
msg_info "Updating ZTNet"
|
||||
curl -s http://install.ztnet.network | bash
|
||||
msg_ok "Updated ZTNet"
|
||||
|
||||
msg_info "Restoring Data"
|
||||
cp -r /opt/ztnet_data_backup/. /opt/ztnet/data 2>/dev/null || true
|
||||
cp /opt/ztnet_env_backup /opt/ztnet/.env 2>/dev/null || true
|
||||
rm -rf /opt/ztnet_data_backup /opt/ztnet_env_backup
|
||||
msg_ok "Restored Data"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start ztnet
|
||||
msg_ok "Started Service"
|
||||
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 it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:3000${CL}"
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Author: BillyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://ztnet.network
|
||||
|
||||
source /dev/stdin <<<"$FUNCTIONS_FILE_PATH"
|
||||
color
|
||||
verb_ip6
|
||||
catch_errors
|
||||
setting_up_container
|
||||
network_check
|
||||
update_os
|
||||
|
||||
msg_info "Installing Dependencies"
|
||||
$STD apt-get install -y \
|
||||
curl \
|
||||
jq \
|
||||
git \
|
||||
openssl \
|
||||
gnupg \
|
||||
lsb-release \
|
||||
postgresql \
|
||||
postgresql-contrib
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
msg_info "Installing ZeroTier"
|
||||
curl -s 'https://raw.githubusercontent.com/zerotier/ZeroTierOne/main/doc/contact%40zerotier.com.gpg' | gpg --import
|
||||
if z=$(curl -s 'https://install.zerotier.com/' | gpg); then
|
||||
echo "$z" | bash
|
||||
fi
|
||||
$STD systemctl enable --now zerotier-one
|
||||
msg_ok "Installed ZeroTier"
|
||||
|
||||
msg_info "Installing ZTNet"
|
||||
curl -s http://install.ztnet.network | bash
|
||||
msg_ok "Installed ZTNet"
|
||||
|
||||
msg_info "Enabling ZTNet Service"
|
||||
$STD systemctl enable --now ztnet
|
||||
msg_ok "Started ZTNet"
|
||||
|
||||
motd_ssh
|
||||
customize
|
||||
cleanup_lxc
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
+14813
File diff suppressed because it is too large
Load Diff
+542
@@ -0,0 +1,542 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useEffect, useState, useMemo } from "react";
|
||||
import { Loader2, Copy, Check, Terminal, Settings2, Server, Cpu, HardDrive, Network, Shield, Play } from "lucide-react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Search } from "@/components/search";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { basePath } from "@/config/site-config";
|
||||
import {
|
||||
generateInstallCommand,
|
||||
validateConfig,
|
||||
getAvailableOS,
|
||||
getDefaultResources,
|
||||
getScriptTypeDisplay,
|
||||
type GeneratorConfig,
|
||||
DEFAULT_CONFIG,
|
||||
} from "@/lib/generate-command";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function GeneratorContent() {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [selectedScript, setSelectedScript] = useState<Script | null>(null);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [search, setSearch] = useQueryState("search");
|
||||
|
||||
// Configuration state
|
||||
const [config, setConfig] = useState<GeneratorConfig>(DEFAULT_CONFIG);
|
||||
|
||||
// Get all scripts from all categories
|
||||
const allScripts = useMemo(() => {
|
||||
if (!categories.length) return [];
|
||||
const scripts = categories.flatMap((category) => category.scripts || []);
|
||||
// Remove duplicates by slug
|
||||
const uniqueScripts = new Map<string, Script>();
|
||||
scripts.forEach((script) => {
|
||||
if (!uniqueScripts.has(script.slug)) {
|
||||
uniqueScripts.set(script.slug, script);
|
||||
}
|
||||
});
|
||||
return Array.from(uniqueScripts.values());
|
||||
}, [categories]);
|
||||
|
||||
// Filter scripts by search
|
||||
const filteredScripts = useMemo(() => {
|
||||
if (!search) return allScripts;
|
||||
const searchLower = search.toLowerCase();
|
||||
return allScripts.filter(
|
||||
(script) =>
|
||||
script.name.toLowerCase().includes(searchLower) ||
|
||||
script.description.toLowerCase().includes(searchLower)
|
||||
);
|
||||
}, [allScripts, search]);
|
||||
|
||||
// Available OS options for selected script
|
||||
const availableOS = useMemo(() => getAvailableOS(selectedScript), [selectedScript]);
|
||||
|
||||
// Default resources for selected script
|
||||
const defaultResources = useMemo(() => getDefaultResources(selectedScript), [selectedScript]);
|
||||
|
||||
// Load categories on mount
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
.then((data) => setCategories(data))
|
||||
.catch((error) => console.error(error));
|
||||
}, []);
|
||||
|
||||
// Update config when script is selected
|
||||
useEffect(() => {
|
||||
if (selectedScript) {
|
||||
const defaults = getDefaultResources(selectedScript);
|
||||
const os = getAvailableOS(selectedScript)[0] || "";
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
script: selectedScript,
|
||||
os,
|
||||
cpuCores: defaults.cpu || prev.cpuCores,
|
||||
ram: defaults.ram || prev.ram,
|
||||
diskSize: defaults.disk || prev.diskSize,
|
||||
}));
|
||||
}
|
||||
}, [selectedScript]);
|
||||
|
||||
// Generate command
|
||||
const command = useMemo(() => generateInstallCommand(config), [config]);
|
||||
|
||||
// Validation
|
||||
const validation = useMemo(() => validateConfig(config), [config]);
|
||||
|
||||
// Copy command to clipboard
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(command);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy:", err);
|
||||
}
|
||||
};
|
||||
|
||||
// Update config helper
|
||||
const updateConfig = <K extends keyof GeneratorConfig>(key: K, value: GeneratorConfig[K]) => {
|
||||
setConfig((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 mt-16">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground mb-2">Unattended Script Generator</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Generate installation commands for automated deployments with custom configurations
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-2">
|
||||
{/* Script Selection */}
|
||||
<Card className="border-rust/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-brass">
|
||||
<Server className="h-5 w-5" />
|
||||
Select Script
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Choose a script to configure for unattended installation
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Search
|
||||
placeholder="Search scripts..."
|
||||
value={search || ""}
|
||||
onChange={(e) => setSearch(e.target.value || null)}
|
||||
/>
|
||||
|
||||
<div className="max-h-[400px] overflow-y-auto space-y-2">
|
||||
{filteredScripts.length === 0 ? (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
No scripts found
|
||||
</div>
|
||||
) : (
|
||||
filteredScripts.slice(0, 50).map((script) => (
|
||||
<button
|
||||
key={script.slug}
|
||||
onClick={() => setSelectedScript(script)}
|
||||
className={cn(
|
||||
"w-full flex items-center gap-3 p-3 rounded-lg border transition-colors text-left",
|
||||
selectedScript?.slug === script.slug
|
||||
? "border-brass bg-brass/10"
|
||||
: "border-rust/30 hover:border-brass/50"
|
||||
)}
|
||||
>
|
||||
<div className="flex h-12 w-12 min-w-12 items-center justify-center rounded-lg bg-accent p-1">
|
||||
<Image
|
||||
src={script.logo || `/${basePath}/logo.png`}
|
||||
unoptimized
|
||||
height={48}
|
||||
width={48}
|
||||
alt=""
|
||||
className="h-10 w-10 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium truncate">{script.name}</span>
|
||||
<Badge variant="outline" className="text-xs border-copper/50">
|
||||
{getScriptTypeDisplay(script.type)}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground line-clamp-1">
|
||||
{script.description}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Configuration */}
|
||||
<Card className="border-rust/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-brass">
|
||||
<Settings2 className="h-5 w-5" />
|
||||
Configuration
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Customize the installation parameters
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="basic" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-3">
|
||||
<TabsTrigger value="basic">Basic</TabsTrigger>
|
||||
<TabsTrigger value="network">Network</TabsTrigger>
|
||||
<TabsTrigger value="resources">Resources</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Basic Tab */}
|
||||
<TabsContent value="basic" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="hostname" className="text-copper">Hostname</Label>
|
||||
<Input
|
||||
id="hostname"
|
||||
placeholder="e.g., my-container"
|
||||
value={config.hostname}
|
||||
onChange={(e) => updateConfig("hostname", e.target.value)}
|
||||
className="border-rust/30 focus:border-brass"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="os" className="text-copper">Operating System</Label>
|
||||
<Select
|
||||
value={config.os}
|
||||
onValueChange={(value) => updateConfig("os", value)}
|
||||
disabled={!selectedScript || availableOS.length === 0}
|
||||
>
|
||||
<SelectTrigger className="border-rust/30 focus:border-brass">
|
||||
<SelectValue placeholder="Select OS" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableOS.map((os) => (
|
||||
<SelectItem key={os} value={os}>
|
||||
{os}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="unprivileged" className="text-copper">Unprivileged Container</Label>
|
||||
<Switch
|
||||
id="unprivileged"
|
||||
checked={config.unprivileged}
|
||||
onCheckedChange={(checked) => updateConfig("unprivileged", checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="startAfter" className="text-copper">Start After Creation</Label>
|
||||
<Switch
|
||||
id="startAfter"
|
||||
checked={config.startAfterCreation}
|
||||
onCheckedChange={(checked) => updateConfig("startAfterCreation", checked)}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Network Tab */}
|
||||
<TabsContent value="network" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-copper">Network Type</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={config.networkType === "dhcp" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => updateConfig("networkType", "dhcp")}
|
||||
className={cn(
|
||||
"flex-1",
|
||||
config.networkType === "dhcp"
|
||||
? "bg-brass text-background hover:bg-brass/90"
|
||||
: "border-rust/30 hover:border-brass"
|
||||
)}
|
||||
>
|
||||
DHCP
|
||||
</Button>
|
||||
<Button
|
||||
variant={config.networkType === "static" ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => updateConfig("networkType", "static")}
|
||||
className={cn(
|
||||
"flex-1",
|
||||
config.networkType === "static"
|
||||
? "bg-brass text-background hover:bg-brass/90"
|
||||
: "border-rust/30 hover:border-brass"
|
||||
)}
|
||||
>
|
||||
Static
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.networkType === "static" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ip" className="text-copper">IP Address</Label>
|
||||
<Input
|
||||
id="ip"
|
||||
placeholder="e.g., 192.168.1.100"
|
||||
value={config.ip}
|
||||
onChange={(e) => updateConfig("ip", e.target.value)}
|
||||
className="border-rust/30 focus:border-brass"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="gateway" className="text-copper">Gateway</Label>
|
||||
<Input
|
||||
id="gateway"
|
||||
placeholder="e.g., 192.168.1.1"
|
||||
value={config.gateway}
|
||||
onChange={(e) => updateConfig("gateway", e.target.value)}
|
||||
className="border-rust/30 focus:border-brass"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="dns" className="text-copper">DNS Servers (comma-separated)</Label>
|
||||
<Input
|
||||
id="dns"
|
||||
placeholder="e.g., 8.8.8.8, 8.8.4.4"
|
||||
value={config.dns.join(", ")}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
"dns",
|
||||
e.target.value.split(",").map((s) => s.trim()).filter(Boolean)
|
||||
)
|
||||
}
|
||||
className="border-rust/30 focus:border-brass"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="ssh" className="text-copper">Enable SSH</Label>
|
||||
<Switch
|
||||
id="ssh"
|
||||
checked={config.sshEnabled}
|
||||
onCheckedChange={(checked) => updateConfig("sshEnabled", checked)}
|
||||
/>
|
||||
</div>
|
||||
{config.sshEnabled && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="sshPort" className="text-copper">SSH Port</Label>
|
||||
<Input
|
||||
id="sshPort"
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
value={config.sshPort}
|
||||
onChange={(e) => updateConfig("sshPort", parseInt(e.target.value) || 22)}
|
||||
className="border-rust/30 focus:border-brass"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Resources Tab */}
|
||||
<TabsContent value="resources" className="space-y-4 mt-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="cpu" className="text-copper flex items-center gap-2">
|
||||
<Cpu className="h-4 w-4" />
|
||||
CPU Cores
|
||||
</Label>
|
||||
<Input
|
||||
id="cpu"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={defaultResources.cpu?.toString() || "e.g., 2"}
|
||||
value={config.cpuCores || ""}
|
||||
onChange={(e) => updateConfig("cpuCores", e.target.value ? parseInt(e.target.value) : null)}
|
||||
className="border-rust/30 focus:border-brass"
|
||||
/>
|
||||
{defaultResources.cpu && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Default: {defaultResources.cpu} cores
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ram" className="text-copper flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
RAM (MB)
|
||||
</Label>
|
||||
<Input
|
||||
id="ram"
|
||||
type="number"
|
||||
min={128}
|
||||
placeholder={defaultResources.ram?.toString() || "e.g., 512"}
|
||||
value={config.ram || ""}
|
||||
onChange={(e) => updateConfig("ram", e.target.value ? parseInt(e.target.value) : null)}
|
||||
className="border-rust/30 focus:border-brass"
|
||||
/>
|
||||
{defaultResources.ram && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Default: {defaultResources.ram} MB
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="disk" className="text-copper flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4" />
|
||||
Disk Size (GB)
|
||||
</Label>
|
||||
<Input
|
||||
id="disk"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder={defaultResources.disk?.toString() || "e.g., 8"}
|
||||
value={config.diskSize || ""}
|
||||
onChange={(e) => updateConfig("diskSize", e.target.value ? parseInt(e.target.value) : null)}
|
||||
className="border-rust/30 focus:border-brass"
|
||||
/>
|
||||
{defaultResources.disk && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Default: {defaultResources.disk} GB
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Generated Command */}
|
||||
<Card className="mt-6 border-rust/30">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-brass">
|
||||
<Terminal className="h-5 w-5" />
|
||||
Generated Command
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Copy and run this command in your Proxmox VE shell
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!validation.valid && (
|
||||
<div className="mb-4 p-3 rounded-lg bg-corruption/10 border border-corruption/30">
|
||||
<ul className="text-sm text-corruption">
|
||||
{validation.errors.map((error, i) => (
|
||||
<li key={i}>• {error}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
<Textarea
|
||||
value={command}
|
||||
readOnly
|
||||
className="font-mono text-sm bg-background border-rust/30 min-h-[120px] pr-12"
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
disabled={!validation.valid}
|
||||
className="absolute top-2 right-2 border-rust/30 hover:border-brass"
|
||||
>
|
||||
{copied ? (
|
||||
<>
|
||||
<Check className="h-4 w-4 mr-1" />
|
||||
Copied
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="h-4 w-4 mr-1" />
|
||||
Copy
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{selectedScript && (
|
||||
<div className="mt-4 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Play className="h-4 w-4" />
|
||||
<span>
|
||||
Run this command in your Proxmox VE shell to deploy {selectedScript.name}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSelectedScript(null);
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
setSearch(null);
|
||||
}}
|
||||
className="border-rust/30 hover:border-brass"
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
asChild
|
||||
disabled={!selectedScript}
|
||||
className="bg-brass text-background hover:bg-brass/90"
|
||||
>
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: selectedScript?.slug },
|
||||
}}
|
||||
>
|
||||
View Script Details
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function GeneratorPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GeneratorContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Filter, X, ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
|
||||
type ScriptType = "ct" | "vm" | "pve" | "addon" | "turnkey";
|
||||
type StatusFilter = "all" | "active" | "deprecated";
|
||||
|
||||
interface AdvancedFilterProps {
|
||||
categories: Category[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const SCRIPT_TYPES: { value: ScriptType; label: string }[] = [
|
||||
{ value: "ct", label: "LXC Container" },
|
||||
{ value: "vm", label: "Virtual Machine" },
|
||||
{ value: "pve", label: "Proxmox VE" },
|
||||
{ value: "addon", label: "Addon" },
|
||||
{ value: "turnkey", label: "TurnKey" },
|
||||
];
|
||||
|
||||
const STATUS_OPTIONS: { value: StatusFilter; label: string }[] = [
|
||||
{ value: "all", label: "All Scripts" },
|
||||
{ value: "active", label: "Active Only" },
|
||||
{ value: "deprecated", label: "Deprecated Only" },
|
||||
];
|
||||
|
||||
export function AdvancedFilter({ categories, className }: AdvancedFilterProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// URL state for filters
|
||||
const [types, setTypes] = useQueryState("types");
|
||||
const [categoryIds, setCategoryIds] = useQueryState("categories");
|
||||
const [status, setStatus] = useQueryState("status");
|
||||
const [minCpu, setMinCpu] = useQueryState("minCpu");
|
||||
const [maxCpu, setMaxCpu] = useQueryState("maxCpu");
|
||||
const [minRam, setMinRam] = useQueryState("minRam");
|
||||
const [maxRam, setMaxRam] = useQueryState("maxRam");
|
||||
|
||||
// Parse current values
|
||||
const selectedTypes = types?.split(",").filter(Boolean) as ScriptType[] || [];
|
||||
const selectedCategoryIds = categoryIds?.split(",").filter(Boolean).map(Number) || [];
|
||||
const currentStatus = (status as StatusFilter) || "all";
|
||||
|
||||
// Count active filters
|
||||
const activeFilterCount =
|
||||
selectedTypes.length +
|
||||
selectedCategoryIds.length +
|
||||
(status && status !== "all" ? 1 : 0) +
|
||||
(minCpu ? 1 : 0) +
|
||||
(maxCpu ? 1 : 0) +
|
||||
(minRam ? 1 : 0) +
|
||||
(maxRam ? 1 : 0);
|
||||
|
||||
const toggleType = useCallback((type: ScriptType) => {
|
||||
const newTypes = selectedTypes.includes(type)
|
||||
? selectedTypes.filter((t) => t !== type)
|
||||
: [...selectedTypes, type];
|
||||
setTypes(newTypes.length > 0 ? newTypes.join(",") : null);
|
||||
}, [selectedTypes, setTypes]);
|
||||
|
||||
const toggleCategory = useCallback((categoryId: number) => {
|
||||
const newCategoryIds = selectedCategoryIds.includes(categoryId)
|
||||
? selectedCategoryIds.filter((id) => id !== categoryId)
|
||||
: [...selectedCategoryIds, categoryId];
|
||||
setCategoryIds(newCategoryIds.length > 0 ? newCategoryIds.join(",") : null);
|
||||
}, [selectedCategoryIds, setCategoryIds]);
|
||||
|
||||
const clearAllFilters = useCallback(() => {
|
||||
setTypes(null);
|
||||
setCategoryIds(null);
|
||||
setStatus(null);
|
||||
setMinCpu(null);
|
||||
setMaxCpu(null);
|
||||
setMinRam(null);
|
||||
setMaxRam(null);
|
||||
}, [setTypes, setCategoryIds, setStatus, setMinCpu, setMaxCpu, setMinRam, setMaxRam]);
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{/* Filter Toggle Button */}
|
||||
<div className="flex items-center justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="border-rust/30 hover:border-brass"
|
||||
>
|
||||
<Filter className="mr-2 h-4 w-4" />
|
||||
Advanced Filters
|
||||
{activeFilterCount > 0 && (
|
||||
<Badge variant="secondary" className="ml-2 bg-copper/20 text-copper">
|
||||
{activeFilterCount}
|
||||
</Badge>
|
||||
)}
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="ml-2 h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="ml-2 h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{activeFilterCount > 0 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={clearAllFilters}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<X className="mr-1 h-4 w-4" />
|
||||
Clear Filters
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expanded Filter Panel */}
|
||||
{isExpanded && (
|
||||
<div className="grid gap-6 rounded-lg border border-rust/30 bg-card/50 p-4 md:grid-cols-2 lg:grid-cols-4">
|
||||
{/* Script Type Filter */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-copper">Script Type</Label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{SCRIPT_TYPES.map((type) => (
|
||||
<Badge
|
||||
key={type.value}
|
||||
variant={selectedTypes.includes(type.value) ? "default" : "outline"}
|
||||
className={cn(
|
||||
"cursor-pointer transition-colors",
|
||||
selectedTypes.includes(type.value)
|
||||
? "bg-brass text-background hover:bg-brass/90"
|
||||
: "border-rust/30 hover:border-brass"
|
||||
)}
|
||||
onClick={() => toggleType(type.value)}
|
||||
>
|
||||
{type.label}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-copper">Categories</Label>
|
||||
<div className="max-h-32 overflow-y-auto">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map((category) => (
|
||||
<Badge
|
||||
key={category.id}
|
||||
variant={selectedCategoryIds.includes(category.id) ? "default" : "outline"}
|
||||
className={cn(
|
||||
"cursor-pointer transition-colors",
|
||||
selectedCategoryIds.includes(category.id)
|
||||
? "bg-brass text-background hover:bg-brass/90"
|
||||
: "border-rust/30 hover:border-brass"
|
||||
)}
|
||||
onClick={() => toggleCategory(category.id)}
|
||||
>
|
||||
{category.name}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status Filter */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-copper">Status</Label>
|
||||
<Select
|
||||
value={currentStatus}
|
||||
onValueChange={(value) => setStatus(value === "all" ? null : value)}
|
||||
>
|
||||
<SelectTrigger className="border-rust/30 focus:border-brass">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{STATUS_OPTIONS.map((option) => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Resource Filters */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-copper">Resources</Label>
|
||||
|
||||
{/* CPU Range */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Min CPU"
|
||||
value={minCpu || ""}
|
||||
onChange={(e) => setMinCpu(e.target.value || null)}
|
||||
className="h-8 w-20 border-rust/30 focus:border-brass"
|
||||
min={1}
|
||||
/>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Max CPU"
|
||||
value={maxCpu || ""}
|
||||
onChange={(e) => setMaxCpu(e.target.value || null)}
|
||||
className="h-8 w-20 border-rust/30 focus:border-brass"
|
||||
min={1}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">cores</span>
|
||||
</div>
|
||||
|
||||
{/* RAM Range */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Min RAM"
|
||||
value={minRam || ""}
|
||||
onChange={(e) => setMinRam(e.target.value || null)}
|
||||
className="h-8 w-20 border-rust/30 focus:border-brass"
|
||||
min={128}
|
||||
/>
|
||||
<span className="text-muted-foreground">-</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="Max RAM"
|
||||
value={maxRam || ""}
|
||||
onChange={(e) => setMaxRam(e.target.value || null)}
|
||||
className="h-8 w-20 border-rust/30 focus:border-brass"
|
||||
min={128}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground">MB</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
"use client";
|
||||
|
||||
import { Search as SearchIcon, X } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface SearchProps {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
debounceMs?: number;
|
||||
// Controlled mode props
|
||||
value?: string;
|
||||
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export function Search({
|
||||
placeholder = "Search scripts...",
|
||||
className,
|
||||
debounceMs = 300,
|
||||
value: controlledValue,
|
||||
onChange: controlledOnChange,
|
||||
}: SearchProps) {
|
||||
const [search, setSearch] = useQueryState("search");
|
||||
const [localValue, setLocalValue] = useState(controlledValue ?? search ?? "");
|
||||
|
||||
// Determine if we're in controlled mode
|
||||
const isControlled = controlledValue !== undefined && controlledOnChange !== undefined;
|
||||
|
||||
// Sync local state with URL state on mount (uncontrolled mode)
|
||||
useEffect(() => {
|
||||
if (!isControlled) {
|
||||
setLocalValue(search ?? "");
|
||||
}
|
||||
}, [search, isControlled]);
|
||||
|
||||
// Sync with controlled value
|
||||
useEffect(() => {
|
||||
if (isControlled) {
|
||||
setLocalValue(controlledValue);
|
||||
}
|
||||
}, [controlledValue, isControlled]);
|
||||
|
||||
// Debounced search update (uncontrolled mode only)
|
||||
useEffect(() => {
|
||||
if (isControlled) return;
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (localValue !== (search ?? "")) {
|
||||
setSearch(localValue || null);
|
||||
}
|
||||
}, debounceMs);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [localValue, debounceMs, search, setSearch, isControlled]);
|
||||
|
||||
const handleClear = useCallback(() => {
|
||||
setLocalValue("");
|
||||
if (!isControlled) {
|
||||
setSearch(null);
|
||||
}
|
||||
if (controlledOnChange) {
|
||||
controlledOnChange({ target: { value: "" } } as React.ChangeEvent<HTMLInputElement>);
|
||||
}
|
||||
}, [setSearch, isControlled, controlledOnChange]);
|
||||
|
||||
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = e.target.value;
|
||||
setLocalValue(newValue);
|
||||
|
||||
if (isControlled && controlledOnChange) {
|
||||
controlledOnChange(e);
|
||||
}
|
||||
}, [isControlled, controlledOnChange]);
|
||||
|
||||
return (
|
||||
<div className={cn("relative", className)}>
|
||||
<SearchIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={placeholder}
|
||||
value={localValue}
|
||||
onChange={handleChange}
|
||||
className="pl-9 pr-9 border-rust/30 focus:border-brass bg-background"
|
||||
/>
|
||||
{localValue && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleClear}
|
||||
className="absolute right-1 top-1/2 h-7 w-7 -translate-y-1/2 p-0 hover:bg-accent"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Clear search</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
import type { Script } from "./types";
|
||||
|
||||
export type ScriptType = "ct" | "vm" | "pve" | "addon" | "turnkey";
|
||||
export type StatusFilter = "all" | "active" | "deprecated";
|
||||
|
||||
export interface FilterState {
|
||||
search: string;
|
||||
types: ScriptType[];
|
||||
categoryIds: number[];
|
||||
status: StatusFilter;
|
||||
minCpu: number | null;
|
||||
maxCpu: number | null;
|
||||
minRam: number | null;
|
||||
maxRam: number | null;
|
||||
}
|
||||
|
||||
export const DEFAULT_FILTER_STATE: FilterState = {
|
||||
search: "",
|
||||
types: [],
|
||||
categoryIds: [],
|
||||
status: "all",
|
||||
minCpu: null,
|
||||
maxCpu: null,
|
||||
minRam: null,
|
||||
maxRam: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter scripts based on search text
|
||||
*/
|
||||
export function filterBySearch(scripts: Script[], search: string): Script[] {
|
||||
if (!search.trim()) return scripts;
|
||||
|
||||
const searchLower = search.toLowerCase().trim();
|
||||
|
||||
return scripts.filter((script) => {
|
||||
// Search in name
|
||||
if (script.name.toLowerCase().includes(searchLower)) return true;
|
||||
|
||||
// Search in description
|
||||
if (script.description.toLowerCase().includes(searchLower)) return true;
|
||||
|
||||
// Search in slug
|
||||
if (script.slug.toLowerCase().includes(searchLower)) return true;
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter scripts by type (ct, vm, pve, addon, turnkey)
|
||||
*/
|
||||
export function filterByType(scripts: Script[], types: ScriptType[]): Script[] {
|
||||
if (!types.length) return scripts;
|
||||
|
||||
return scripts.filter((script) => types.includes(script.type as ScriptType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter scripts by category IDs
|
||||
*/
|
||||
export function filterByCategories(
|
||||
scripts: Script[],
|
||||
categoryIds: number[],
|
||||
allCategories: { id: number; scripts: Script[] }[]
|
||||
): Script[] {
|
||||
if (!categoryIds.length) return scripts;
|
||||
|
||||
// Get all script slugs from the selected categories
|
||||
const categoryScriptSlugs = new Set<string>();
|
||||
|
||||
allCategories.forEach((category) => {
|
||||
if (categoryIds.includes(category.id)) {
|
||||
category.scripts.forEach((script) => {
|
||||
categoryScriptSlugs.add(script.slug);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return scripts.filter((script) => categoryScriptSlugs.has(script.slug));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter scripts by status (active/deprecated)
|
||||
*/
|
||||
export function filterByStatus(
|
||||
scripts: Script[],
|
||||
status: StatusFilter
|
||||
): Script[] {
|
||||
if (status === "all") return scripts;
|
||||
|
||||
return scripts.filter((script) => {
|
||||
if (status === "active") return !script.disable;
|
||||
if (status === "deprecated") return script.disable;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter scripts by CPU cores range
|
||||
*/
|
||||
export function filterByCpu(
|
||||
scripts: Script[],
|
||||
minCpu: number | null,
|
||||
maxCpu: number | null
|
||||
): Script[] {
|
||||
if (minCpu === null && maxCpu === null) return scripts;
|
||||
|
||||
return scripts.filter((script) => {
|
||||
// Get CPU from first install method
|
||||
const cpu = script.install_methods[0]?.resources?.cpu;
|
||||
if (cpu === null || cpu === undefined) return true; // Include if no CPU specified
|
||||
|
||||
if (minCpu !== null && cpu < minCpu) return false;
|
||||
if (maxCpu !== null && cpu > maxCpu) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter scripts by RAM range (in MB)
|
||||
*/
|
||||
export function filterByRam(
|
||||
scripts: Script[],
|
||||
minRam: number | null,
|
||||
maxRam: number | null
|
||||
): Script[] {
|
||||
if (minRam === null && maxRam === null) return scripts;
|
||||
|
||||
return scripts.filter((script) => {
|
||||
// Get RAM from first install method
|
||||
const ram = script.install_methods[0]?.resources?.ram;
|
||||
if (ram === null || ram === undefined) return true; // Include if no RAM specified
|
||||
|
||||
if (minRam !== null && ram < minRam) return false;
|
||||
if (maxRam !== null && ram > maxRam) return false;
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all filters to scripts
|
||||
*/
|
||||
export function filterScripts(
|
||||
scripts: Script[],
|
||||
filters: FilterState,
|
||||
allCategories: { id: number; scripts: Script[] }[]
|
||||
): Script[] {
|
||||
let result = [...scripts];
|
||||
|
||||
// Apply search filter
|
||||
result = filterBySearch(result, filters.search);
|
||||
|
||||
// Apply type filter
|
||||
result = filterByType(result, filters.types);
|
||||
|
||||
// Apply category filter
|
||||
result = filterByCategories(result, filters.categoryIds, allCategories);
|
||||
|
||||
// Apply status filter
|
||||
result = filterByStatus(result, filters.status);
|
||||
|
||||
// Apply CPU filter
|
||||
result = filterByCpu(result, filters.minCpu, filters.maxCpu);
|
||||
|
||||
// Apply RAM filter
|
||||
result = filterByRam(result, filters.minRam, filters.maxRam);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if any filters are active
|
||||
*/
|
||||
export function hasActiveFilters(filters: FilterState): boolean {
|
||||
return (
|
||||
filters.search.trim() !== "" ||
|
||||
filters.types.length > 0 ||
|
||||
filters.categoryIds.length > 0 ||
|
||||
filters.status !== "all" ||
|
||||
filters.minCpu !== null ||
|
||||
filters.maxCpu !== null ||
|
||||
filters.minRam !== null ||
|
||||
filters.maxRam !== null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get count of active filters
|
||||
*/
|
||||
export function getActiveFilterCount(filters: FilterState): number {
|
||||
let count = 0;
|
||||
|
||||
if (filters.search.trim()) count++;
|
||||
count += filters.types.length;
|
||||
count += filters.categoryIds.length;
|
||||
if (filters.status !== "all") count++;
|
||||
if (filters.minCpu !== null) count++;
|
||||
if (filters.maxCpu !== null) count++;
|
||||
if (filters.minRam !== null) count++;
|
||||
if (filters.maxRam !== null) count++;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort scripts by date (newest first)
|
||||
*/
|
||||
export function sortScriptsByDate(scripts: Script[]): Script[] {
|
||||
return [...scripts].sort(
|
||||
(a, b) => new Date(b.date_created).getTime() - new Date(a.date_created).getTime()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort scripts by name (alphabetically)
|
||||
*/
|
||||
export function sortScriptsByName(scripts: Script[]): Script[] {
|
||||
return [...scripts].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique operating systems from scripts
|
||||
*/
|
||||
export function getUniqueOperatingSystems(scripts: Script[]): string[] {
|
||||
const osSet = new Set<string>();
|
||||
|
||||
scripts.forEach((script) => {
|
||||
script.install_methods.forEach((method) => {
|
||||
if (method.resources?.os) {
|
||||
osSet.add(method.resources.os);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(osSet).sort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unique script types from scripts
|
||||
*/
|
||||
export function getUniqueTypes(scripts: Script[]): ScriptType[] {
|
||||
const types = new Set<ScriptType>();
|
||||
scripts.forEach((script) => {
|
||||
types.add(script.type as ScriptType);
|
||||
});
|
||||
return Array.from(types);
|
||||
}
|
||||
+230
@@ -0,0 +1,230 @@
|
||||
import type { Script } from "./types";
|
||||
|
||||
export interface GeneratorConfig {
|
||||
script: Script | null;
|
||||
hostname: string;
|
||||
ip: string;
|
||||
gateway: string;
|
||||
dns: string[];
|
||||
cpuCores: number | null;
|
||||
ram: number | null;
|
||||
diskSize: number | null;
|
||||
os: string;
|
||||
networkType: "dhcp" | "static";
|
||||
sshEnabled: boolean;
|
||||
sshPort: number;
|
||||
unprivileged: boolean;
|
||||
startAfterCreation: boolean;
|
||||
}
|
||||
|
||||
export const DEFAULT_CONFIG: GeneratorConfig = {
|
||||
script: null,
|
||||
hostname: "",
|
||||
ip: "",
|
||||
gateway: "",
|
||||
dns: [],
|
||||
cpuCores: null,
|
||||
ram: null,
|
||||
diskSize: null,
|
||||
os: "",
|
||||
networkType: "dhcp",
|
||||
sshEnabled: true,
|
||||
sshPort: 22,
|
||||
unprivileged: true,
|
||||
startAfterCreation: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate the install command based on configuration
|
||||
*/
|
||||
export function generateInstallCommand(config: GeneratorConfig): string {
|
||||
if (!config.script) {
|
||||
return "# Select a script to generate the command";
|
||||
}
|
||||
|
||||
const scriptPath = config.script.install_methods[0]?.script;
|
||||
if (!scriptPath) {
|
||||
return "# No install script available for this script";
|
||||
}
|
||||
|
||||
const baseUrl = "https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/main";
|
||||
let command = `bash -c "$(wget -qLO - ${baseUrl}/${scriptPath})"`;
|
||||
|
||||
// Build environment variables for unattended installation
|
||||
const envVars: string[] = [];
|
||||
|
||||
// Hostname
|
||||
if (config.hostname) {
|
||||
envVars.push(`HOSTNAME="${config.hostname}"`);
|
||||
}
|
||||
|
||||
// Network configuration (static only)
|
||||
if (config.networkType === "static") {
|
||||
if (config.ip) {
|
||||
envVars.push(`IP="${config.ip}"`);
|
||||
}
|
||||
if (config.gateway) {
|
||||
envVars.push(`GATEWAY="${config.gateway}"`);
|
||||
}
|
||||
if (config.dns.length > 0) {
|
||||
envVars.push(`DNS="${config.dns.join(",")}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Resources
|
||||
if (config.cpuCores) {
|
||||
envVars.push(`CORES="${config.cpuCores}"`);
|
||||
}
|
||||
if (config.ram) {
|
||||
envVars.push(`RAM="${config.ram}"`);
|
||||
}
|
||||
if (config.diskSize) {
|
||||
envVars.push(`DISK_SIZE="${config.diskSize}"`);
|
||||
}
|
||||
|
||||
// SSH configuration
|
||||
if (config.sshEnabled && config.sshPort !== 22) {
|
||||
envVars.push(`SSH_PORT="${config.sshPort}"`);
|
||||
}
|
||||
|
||||
// Unprivileged container
|
||||
if (!config.unprivileged) {
|
||||
envVars.push(`UNPRIVILEGED="no"`);
|
||||
}
|
||||
|
||||
// Start after creation
|
||||
if (!config.startAfterCreation) {
|
||||
envVars.push(`START="no"`);
|
||||
}
|
||||
|
||||
// Prepend environment variables if any
|
||||
if (envVars.length > 0) {
|
||||
command = `${envVars.join(" ")} ${command}`;
|
||||
}
|
||||
|
||||
return command;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a curl command for the script
|
||||
*/
|
||||
export function generateCurlCommand(config: GeneratorConfig): string {
|
||||
if (!config.script) {
|
||||
return "# Select a script to generate the command";
|
||||
}
|
||||
|
||||
const scriptPath = config.script.install_methods[0]?.script;
|
||||
if (!scriptPath) {
|
||||
return "# No install script available for this script";
|
||||
}
|
||||
|
||||
const baseUrl = "https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/main";
|
||||
return `curl -fsSL ${baseUrl}/${scriptPath} | bash`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available operating systems for a script
|
||||
*/
|
||||
export function getAvailableOS(script: Script | null): string[] {
|
||||
if (!script) return [];
|
||||
|
||||
const osSet = new Set<string>();
|
||||
script.install_methods.forEach((method) => {
|
||||
if (method.resources?.os) {
|
||||
osSet.add(method.resources.os);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(osSet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default resources for a script
|
||||
*/
|
||||
export function getDefaultResources(script: Script | null): {
|
||||
cpu: number | null;
|
||||
ram: number | null;
|
||||
disk: number | null;
|
||||
} {
|
||||
if (!script || !script.install_methods[0]?.resources) {
|
||||
return { cpu: null, ram: null, disk: null };
|
||||
}
|
||||
|
||||
const resources = script.install_methods[0].resources;
|
||||
return {
|
||||
cpu: resources.cpu,
|
||||
ram: resources.ram,
|
||||
disk: resources.hdd,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration
|
||||
*/
|
||||
export function validateConfig(config: GeneratorConfig): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
|
||||
if (!config.script) {
|
||||
errors.push("Please select a script");
|
||||
}
|
||||
|
||||
if (config.hostname && !/^[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]$/.test(config.hostname)) {
|
||||
errors.push("Hostname must be alphanumeric with hyphens (no leading/trailing hyphens)");
|
||||
}
|
||||
|
||||
if (config.networkType === "static") {
|
||||
if (!config.ip) {
|
||||
errors.push("IP address is required for static network configuration");
|
||||
} else if (!/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(config.ip)) {
|
||||
errors.push("Invalid IP address format");
|
||||
}
|
||||
|
||||
if (config.gateway && !/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(config.gateway)) {
|
||||
errors.push("Invalid gateway address format");
|
||||
}
|
||||
}
|
||||
|
||||
if (config.cpuCores && config.cpuCores < 1) {
|
||||
errors.push("CPU cores must be at least 1");
|
||||
}
|
||||
|
||||
if (config.ram && config.ram < 128) {
|
||||
errors.push("RAM must be at least 128 MB");
|
||||
}
|
||||
|
||||
if (config.diskSize && config.diskSize < 1) {
|
||||
errors.push("Disk size must be at least 1 GB");
|
||||
}
|
||||
|
||||
if (config.sshPort && (config.sshPort < 1 || config.sshPort > 65535)) {
|
||||
errors.push("SSH port must be between 1 and 65535");
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get script type display name
|
||||
*/
|
||||
export function getScriptTypeDisplay(type: string): string {
|
||||
switch (type) {
|
||||
case "ct":
|
||||
return "LXC Container";
|
||||
case "vm":
|
||||
return "Virtual Machine";
|
||||
case "pve":
|
||||
return "Proxmox VE";
|
||||
case "addon":
|
||||
return "Addon";
|
||||
case "turnkey":
|
||||
return "TurnKey";
|
||||
default:
|
||||
return type;
|
||||
}
|
||||
}
|
||||
+41
@@ -51,6 +51,47 @@ fi
|
||||
# Get LXC IP address (must be called INSIDE container, after network is up)
|
||||
get_lxc_ip
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# detect_os()
|
||||
#
|
||||
# - Detects the operating system type, version, and family
|
||||
# - Sets global variables: OS_TYPE, OS_VERSION, OS_FAMILY, OS_CODENAME
|
||||
# - Used by addon scripts to determine OS-specific configuration
|
||||
# - Returns: 0 on success, 1 on failure
|
||||
# ------------------------------------------------------------------------------
|
||||
detect_os() {
|
||||
# Check if /etc/os-release exists
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
msg_error "Cannot detect OS: /etc/os-release not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Source os-release to get variables
|
||||
. /etc/os-release
|
||||
|
||||
# Set OS_TYPE (lowercase ID)
|
||||
OS_TYPE="${ID,,}"
|
||||
|
||||
# Set OS_VERSION (VERSION_ID without quotes)
|
||||
OS_VERSION="${VERSION_ID}"
|
||||
|
||||
# Set OS_CODENAME (VERSION_CODENAME)
|
||||
OS_CODENAME="${VERSION_CODENAME:-}"
|
||||
|
||||
# Set OS_FAMILY based on ID_LIKE or ID
|
||||
if [[ -n "${ID_LIKE:-}" ]]; then
|
||||
OS_FAMILY="${ID_LIKE,,}"
|
||||
else
|
||||
OS_FAMILY="${OS_TYPE}"
|
||||
fi
|
||||
|
||||
# Export variables for use by other scripts
|
||||
export OS_TYPE OS_VERSION OS_FAMILY OS_CODENAME
|
||||
|
||||
msg_ok "Detected OS: ${OS_TYPE} ${OS_VERSION} (${OS_CODENAME:-unknown codename})"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# post_progress_to_api()
|
||||
#
|
||||
|
||||
+161
@@ -1,3 +1,164 @@
|
||||
## 2026-03-14
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Patchmon: remove v prefix from pinned version [@MickLesk](https://github.com/MickLesk) ([#12891](https://github.com/community-scripts/ProxmoxVE/pull/12891))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- tools.func: don't abort on AMD repo apt update failure [@MickLesk](https://github.com/MickLesk) ([#12890](https://github.com/community-scripts/ProxmoxVE/pull/12890))
|
||||
|
||||
## 2026-03-13
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Hotfix: Removed clean install usage from original script. [@nickheyer](https://github.com/nickheyer) ([#12870](https://github.com/community-scripts/ProxmoxVE/pull/12870))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Discopanel: V2 Support + Script rewrite [@nickheyer](https://github.com/nickheyer) ([#12763](https://github.com/community-scripts/ProxmoxVE/pull/12763))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- update-apps: fix restore path, add PBS support and improve restore messages [@omertahaoztop](https://github.com/omertahaoztop) ([#12528](https://github.com/community-scripts/ProxmoxVE/pull/12528))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(pve-privilege-converter): handle already stopped container in manage_states [@liuqitoday](https://github.com/liuqitoday) ([#12765](https://github.com/community-scripts/ProxmoxVE/pull/12765))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update: Docs/website metadata workflow [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12858](https://github.com/community-scripts/ProxmoxVE/pull/12858))
|
||||
|
||||
## 2026-03-12
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- manyfold: fix incorrect port in upstream requests by forwarding original host [@anlopo](https://github.com/anlopo) ([#12812](https://github.com/community-scripts/ProxmoxVE/pull/12812))
|
||||
- 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))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Pin Patchmon to 1.4.2 [@vhsdream](https://github.com/vhsdream) ([#12789](https://github.com/community-scripts/ProxmoxVE/pull/12789))
|
||||
|
||||
### 💾 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))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Fix PBS microcode naming [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12834](https://github.com/community-scripts/ProxmoxVE/pull/12834))
|
||||
|
||||
### 📂 Github
|
||||
|
||||
- Cleanup: remove old workflow files [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12818](https://github.com/community-scripts/ProxmoxVE/pull/12818))
|
||||
- Cleanup: remove frontend, move JSONs to json/ top-level [@MickLesk](https://github.com/MickLesk) ([#12813](https://github.com/community-scripts/ProxmoxVE/pull/12813))
|
||||
|
||||
### ❔ Uncategorized
|
||||
|
||||
- Remove json files [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12830](https://github.com/community-scripts/ProxmoxVE/pull/12830))
|
||||
|
||||
## 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
|
||||
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
# 🤖 AI Contribution Guidelines for ProxmoxVE
|
||||
# 🤖 AI Contribution Guidelines for ProxmoxVE
|
||||
|
||||
> **This documentation is intended for all AI assistants (GitHub Copilot, Claude, ChatGPT, etc.) contributing to this project.**
|
||||
|
||||
@@ -653,15 +653,15 @@ Look at these recent well-implemented applications as reference:
|
||||
- Use of `check_for_gh_release` and `fetch_and_deploy_gh_release`
|
||||
- Correct backup/restore patterns in `update_script`
|
||||
- Footer always ends with `motd_ssh`, `customize`, `cleanup_lxc`
|
||||
- JSON metadata files created for each app
|
||||
- Website metadata requested via the website (Report issue on script page) if needed
|
||||
|
||||
---
|
||||
|
||||
## � JSON Metadata Files
|
||||
## Website Metadata (Reference)
|
||||
|
||||
Every application requires a JSON metadata file in `frontend/public/json/<appname>.json`.
|
||||
Website metadata (name, slug, description, logo, categories, etc.) is **not** added as files in the repo. Contributors request or update it via the **website**: go to the script's page and use the **Report issue** button; the flow will guide you. The structure below is a **reference** for what metadata exists (e.g. for the form or when describing what you need).
|
||||
|
||||
### JSON Structure
|
||||
### JSON Structure (Reference)
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -804,7 +804,7 @@ Or no credentials:
|
||||
- [ ] `motd_ssh`, `customize`, `cleanup_lxc` at the end of install scripts
|
||||
- [ ] No custom download/version-check logic
|
||||
- [ ] All links point to `community-scripts/ProxmoxVE` (not `ProxmoxVED`!)
|
||||
- [ ] JSON metadata file created in `frontend/public/json/<appname>.json`
|
||||
- [ ] Website metadata requested via the website (Report issue) if needed
|
||||
- [ ] Category IDs are valid (0-25)
|
||||
- [ ] Default OS version is Debian 13 or newer (unless special requirement)
|
||||
- [ ] Default resources are reasonable for the application
|
||||
@@ -832,15 +832,15 @@ Or no credentials:
|
||||
|
||||
## 🍒 Important: Cherry-Picking Your Files for PR Submission
|
||||
|
||||
⚠️ **CRITICAL**: When you submit your PR, you must use git cherry-pick to send ONLY your 3-4 files!
|
||||
⚠️ **CRITICAL**: When you submit your PR, you must use git cherry-pick to send ONLY your 2 files!
|
||||
|
||||
Why? Because `setup-fork.sh` modifies 600+ files to update links. If you commit all changes, your PR will be impossible to merge.
|
||||
|
||||
**See**: [README.md - Cherry-Pick Section](README.md#-cherry-pick-submitting-only-your-changes) for complete instructions on:
|
||||
|
||||
- Creating a clean submission branch
|
||||
- Cherry-picking only your files (ct/myapp.sh, install/myapp-install.sh, frontend/public/json/myapp.json)
|
||||
- Verifying your PR has only 3 file changes (not 600+)
|
||||
- Cherry-picking only your files (ct/myapp.sh, install/myapp-install.sh)
|
||||
- Verifying your PR has only 2 file changes (not 600+)
|
||||
|
||||
**Quick reference**:
|
||||
|
||||
@@ -849,7 +849,7 @@ Why? Because `setup-fork.sh` modifies 600+ files to update links. If you commit
|
||||
git fetch upstream
|
||||
git checkout -b submit/myapp upstream/main
|
||||
|
||||
# Cherry-pick your commit(s) or manually add your 3-4 files
|
||||
# Cherry-pick your commit(s) or manually add your 2 files
|
||||
# Then push to your fork and create PR
|
||||
```
|
||||
|
||||
@@ -865,4 +865,4 @@ git checkout -b submit/myapp upstream/main
|
||||
- [../EXIT_CODES.md](../EXIT_CODES.md) - Exit code reference
|
||||
- [templates_ct/](templates_ct/) - CT script templates
|
||||
- [templates_install/](templates_install/) - Install script templates
|
||||
- [templates_json/](templates_json/) - JSON metadata templates
|
||||
- [templates_json/](templates_json/) - Metadata structure reference; submit via website
|
||||
|
||||
+2
-2
@@ -24,9 +24,9 @@ This guide explains the current execution flow and what to verify during reviews
|
||||
- Uses `tools.func` helpers (setup\_\*).
|
||||
- Ends with `motd_ssh`, `customize`, `cleanup_lxc`.
|
||||
|
||||
### JSON Metadata
|
||||
### Website Metadata
|
||||
|
||||
- File in `frontend/public/json/<appname>.json` matches template schema.
|
||||
- Website metadata for new/updated scripts is requested via the website (Report issue on script page) where applicable.
|
||||
|
||||
### Testing
|
||||
|
||||
|
||||
+3
-3
@@ -54,15 +54,13 @@ git checkout -b add/my-awesome-app
|
||||
# 2. Create application scripts from templates
|
||||
cp docs/contribution/templates_ct/AppName.sh ct/myapp.sh
|
||||
cp docs/contribution/templates_install/AppName-install.sh install/myapp-install.sh
|
||||
cp docs/contribution/templates_json/AppName.json frontend/public/json/myapp.json
|
||||
|
||||
# 3. Edit your scripts
|
||||
nano ct/myapp.sh
|
||||
nano install/myapp-install.sh
|
||||
nano frontend/public/json/myapp.json
|
||||
|
||||
# 4. Commit and push to your fork
|
||||
git add ct/myapp.sh install/myapp-install.sh frontend/public/json/myapp.json
|
||||
git add ct/myapp.sh install/myapp-install.sh
|
||||
git commit -m "feat: add MyApp container and install scripts"
|
||||
git push origin add/my-awesome-app
|
||||
|
||||
@@ -74,6 +72,8 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/YOUR_USERNAME/ProxmoxVE/
|
||||
|
||||
# 7. Open Pull Request on GitHub
|
||||
# Create PR from: your-fork/add/my-awesome-app → community-scripts/ProxmoxVE/main
|
||||
|
||||
# To add or change website metadata (description, logo, etc.), use the Report issue button on the script's page on the website.
|
||||
```
|
||||
|
||||
**💡 Tip**: See `../FORK_SETUP.md` for detailed fork setup and troubleshooting
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ fetch_and_deploy_gh_release "myapp" "owner/repo"
|
||||
2. **Only add app-specific dependencies** - Don't add ca-certificates, curl, gnupg (handled by build.func)
|
||||
3. **Test via curl from your fork** - Push first, then: `bash -c "$(curl -fsSL https://raw.githubusercontent.com/YOUR_USERNAME/ProxmoxVE/main/ct/MyApp.sh)"`
|
||||
4. **Wait for GitHub to update** - Takes 10-30 seconds after git push
|
||||
5. **Cherry-pick only YOUR files** - Submit only ct/MyApp.sh, install/MyApp-install.sh, frontend/public/json/myapp.json (3 files)
|
||||
5. **Cherry-pick only YOUR files** - Submit only ct/MyApp.sh, install/MyApp-install.sh (2 files). Website metadata: use Report issue on the script's page on the website.
|
||||
6. **Verify before PR** - Run `git diff upstream/main --name-only` to confirm only your files changed
|
||||
|
||||
---
|
||||
|
||||
+13
-18
@@ -1,21 +1,20 @@
|
||||
# JSON Metadata Files - Quick Reference
|
||||
# Website Metadata - Quick Reference
|
||||
|
||||
The metadata file (`frontend/public/json/myapp.json`) tells the web interface how to display your application.
|
||||
Metadata (name, slug, description, logo, categories, etc.) controls how your application appears on the website. You do **not** add JSON files to the repo — you request changes via the website.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## How to Request or Update Metadata
|
||||
|
||||
**Use the JSON Generator Tool:**
|
||||
[https://community-scripts.github.io/ProxmoxVE/json-editor](https://community-scripts.github.io/ProxmoxVE/json-editor)
|
||||
|
||||
1. Enter application details
|
||||
2. Generator creates `frontend/public/json/myapp.json`
|
||||
3. Copy the output to your contribution
|
||||
1. **Go to the script on the website** — Open the [ProxmoxVE website](https://community-scripts.github.io/ProxmoxVE/), find your script (or the script you want to update).
|
||||
2. **Press the "Report issue" button** on that script’s page.
|
||||
3. **Follow the guide** — The flow will walk you through submitting or updating metadata.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
## Metadata Structure (Reference)
|
||||
|
||||
The following describes the structure of script metadata used by the website. Use it as reference when filling out the form or describing what you need.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -148,18 +147,14 @@ Each installation method specifies resource requirements:
|
||||
|
||||
---
|
||||
|
||||
## Reference Examples
|
||||
## See Examples on the Website
|
||||
|
||||
See actual examples in the repo:
|
||||
|
||||
- [frontend/public/json/trip.json](https://github.com/community-scripts/ProxmoxVE/blob/main/frontend/public/json/trip.json)
|
||||
- [frontend/public/json/thingsboard.json](https://github.com/community-scripts/ProxmoxVE/blob/main/frontend/public/json/thingsboard.json)
|
||||
- [frontend/public/json/unifi.json](https://github.com/community-scripts/ProxmoxVE/blob/main/frontend/public/json/unifi.json)
|
||||
View script pages on the [ProxmoxVE website](https://community-scripts.github.io/ProxmoxVE/) to see how metadata is displayed for existing scripts.
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **[JSON Generator](https://community-scripts.github.io/ProxmoxVE/json-editor)** - Interactive tool
|
||||
- **Request metadata** — Use the Report issue button on the script’s page on the website (see [How to Request or Update Metadata](#how-to-request-or-update-metadata) above).
|
||||
- **[JSON Generator](https://community-scripts.github.io/ProxmoxVE/json-editor)** - Reference only; structure validation
|
||||
- **[README.md](../README.md)** - Full contribution workflow
|
||||
- **[Quick Start](../README.md)** - Step-by-step guide
|
||||
|
||||
+14
-7
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-03-14T06:30:27Z",
|
||||
"generated": "2026-03-15T12:22:39Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "agregarr",
|
||||
@@ -18,23 +18,23 @@
|
||||
{
|
||||
"slug": "llamacpp",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"version": "b8329",
|
||||
"version": "b8354",
|
||||
"pinned": false,
|
||||
"date": "2026-03-14T04:58:36Z"
|
||||
"date": "2026-03-15T10:06:38Z"
|
||||
},
|
||||
{
|
||||
"slug": "localai",
|
||||
"repo": "mudler/LocalAI",
|
||||
"version": "v3.12.1",
|
||||
"version": "v4.0.0",
|
||||
"pinned": false,
|
||||
"date": "2026-02-21T13:49:24Z"
|
||||
"date": "2026-03-14T18:18:41Z"
|
||||
},
|
||||
{
|
||||
"slug": "localrecall",
|
||||
"repo": "mudler/LocalRecall",
|
||||
"version": "v0.5.5",
|
||||
"version": "v0.5.8",
|
||||
"pinned": false,
|
||||
"date": "2026-02-16T22:38:06Z"
|
||||
"date": "2026-03-14T22:09:26Z"
|
||||
},
|
||||
{
|
||||
"slug": "maintainerr",
|
||||
@@ -50,6 +50,13 @@
|
||||
"pinned": false,
|
||||
"date": "2026-03-11T20:23:52Z"
|
||||
},
|
||||
{
|
||||
"slug": "ragflow",
|
||||
"repo": "infiniflow/ragflow",
|
||||
"version": "v0.24.0",
|
||||
"pinned": true,
|
||||
"date": "2026-02-10T09:27:14Z"
|
||||
},
|
||||
{
|
||||
"slug": "wakapi",
|
||||
"repo": "muety/wakapi",
|
||||
|
||||
+123
-47
@@ -1,22 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { FolderOpen } from "lucide-react";
|
||||
import { FolderOpen, Search as SearchIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { Suspense, useEffect, useState, useMemo } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search } from "@/components/search";
|
||||
|
||||
function CategoriesContent() {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedCategory, setSelectedCategory] = useQueryState("category");
|
||||
const [search, setSearch] = useQueryState("search");
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
@@ -38,6 +39,49 @@ function CategoriesContent() {
|
||||
return { totalScripts, devScripts };
|
||||
};
|
||||
|
||||
// Filter categories by search
|
||||
const filteredCategories = useMemo(() => {
|
||||
if (!search) return categories;
|
||||
|
||||
const searchLower = search.toLowerCase();
|
||||
return categories.filter((category) => {
|
||||
// Search in category name
|
||||
if (category.name.toLowerCase().includes(searchLower)) return true;
|
||||
|
||||
// Search in category description
|
||||
if (category.description?.toLowerCase().includes(searchLower)) return true;
|
||||
|
||||
// Search in script names within category
|
||||
const hasMatchingScript = category.scripts?.some(
|
||||
(script) =>
|
||||
script.name.toLowerCase().includes(searchLower) ||
|
||||
script.description.toLowerCase().includes(searchLower)
|
||||
);
|
||||
|
||||
return hasMatchingScript;
|
||||
});
|
||||
}, [categories, search]);
|
||||
|
||||
// Calculate total scripts
|
||||
const totalScripts = categories.reduce(
|
||||
(acc, cat) => acc + (cat.scripts?.length || 0),
|
||||
0
|
||||
);
|
||||
|
||||
// Calculate matching scripts in filtered categories
|
||||
const matchingScripts = useMemo(() => {
|
||||
if (!search) return totalScripts;
|
||||
|
||||
return filteredCategories.reduce((acc, cat) => {
|
||||
const matchingInCategory = cat.scripts?.filter(
|
||||
(script) =>
|
||||
script.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
script.description.toLowerCase().includes(search.toLowerCase())
|
||||
).length || 0;
|
||||
return acc + matchingInCategory;
|
||||
}, 0);
|
||||
}, [filteredCategories, search, totalScripts]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-[50vh] w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
@@ -46,11 +90,6 @@ function CategoriesContent() {
|
||||
);
|
||||
}
|
||||
|
||||
const totalScripts = categories.reduce(
|
||||
(acc, cat) => acc + (cat.scripts?.length || 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mt-20 px-4 xl:px-0">
|
||||
@@ -68,44 +107,81 @@ function CategoriesContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Categories Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{categories.map((category) => {
|
||||
const { totalScripts, devScripts } = getCategoryStats(category);
|
||||
return (
|
||||
<Link
|
||||
key={category.id}
|
||||
href={`/scripts?category=${category.id}`}
|
||||
className="group"
|
||||
>
|
||||
<Card className="h-full transition-all hover:border-primary/50 hover:shadow-md cursor-pointer">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">{category.name}</CardTitle>
|
||||
<FolderOpen className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">
|
||||
{totalScripts}
|
||||
{" "}
|
||||
{totalScripts === 1 ? "script" : "scripts"}
|
||||
</Badge>
|
||||
{devScripts > 0 && (
|
||||
<Badge variant="outline" className="text-orange-500 border-orange-500/50">
|
||||
{devScripts}
|
||||
{" "}
|
||||
in dev
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{/* Search */}
|
||||
<div className="mb-6">
|
||||
<Search
|
||||
placeholder="Search categories or scripts..."
|
||||
className="w-full max-w-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Search Results Info */}
|
||||
{search && (
|
||||
<div className="mb-4 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<SearchIcon className="h-4 w-4" />
|
||||
<span>
|
||||
Found {filteredCategories.length} categories
|
||||
{matchingScripts > 0 && ` with ${matchingScripts} matching scripts`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Categories Grid */}
|
||||
{filteredCategories.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<SearchIcon className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-foreground/80">No categories found</h3>
|
||||
<p className="text-muted-foreground">Try adjusting your search</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredCategories.map((category) => {
|
||||
const { totalScripts, devScripts } = getCategoryStats(category);
|
||||
return (
|
||||
<Link
|
||||
key={category.id}
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: search ? { category: category.id, search } : { category: category.id },
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<Card className="h-full transition-all hover:border-brass/50 hover:shadow-md cursor-pointer border-rust/30">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg text-foreground/90 group-hover:text-brass transition-colors">
|
||||
{category.name}
|
||||
</CardTitle>
|
||||
<FolderOpen className="h-5 w-5 text-muted-foreground group-hover:text-brass transition-colors" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="secondary" className="bg-accent">
|
||||
{totalScripts}
|
||||
{" "}
|
||||
{totalScripts === 1 ? "script" : "scripts"}
|
||||
</Badge>
|
||||
{devScripts > 0 && (
|
||||
<Badge variant="outline" className="text-orange-500 border-orange-500/50">
|
||||
{devScripts}
|
||||
{" "}
|
||||
in dev
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{category.description && (
|
||||
<p className="mt-2 text-sm text-muted-foreground line-clamp-2">
|
||||
{category.description}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,11 +191,11 @@ function CategoriesContent() {
|
||||
export default function CategoriesPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={(
|
||||
fallback={
|
||||
<div className="flex h-[50vh] w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<CategoriesContent />
|
||||
</Suspense>
|
||||
|
||||
+218
-27
@@ -1,12 +1,21 @@
|
||||
"use client";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { Loader2, X } from "lucide-react";
|
||||
import { Suspense, useEffect, useState, useMemo } from "react";
|
||||
import { Loader2, X, Search as SearchIcon } from "lucide-react";
|
||||
import { useQueryState } from "nuqs";
|
||||
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
|
||||
import { ScriptItem } from "@/app/scripts/_components/script-item";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Search } from "@/components/search";
|
||||
import { AdvancedFilter } from "@/components/advanced-filter";
|
||||
import {
|
||||
filterScripts,
|
||||
hasActiveFilters,
|
||||
sortScriptsByDate,
|
||||
type FilterState,
|
||||
type ScriptType,
|
||||
} from "@/lib/filter-utils";
|
||||
|
||||
import { LatestScripts, MostViewedScripts } from "./_components/script-info-blocks";
|
||||
import Sidebar from "./_components/sidebar";
|
||||
@@ -20,6 +29,53 @@ function ScriptContent() {
|
||||
const [item, setItem] = useState<Script>();
|
||||
const [latestPage, setLatestPage] = useState(1);
|
||||
|
||||
// Filter state from URL
|
||||
const [search, setSearch] = useQueryState("search");
|
||||
const [types, setTypes] = useQueryState("types");
|
||||
const [categoryIds, setCategoryIds] = useQueryState("categories");
|
||||
const [status, setStatus] = useQueryState("status");
|
||||
const [minCpu, setMinCpu] = useQueryState("minCpu");
|
||||
const [maxCpu, setMaxCpu] = useQueryState("maxCpu");
|
||||
const [minRam, setMinRam] = useQueryState("minRam");
|
||||
const [maxRam, setMaxRam] = useQueryState("maxRam");
|
||||
|
||||
// Parse filter state
|
||||
const filterState: FilterState = useMemo(() => ({
|
||||
search: search || "",
|
||||
types: (types?.split(",").filter(Boolean) as ScriptType[]) || [],
|
||||
categoryIds: (categoryIds?.split(",").filter(Boolean).map(Number)) || [],
|
||||
status: (status as FilterState["status"]) || "all",
|
||||
minCpu: minCpu ? parseInt(minCpu) : null,
|
||||
maxCpu: maxCpu ? parseInt(maxCpu) : null,
|
||||
minRam: minRam ? parseInt(minRam) : null,
|
||||
maxRam: maxRam ? parseInt(maxRam) : null,
|
||||
}), [search, types, categoryIds, status, minCpu, maxCpu, minRam, maxRam]);
|
||||
|
||||
// Get all scripts from all categories
|
||||
const allScripts = useMemo(() => {
|
||||
if (!links.length) return [];
|
||||
const scripts = links.flatMap((category) => category.scripts || []);
|
||||
// Remove duplicates by slug
|
||||
const uniqueScripts = new Map<string, Script>();
|
||||
scripts.forEach((script) => {
|
||||
if (!uniqueScripts.has(script.slug)) {
|
||||
uniqueScripts.set(script.slug, script);
|
||||
}
|
||||
});
|
||||
return Array.from(uniqueScripts.values());
|
||||
}, [links]);
|
||||
|
||||
// Filter scripts based on current filter state
|
||||
const filteredScripts = useMemo(() => {
|
||||
if (!hasActiveFilters(filterState)) {
|
||||
return allScripts;
|
||||
}
|
||||
return filterScripts(allScripts, filterState, links);
|
||||
}, [allScripts, filterState, links]);
|
||||
|
||||
// Check if any filters are active
|
||||
const isFiltering = hasActiveFilters(filterState);
|
||||
|
||||
const closeScript = () => {
|
||||
window.history.pushState({}, document.title, window.location.pathname);
|
||||
setSelectedScript(null);
|
||||
@@ -28,9 +84,9 @@ function ScriptContent() {
|
||||
useEffect(() => {
|
||||
if (selectedScript && links.length > 0) {
|
||||
const script = links
|
||||
.map(category => category.scripts)
|
||||
.map((category) => category.scripts)
|
||||
.flat()
|
||||
.find(script => script.slug === selectedScript);
|
||||
.find((script) => script.slug === selectedScript);
|
||||
setItem(script);
|
||||
if (script) {
|
||||
document.title = `${script.name} | Heretek AI`;
|
||||
@@ -45,7 +101,7 @@ function ScriptContent() {
|
||||
.then((categories) => {
|
||||
setLinks(categories);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
.catch((error) => console.error(error));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -61,43 +117,178 @@ function ScriptContent() {
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 w-full sm:max-w-[calc(100%-350px-16px)]">
|
||||
{selectedScript && item
|
||||
? (
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground/90">Selected Script</h2>
|
||||
<button
|
||||
onClick={closeScript}
|
||||
className="rounded-full p-2 text-muted-foreground hover:bg-card/50 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
{selectedScript && item ? (
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground/90">
|
||||
Selected Script
|
||||
</h2>
|
||||
<button
|
||||
onClick={closeScript}
|
||||
className="rounded-full p-2 text-muted-foreground hover:bg-card/50 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<ScriptItem item={item} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
{/* Search and Filter Section */}
|
||||
<div className="space-y-4">
|
||||
<Search
|
||||
placeholder="Search scripts by name or description..."
|
||||
className="w-full"
|
||||
/>
|
||||
<AdvancedFilter categories={links} />
|
||||
</div>
|
||||
|
||||
{/* Results Count */}
|
||||
{isFiltering && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<SearchIcon className="h-4 w-4" />
|
||||
<span>
|
||||
{filteredScripts.length} script{filteredScripts.length !== 1 ? "s" : ""} found
|
||||
</span>
|
||||
</div>
|
||||
<ScriptItem item={item} />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<LatestScripts items={links} page={latestPage} onPageChange={setLatestPage} />
|
||||
<MostViewedScripts items={links} />
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Script Lists */}
|
||||
{isFiltering ? (
|
||||
<FilteredScriptsList scripts={filteredScripts} onSelect={setSelectedScript} />
|
||||
) : (
|
||||
<>
|
||||
<LatestScripts items={links} page={latestPage} onPageChange={setLatestPage} />
|
||||
<MostViewedScripts items={links} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Component to display filtered scripts
|
||||
function FilteredScriptsList({
|
||||
scripts,
|
||||
onSelect,
|
||||
}: {
|
||||
scripts: Script[];
|
||||
onSelect: (slug: string) => void;
|
||||
}) {
|
||||
const sortedScripts = useMemo(() => sortScriptsByDate(scripts), [scripts]);
|
||||
|
||||
if (sortedScripts.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<SearchIcon className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-foreground/80">No scripts found</h3>
|
||||
<p className="text-muted-foreground">Try adjusting your search or filters</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-1">
|
||||
{sortedScripts.map((script) => (
|
||||
<ScriptCard key={script.slug} script={script} onSelect={onSelect} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple script card for filtered results
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { basePath } from "@/config/site-config";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { CalendarPlus } from "lucide-react";
|
||||
|
||||
function ScriptCard({
|
||||
script,
|
||||
onSelect,
|
||||
}: {
|
||||
script: Script;
|
||||
onSelect: (slug: string) => void;
|
||||
}) {
|
||||
const getDisplayValueFromType = (type: string) => {
|
||||
switch (type) {
|
||||
case "ct":
|
||||
return "LXC";
|
||||
case "vm":
|
||||
return "VM";
|
||||
case "pve":
|
||||
return "PVE";
|
||||
case "addon":
|
||||
return "ADDON";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="min-w-[250px] flex-1 flex-grow bg-accent/30 hover:border-brass/50 transition-colors">
|
||||
<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>
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.slug },
|
||||
}}
|
||||
>
|
||||
View Script
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={(
|
||||
fallback={
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<ScriptContent />
|
||||
</Suspense>
|
||||
|
||||
+26
-43
@@ -6,7 +6,6 @@ import Link from "next/link";
|
||||
|
||||
import { navbarLinks } from "@/config/site-config";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||
import { GitHubStarsButton } from "./animate-ui/components/buttons/github-stars";
|
||||
import { Button } from "./animate-ui/components/buttons/button";
|
||||
import MobileSidebar from "./navigation/mobile-sidebar";
|
||||
@@ -34,8 +33,8 @@ function Navbar() {
|
||||
<>
|
||||
<div
|
||||
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 transition-all duration-300 ${isScrolled
|
||||
? "glass rust-border border-b bg-background/50"
|
||||
: ""
|
||||
? "glass rust-border border-b bg-background/50"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-20 w-full max-w-[1440px] items-center justify-between sm:flex-row">
|
||||
@@ -58,52 +57,36 @@ function Navbar() {
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center gap-2">
|
||||
{navbarLinks.filter(link => !link.external).map(({ href, event, icon, text }) => (
|
||||
<TooltipProvider key={event}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
asChild
|
||||
className="text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300"
|
||||
>
|
||||
<Link href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="ml-2 hidden lg:inline">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs bg-card border-rust-500/30">
|
||||
{text}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Button
|
||||
key={event}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
asChild
|
||||
className="text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300"
|
||||
>
|
||||
<Link href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="ml-2 hidden lg:inline">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex sm:gap-2">
|
||||
<CommandMenu />
|
||||
<GitHubStarsButton username="Heretek-AI" repo="ProxmoxVE" className="hidden md:flex" />
|
||||
{navbarLinks.filter(link => link.external).map(({ href, event, icon, text, mobileHidden }) => (
|
||||
<TooltipProvider key={event}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger className={mobileHidden ? "hidden lg:block" : ""}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
asChild
|
||||
className="text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300"
|
||||
>
|
||||
<Link target="_blank" href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="sr-only">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs bg-card border-rust-500/30">
|
||||
{text}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Button
|
||||
key={event}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
asChild
|
||||
className={`text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300 ${mobileHidden ? "hidden lg:flex" : ""}`}
|
||||
>
|
||||
<Link target="_blank" href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="sr-only">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -1,4 +1,4 @@
|
||||
import { MessagesSquare, Scroll, FolderOpen, FileCode } from "lucide-react";
|
||||
import { MessagesSquare, Scroll, FolderOpen, FileCode, Terminal } from "lucide-react";
|
||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||
import React from "react";
|
||||
|
||||
@@ -20,6 +20,12 @@ export const navbarLinks = [
|
||||
icon: <FolderOpen className="h-4 w-4" />,
|
||||
text: "Categories",
|
||||
},
|
||||
{
|
||||
href: "/generator",
|
||||
event: "Generator",
|
||||
icon: <Terminal className="h-4 w-4" />,
|
||||
text: "Generator",
|
||||
},
|
||||
{
|
||||
href: "/community",
|
||||
event: "Community",
|
||||
|
||||
+41
@@ -51,6 +51,47 @@ fi
|
||||
# Get LXC IP address (must be called INSIDE container, after network is up)
|
||||
get_lxc_ip
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# detect_os()
|
||||
#
|
||||
# - Detects the operating system type, version, and family
|
||||
# - Sets global variables: OS_TYPE, OS_VERSION, OS_FAMILY, OS_CODENAME
|
||||
# - Used by addon scripts to determine OS-specific configuration
|
||||
# - Returns: 0 on success, 1 on failure
|
||||
# ------------------------------------------------------------------------------
|
||||
detect_os() {
|
||||
# Check if /etc/os-release exists
|
||||
if [[ ! -f /etc/os-release ]]; then
|
||||
msg_error "Cannot detect OS: /etc/os-release not found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Source os-release to get variables
|
||||
. /etc/os-release
|
||||
|
||||
# Set OS_TYPE (lowercase ID)
|
||||
OS_TYPE="${ID,,}"
|
||||
|
||||
# Set OS_VERSION (VERSION_ID without quotes)
|
||||
OS_VERSION="${VERSION_ID}"
|
||||
|
||||
# Set OS_CODENAME (VERSION_CODENAME)
|
||||
OS_CODENAME="${VERSION_CODENAME:-}"
|
||||
|
||||
# Set OS_FAMILY based on ID_LIKE or ID
|
||||
if [[ -n "${ID_LIKE:-}" ]]; then
|
||||
OS_FAMILY="${ID_LIKE,,}"
|
||||
else
|
||||
OS_FAMILY="${OS_TYPE}"
|
||||
fi
|
||||
|
||||
# Export variables for use by other scripts
|
||||
export OS_TYPE OS_VERSION OS_FAMILY OS_CODENAME
|
||||
|
||||
msg_ok "Detected OS: ${OS_TYPE} ${OS_VERSION} (${OS_CODENAME:-unknown codename})"
|
||||
return 0
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# post_progress_to_api()
|
||||
#
|
||||
|
||||
+161
@@ -1,3 +1,164 @@
|
||||
## 2026-03-14
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Patchmon: remove v prefix from pinned version [@MickLesk](https://github.com/MickLesk) ([#12891](https://github.com/community-scripts/ProxmoxVE/pull/12891))
|
||||
|
||||
### 💾 Core
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- tools.func: don't abort on AMD repo apt update failure [@MickLesk](https://github.com/MickLesk) ([#12890](https://github.com/community-scripts/ProxmoxVE/pull/12890))
|
||||
|
||||
## 2026-03-13
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Hotfix: Removed clean install usage from original script. [@nickheyer](https://github.com/nickheyer) ([#12870](https://github.com/community-scripts/ProxmoxVE/pull/12870))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Discopanel: V2 Support + Script rewrite [@nickheyer](https://github.com/nickheyer) ([#12763](https://github.com/community-scripts/ProxmoxVE/pull/12763))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- update-apps: fix restore path, add PBS support and improve restore messages [@omertahaoztop](https://github.com/omertahaoztop) ([#12528](https://github.com/community-scripts/ProxmoxVE/pull/12528))
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- fix(pve-privilege-converter): handle already stopped container in manage_states [@liuqitoday](https://github.com/liuqitoday) ([#12765](https://github.com/community-scripts/ProxmoxVE/pull/12765))
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- Update: Docs/website metadata workflow [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12858](https://github.com/community-scripts/ProxmoxVE/pull/12858))
|
||||
|
||||
## 2026-03-12
|
||||
|
||||
### 🚀 Updated Scripts
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- manyfold: fix incorrect port in upstream requests by forwarding original host [@anlopo](https://github.com/anlopo) ([#12812](https://github.com/community-scripts/ProxmoxVE/pull/12812))
|
||||
- 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))
|
||||
|
||||
- #### 🔧 Refactor
|
||||
|
||||
- Pin Patchmon to 1.4.2 [@vhsdream](https://github.com/vhsdream) ([#12789](https://github.com/community-scripts/ProxmoxVE/pull/12789))
|
||||
|
||||
### 💾 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))
|
||||
|
||||
### 🧰 Tools
|
||||
|
||||
- #### 🐞 Bug Fixes
|
||||
|
||||
- Fix PBS microcode naming [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12834](https://github.com/community-scripts/ProxmoxVE/pull/12834))
|
||||
|
||||
### 📂 Github
|
||||
|
||||
- Cleanup: remove old workflow files [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12818](https://github.com/community-scripts/ProxmoxVE/pull/12818))
|
||||
- Cleanup: remove frontend, move JSONs to json/ top-level [@MickLesk](https://github.com/MickLesk) ([#12813](https://github.com/community-scripts/ProxmoxVE/pull/12813))
|
||||
|
||||
### ❔ Uncategorized
|
||||
|
||||
- Remove json files [@michelroegl-brunner](https://github.com/michelroegl-brunner) ([#12830](https://github.com/community-scripts/ProxmoxVE/pull/12830))
|
||||
|
||||
## 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
|
||||
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
# 🤖 AI Contribution Guidelines for ProxmoxVE
|
||||
# 🤖 AI Contribution Guidelines for ProxmoxVE
|
||||
|
||||
> **This documentation is intended for all AI assistants (GitHub Copilot, Claude, ChatGPT, etc.) contributing to this project.**
|
||||
|
||||
@@ -653,15 +653,15 @@ Look at these recent well-implemented applications as reference:
|
||||
- Use of `check_for_gh_release` and `fetch_and_deploy_gh_release`
|
||||
- Correct backup/restore patterns in `update_script`
|
||||
- Footer always ends with `motd_ssh`, `customize`, `cleanup_lxc`
|
||||
- JSON metadata files created for each app
|
||||
- Website metadata requested via the website (Report issue on script page) if needed
|
||||
|
||||
---
|
||||
|
||||
## � JSON Metadata Files
|
||||
## Website Metadata (Reference)
|
||||
|
||||
Every application requires a JSON metadata file in `frontend/public/json/<appname>.json`.
|
||||
Website metadata (name, slug, description, logo, categories, etc.) is **not** added as files in the repo. Contributors request or update it via the **website**: go to the script's page and use the **Report issue** button; the flow will guide you. The structure below is a **reference** for what metadata exists (e.g. for the form or when describing what you need).
|
||||
|
||||
### JSON Structure
|
||||
### JSON Structure (Reference)
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -804,7 +804,7 @@ Or no credentials:
|
||||
- [ ] `motd_ssh`, `customize`, `cleanup_lxc` at the end of install scripts
|
||||
- [ ] No custom download/version-check logic
|
||||
- [ ] All links point to `community-scripts/ProxmoxVE` (not `ProxmoxVED`!)
|
||||
- [ ] JSON metadata file created in `frontend/public/json/<appname>.json`
|
||||
- [ ] Website metadata requested via the website (Report issue) if needed
|
||||
- [ ] Category IDs are valid (0-25)
|
||||
- [ ] Default OS version is Debian 13 or newer (unless special requirement)
|
||||
- [ ] Default resources are reasonable for the application
|
||||
@@ -832,15 +832,15 @@ Or no credentials:
|
||||
|
||||
## 🍒 Important: Cherry-Picking Your Files for PR Submission
|
||||
|
||||
⚠️ **CRITICAL**: When you submit your PR, you must use git cherry-pick to send ONLY your 3-4 files!
|
||||
⚠️ **CRITICAL**: When you submit your PR, you must use git cherry-pick to send ONLY your 2 files!
|
||||
|
||||
Why? Because `setup-fork.sh` modifies 600+ files to update links. If you commit all changes, your PR will be impossible to merge.
|
||||
|
||||
**See**: [README.md - Cherry-Pick Section](README.md#-cherry-pick-submitting-only-your-changes) for complete instructions on:
|
||||
|
||||
- Creating a clean submission branch
|
||||
- Cherry-picking only your files (ct/myapp.sh, install/myapp-install.sh, frontend/public/json/myapp.json)
|
||||
- Verifying your PR has only 3 file changes (not 600+)
|
||||
- Cherry-picking only your files (ct/myapp.sh, install/myapp-install.sh)
|
||||
- Verifying your PR has only 2 file changes (not 600+)
|
||||
|
||||
**Quick reference**:
|
||||
|
||||
@@ -849,7 +849,7 @@ Why? Because `setup-fork.sh` modifies 600+ files to update links. If you commit
|
||||
git fetch upstream
|
||||
git checkout -b submit/myapp upstream/main
|
||||
|
||||
# Cherry-pick your commit(s) or manually add your 3-4 files
|
||||
# Cherry-pick your commit(s) or manually add your 2 files
|
||||
# Then push to your fork and create PR
|
||||
```
|
||||
|
||||
@@ -865,4 +865,4 @@ git checkout -b submit/myapp upstream/main
|
||||
- [../EXIT_CODES.md](../EXIT_CODES.md) - Exit code reference
|
||||
- [templates_ct/](templates_ct/) - CT script templates
|
||||
- [templates_install/](templates_install/) - Install script templates
|
||||
- [templates_json/](templates_json/) - JSON metadata templates
|
||||
- [templates_json/](templates_json/) - Metadata structure reference; submit via website
|
||||
|
||||
+2
-2
@@ -24,9 +24,9 @@ This guide explains the current execution flow and what to verify during reviews
|
||||
- Uses `tools.func` helpers (setup\_\*).
|
||||
- Ends with `motd_ssh`, `customize`, `cleanup_lxc`.
|
||||
|
||||
### JSON Metadata
|
||||
### Website Metadata
|
||||
|
||||
- File in `frontend/public/json/<appname>.json` matches template schema.
|
||||
- Website metadata for new/updated scripts is requested via the website (Report issue on script page) where applicable.
|
||||
|
||||
### Testing
|
||||
|
||||
|
||||
+3
-3
@@ -54,15 +54,13 @@ git checkout -b add/my-awesome-app
|
||||
# 2. Create application scripts from templates
|
||||
cp docs/contribution/templates_ct/AppName.sh ct/myapp.sh
|
||||
cp docs/contribution/templates_install/AppName-install.sh install/myapp-install.sh
|
||||
cp docs/contribution/templates_json/AppName.json frontend/public/json/myapp.json
|
||||
|
||||
# 3. Edit your scripts
|
||||
nano ct/myapp.sh
|
||||
nano install/myapp-install.sh
|
||||
nano frontend/public/json/myapp.json
|
||||
|
||||
# 4. Commit and push to your fork
|
||||
git add ct/myapp.sh install/myapp-install.sh frontend/public/json/myapp.json
|
||||
git add ct/myapp.sh install/myapp-install.sh
|
||||
git commit -m "feat: add MyApp container and install scripts"
|
||||
git push origin add/my-awesome-app
|
||||
|
||||
@@ -74,6 +72,8 @@ bash -c "$(curl -fsSL https://raw.githubusercontent.com/YOUR_USERNAME/ProxmoxVE/
|
||||
|
||||
# 7. Open Pull Request on GitHub
|
||||
# Create PR from: your-fork/add/my-awesome-app → community-scripts/ProxmoxVE/main
|
||||
|
||||
# To add or change website metadata (description, logo, etc.), use the Report issue button on the script's page on the website.
|
||||
```
|
||||
|
||||
**💡 Tip**: See `../FORK_SETUP.md` for detailed fork setup and troubleshooting
|
||||
|
||||
+1
-1
@@ -149,7 +149,7 @@ fetch_and_deploy_gh_release "myapp" "owner/repo"
|
||||
2. **Only add app-specific dependencies** - Don't add ca-certificates, curl, gnupg (handled by build.func)
|
||||
3. **Test via curl from your fork** - Push first, then: `bash -c "$(curl -fsSL https://raw.githubusercontent.com/YOUR_USERNAME/ProxmoxVE/main/ct/MyApp.sh)"`
|
||||
4. **Wait for GitHub to update** - Takes 10-30 seconds after git push
|
||||
5. **Cherry-pick only YOUR files** - Submit only ct/MyApp.sh, install/MyApp-install.sh, frontend/public/json/myapp.json (3 files)
|
||||
5. **Cherry-pick only YOUR files** - Submit only ct/MyApp.sh, install/MyApp-install.sh (2 files). Website metadata: use Report issue on the script's page on the website.
|
||||
6. **Verify before PR** - Run `git diff upstream/main --name-only` to confirm only your files changed
|
||||
|
||||
---
|
||||
|
||||
+13
-18
@@ -1,21 +1,20 @@
|
||||
# JSON Metadata Files - Quick Reference
|
||||
# Website Metadata - Quick Reference
|
||||
|
||||
The metadata file (`frontend/public/json/myapp.json`) tells the web interface how to display your application.
|
||||
Metadata (name, slug, description, logo, categories, etc.) controls how your application appears on the website. You do **not** add JSON files to the repo — you request changes via the website.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## How to Request or Update Metadata
|
||||
|
||||
**Use the JSON Generator Tool:**
|
||||
[https://community-scripts.github.io/ProxmoxVE/json-editor](https://community-scripts.github.io/ProxmoxVE/json-editor)
|
||||
|
||||
1. Enter application details
|
||||
2. Generator creates `frontend/public/json/myapp.json`
|
||||
3. Copy the output to your contribution
|
||||
1. **Go to the script on the website** — Open the [ProxmoxVE website](https://community-scripts.github.io/ProxmoxVE/), find your script (or the script you want to update).
|
||||
2. **Press the "Report issue" button** on that script’s page.
|
||||
3. **Follow the guide** — The flow will walk you through submitting or updating metadata.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
## Metadata Structure (Reference)
|
||||
|
||||
The following describes the structure of script metadata used by the website. Use it as reference when filling out the form or describing what you need.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -148,18 +147,14 @@ Each installation method specifies resource requirements:
|
||||
|
||||
---
|
||||
|
||||
## Reference Examples
|
||||
## See Examples on the Website
|
||||
|
||||
See actual examples in the repo:
|
||||
|
||||
- [frontend/public/json/trip.json](https://github.com/community-scripts/ProxmoxVE/blob/main/frontend/public/json/trip.json)
|
||||
- [frontend/public/json/thingsboard.json](https://github.com/community-scripts/ProxmoxVE/blob/main/frontend/public/json/thingsboard.json)
|
||||
- [frontend/public/json/unifi.json](https://github.com/community-scripts/ProxmoxVE/blob/main/frontend/public/json/unifi.json)
|
||||
View script pages on the [ProxmoxVE website](https://community-scripts.github.io/ProxmoxVE/) to see how metadata is displayed for existing scripts.
|
||||
|
||||
---
|
||||
|
||||
## Need Help?
|
||||
|
||||
- **[JSON Generator](https://community-scripts.github.io/ProxmoxVE/json-editor)** - Interactive tool
|
||||
- **Request metadata** — Use the Report issue button on the script’s page on the website (see [How to Request or Update Metadata](#how-to-request-or-update-metadata) above).
|
||||
- **[JSON Generator](https://community-scripts.github.io/ProxmoxVE/json-editor)** - Reference only; structure validation
|
||||
- **[README.md](../README.md)** - Full contribution workflow
|
||||
- **[Quick Start](../README.md)** - Step-by-step guide
|
||||
|
||||
+14
-7
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-03-14T06:30:27Z",
|
||||
"generated": "2026-03-15T12:22:39Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "agregarr",
|
||||
@@ -18,23 +18,23 @@
|
||||
{
|
||||
"slug": "llamacpp",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"version": "b8329",
|
||||
"version": "b8354",
|
||||
"pinned": false,
|
||||
"date": "2026-03-14T04:58:36Z"
|
||||
"date": "2026-03-15T10:06:38Z"
|
||||
},
|
||||
{
|
||||
"slug": "localai",
|
||||
"repo": "mudler/LocalAI",
|
||||
"version": "v3.12.1",
|
||||
"version": "v4.0.0",
|
||||
"pinned": false,
|
||||
"date": "2026-02-21T13:49:24Z"
|
||||
"date": "2026-03-14T18:18:41Z"
|
||||
},
|
||||
{
|
||||
"slug": "localrecall",
|
||||
"repo": "mudler/LocalRecall",
|
||||
"version": "v0.5.5",
|
||||
"version": "v0.5.8",
|
||||
"pinned": false,
|
||||
"date": "2026-02-16T22:38:06Z"
|
||||
"date": "2026-03-14T22:09:26Z"
|
||||
},
|
||||
{
|
||||
"slug": "maintainerr",
|
||||
@@ -50,6 +50,13 @@
|
||||
"pinned": false,
|
||||
"date": "2026-03-11T20:23:52Z"
|
||||
},
|
||||
{
|
||||
"slug": "ragflow",
|
||||
"repo": "infiniflow/ragflow",
|
||||
"version": "v0.24.0",
|
||||
"pinned": true,
|
||||
"date": "2026-02-10T09:27:14Z"
|
||||
},
|
||||
{
|
||||
"slug": "wakapi",
|
||||
"repo": "muety/wakapi",
|
||||
|
||||
+123
-47
@@ -1,22 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { FolderOpen } from "lucide-react";
|
||||
import { FolderOpen, Search as SearchIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useQueryState } from "nuqs";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { Suspense, useEffect, useState, useMemo } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search } from "@/components/search";
|
||||
|
||||
function CategoriesContent() {
|
||||
const [categories, setCategories] = useState<Category[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedCategory, setSelectedCategory] = useQueryState("category");
|
||||
const [search, setSearch] = useQueryState("search");
|
||||
|
||||
useEffect(() => {
|
||||
fetchCategories()
|
||||
@@ -38,6 +39,49 @@ function CategoriesContent() {
|
||||
return { totalScripts, devScripts };
|
||||
};
|
||||
|
||||
// Filter categories by search
|
||||
const filteredCategories = useMemo(() => {
|
||||
if (!search) return categories;
|
||||
|
||||
const searchLower = search.toLowerCase();
|
||||
return categories.filter((category) => {
|
||||
// Search in category name
|
||||
if (category.name.toLowerCase().includes(searchLower)) return true;
|
||||
|
||||
// Search in category description
|
||||
if (category.description?.toLowerCase().includes(searchLower)) return true;
|
||||
|
||||
// Search in script names within category
|
||||
const hasMatchingScript = category.scripts?.some(
|
||||
(script) =>
|
||||
script.name.toLowerCase().includes(searchLower) ||
|
||||
script.description.toLowerCase().includes(searchLower)
|
||||
);
|
||||
|
||||
return hasMatchingScript;
|
||||
});
|
||||
}, [categories, search]);
|
||||
|
||||
// Calculate total scripts
|
||||
const totalScripts = categories.reduce(
|
||||
(acc, cat) => acc + (cat.scripts?.length || 0),
|
||||
0
|
||||
);
|
||||
|
||||
// Calculate matching scripts in filtered categories
|
||||
const matchingScripts = useMemo(() => {
|
||||
if (!search) return totalScripts;
|
||||
|
||||
return filteredCategories.reduce((acc, cat) => {
|
||||
const matchingInCategory = cat.scripts?.filter(
|
||||
(script) =>
|
||||
script.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
script.description.toLowerCase().includes(search.toLowerCase())
|
||||
).length || 0;
|
||||
return acc + matchingInCategory;
|
||||
}, 0);
|
||||
}, [filteredCategories, search, totalScripts]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex h-[50vh] w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
@@ -46,11 +90,6 @@ function CategoriesContent() {
|
||||
);
|
||||
}
|
||||
|
||||
const totalScripts = categories.reduce(
|
||||
(acc, cat) => acc + (cat.scripts?.length || 0),
|
||||
0,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mb-3">
|
||||
<div className="mt-20 px-4 xl:px-0">
|
||||
@@ -68,44 +107,81 @@ function CategoriesContent() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Categories Grid */}
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{categories.map((category) => {
|
||||
const { totalScripts, devScripts } = getCategoryStats(category);
|
||||
return (
|
||||
<Link
|
||||
key={category.id}
|
||||
href={`/scripts?category=${category.id}`}
|
||||
className="group"
|
||||
>
|
||||
<Card className="h-full transition-all hover:border-primary/50 hover:shadow-md cursor-pointer">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">{category.name}</CardTitle>
|
||||
<FolderOpen className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">
|
||||
{totalScripts}
|
||||
{" "}
|
||||
{totalScripts === 1 ? "script" : "scripts"}
|
||||
</Badge>
|
||||
{devScripts > 0 && (
|
||||
<Badge variant="outline" className="text-orange-500 border-orange-500/50">
|
||||
{devScripts}
|
||||
{" "}
|
||||
in dev
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
{/* Search */}
|
||||
<div className="mb-6">
|
||||
<Search
|
||||
placeholder="Search categories or scripts..."
|
||||
className="w-full max-w-md"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Search Results Info */}
|
||||
{search && (
|
||||
<div className="mb-4 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<SearchIcon className="h-4 w-4" />
|
||||
<span>
|
||||
Found {filteredCategories.length} categories
|
||||
{matchingScripts > 0 && ` with ${matchingScripts} matching scripts`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Categories Grid */}
|
||||
{filteredCategories.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<SearchIcon className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-foreground/80">No categories found</h3>
|
||||
<p className="text-muted-foreground">Try adjusting your search</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{filteredCategories.map((category) => {
|
||||
const { totalScripts, devScripts } = getCategoryStats(category);
|
||||
return (
|
||||
<Link
|
||||
key={category.id}
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: search ? { category: category.id, search } : { category: category.id },
|
||||
}}
|
||||
className="group"
|
||||
>
|
||||
<Card className="h-full transition-all hover:border-brass/50 hover:shadow-md cursor-pointer border-rust/30">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg text-foreground/90 group-hover:text-brass transition-colors">
|
||||
{category.name}
|
||||
</CardTitle>
|
||||
<FolderOpen className="h-5 w-5 text-muted-foreground group-hover:text-brass transition-colors" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<Badge variant="secondary" className="bg-accent">
|
||||
{totalScripts}
|
||||
{" "}
|
||||
{totalScripts === 1 ? "script" : "scripts"}
|
||||
</Badge>
|
||||
{devScripts > 0 && (
|
||||
<Badge variant="outline" className="text-orange-500 border-orange-500/50">
|
||||
{devScripts}
|
||||
{" "}
|
||||
in dev
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{category.description && (
|
||||
<p className="mt-2 text-sm text-muted-foreground line-clamp-2">
|
||||
{category.description}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,11 +191,11 @@ function CategoriesContent() {
|
||||
export default function CategoriesPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={(
|
||||
fallback={
|
||||
<div className="flex h-[50vh] w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<CategoriesContent />
|
||||
</Suspense>
|
||||
|
||||
+218
-27
@@ -1,12 +1,21 @@
|
||||
"use client";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { Loader2, X } from "lucide-react";
|
||||
import { Suspense, useEffect, useState, useMemo } from "react";
|
||||
import { Loader2, X, Search as SearchIcon } from "lucide-react";
|
||||
import { useQueryState } from "nuqs";
|
||||
|
||||
import type { Category, Script } from "@/lib/types";
|
||||
|
||||
import { ScriptItem } from "@/app/scripts/_components/script-item";
|
||||
import { fetchCategories } from "@/lib/data";
|
||||
import { Search } from "@/components/search";
|
||||
import { AdvancedFilter } from "@/components/advanced-filter";
|
||||
import {
|
||||
filterScripts,
|
||||
hasActiveFilters,
|
||||
sortScriptsByDate,
|
||||
type FilterState,
|
||||
type ScriptType,
|
||||
} from "@/lib/filter-utils";
|
||||
|
||||
import { LatestScripts, MostViewedScripts } from "./_components/script-info-blocks";
|
||||
import Sidebar from "./_components/sidebar";
|
||||
@@ -20,6 +29,53 @@ function ScriptContent() {
|
||||
const [item, setItem] = useState<Script>();
|
||||
const [latestPage, setLatestPage] = useState(1);
|
||||
|
||||
// Filter state from URL
|
||||
const [search, setSearch] = useQueryState("search");
|
||||
const [types, setTypes] = useQueryState("types");
|
||||
const [categoryIds, setCategoryIds] = useQueryState("categories");
|
||||
const [status, setStatus] = useQueryState("status");
|
||||
const [minCpu, setMinCpu] = useQueryState("minCpu");
|
||||
const [maxCpu, setMaxCpu] = useQueryState("maxCpu");
|
||||
const [minRam, setMinRam] = useQueryState("minRam");
|
||||
const [maxRam, setMaxRam] = useQueryState("maxRam");
|
||||
|
||||
// Parse filter state
|
||||
const filterState: FilterState = useMemo(() => ({
|
||||
search: search || "",
|
||||
types: (types?.split(",").filter(Boolean) as ScriptType[]) || [],
|
||||
categoryIds: (categoryIds?.split(",").filter(Boolean).map(Number)) || [],
|
||||
status: (status as FilterState["status"]) || "all",
|
||||
minCpu: minCpu ? parseInt(minCpu) : null,
|
||||
maxCpu: maxCpu ? parseInt(maxCpu) : null,
|
||||
minRam: minRam ? parseInt(minRam) : null,
|
||||
maxRam: maxRam ? parseInt(maxRam) : null,
|
||||
}), [search, types, categoryIds, status, minCpu, maxCpu, minRam, maxRam]);
|
||||
|
||||
// Get all scripts from all categories
|
||||
const allScripts = useMemo(() => {
|
||||
if (!links.length) return [];
|
||||
const scripts = links.flatMap((category) => category.scripts || []);
|
||||
// Remove duplicates by slug
|
||||
const uniqueScripts = new Map<string, Script>();
|
||||
scripts.forEach((script) => {
|
||||
if (!uniqueScripts.has(script.slug)) {
|
||||
uniqueScripts.set(script.slug, script);
|
||||
}
|
||||
});
|
||||
return Array.from(uniqueScripts.values());
|
||||
}, [links]);
|
||||
|
||||
// Filter scripts based on current filter state
|
||||
const filteredScripts = useMemo(() => {
|
||||
if (!hasActiveFilters(filterState)) {
|
||||
return allScripts;
|
||||
}
|
||||
return filterScripts(allScripts, filterState, links);
|
||||
}, [allScripts, filterState, links]);
|
||||
|
||||
// Check if any filters are active
|
||||
const isFiltering = hasActiveFilters(filterState);
|
||||
|
||||
const closeScript = () => {
|
||||
window.history.pushState({}, document.title, window.location.pathname);
|
||||
setSelectedScript(null);
|
||||
@@ -28,9 +84,9 @@ function ScriptContent() {
|
||||
useEffect(() => {
|
||||
if (selectedScript && links.length > 0) {
|
||||
const script = links
|
||||
.map(category => category.scripts)
|
||||
.map((category) => category.scripts)
|
||||
.flat()
|
||||
.find(script => script.slug === selectedScript);
|
||||
.find((script) => script.slug === selectedScript);
|
||||
setItem(script);
|
||||
if (script) {
|
||||
document.title = `${script.name} | Heretek AI`;
|
||||
@@ -45,7 +101,7 @@ function ScriptContent() {
|
||||
.then((categories) => {
|
||||
setLinks(categories);
|
||||
})
|
||||
.catch(error => console.error(error));
|
||||
.catch((error) => console.error(error));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -61,43 +117,178 @@ function ScriptContent() {
|
||||
/>
|
||||
</div>
|
||||
<div className="px-4 w-full sm:max-w-[calc(100%-350px-16px)]">
|
||||
{selectedScript && item
|
||||
? (
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground/90">Selected Script</h2>
|
||||
<button
|
||||
onClick={closeScript}
|
||||
className="rounded-full p-2 text-muted-foreground hover:bg-card/50 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
{selectedScript && item ? (
|
||||
<div className="flex w-full flex-col">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-2xl font-semibold tracking-tight text-foreground/90">
|
||||
Selected Script
|
||||
</h2>
|
||||
<button
|
||||
onClick={closeScript}
|
||||
className="rounded-full p-2 text-muted-foreground hover:bg-card/50 transition-colors"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
<ScriptItem item={item} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
{/* Search and Filter Section */}
|
||||
<div className="space-y-4">
|
||||
<Search
|
||||
placeholder="Search scripts by name or description..."
|
||||
className="w-full"
|
||||
/>
|
||||
<AdvancedFilter categories={links} />
|
||||
</div>
|
||||
|
||||
{/* Results Count */}
|
||||
{isFiltering && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<SearchIcon className="h-4 w-4" />
|
||||
<span>
|
||||
{filteredScripts.length} script{filteredScripts.length !== 1 ? "s" : ""} found
|
||||
</span>
|
||||
</div>
|
||||
<ScriptItem item={item} />
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div className="flex w-full flex-col gap-5">
|
||||
<LatestScripts items={links} page={latestPage} onPageChange={setLatestPage} />
|
||||
<MostViewedScripts items={links} />
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Script Lists */}
|
||||
{isFiltering ? (
|
||||
<FilteredScriptsList scripts={filteredScripts} onSelect={setSelectedScript} />
|
||||
) : (
|
||||
<>
|
||||
<LatestScripts items={links} page={latestPage} onPageChange={setLatestPage} />
|
||||
<MostViewedScripts items={links} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Component to display filtered scripts
|
||||
function FilteredScriptsList({
|
||||
scripts,
|
||||
onSelect,
|
||||
}: {
|
||||
scripts: Script[];
|
||||
onSelect: (slug: string) => void;
|
||||
}) {
|
||||
const sortedScripts = useMemo(() => sortScriptsByDate(scripts), [scripts]);
|
||||
|
||||
if (sortedScripts.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<SearchIcon className="h-12 w-12 text-muted-foreground/50 mb-4" />
|
||||
<h3 className="text-lg font-semibold text-foreground/80">No scripts found</h3>
|
||||
<p className="text-muted-foreground">Try adjusting your search or filters</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-1">
|
||||
{sortedScripts.map((script) => (
|
||||
<ScriptCard key={script.slug} script={script} onSelect={onSelect} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple script card for filtered results
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { basePath } from "@/config/site-config";
|
||||
import { extractDate } from "@/lib/time";
|
||||
import { CalendarPlus } from "lucide-react";
|
||||
|
||||
function ScriptCard({
|
||||
script,
|
||||
onSelect,
|
||||
}: {
|
||||
script: Script;
|
||||
onSelect: (slug: string) => void;
|
||||
}) {
|
||||
const getDisplayValueFromType = (type: string) => {
|
||||
switch (type) {
|
||||
case "ct":
|
||||
return "LXC";
|
||||
case "vm":
|
||||
return "VM";
|
||||
case "pve":
|
||||
return "PVE";
|
||||
case "addon":
|
||||
return "ADDON";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="min-w-[250px] flex-1 flex-grow bg-accent/30 hover:border-brass/50 transition-colors">
|
||||
<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>
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={{
|
||||
pathname: "/scripts",
|
||||
query: { id: script.slug },
|
||||
}}
|
||||
>
|
||||
View Script
|
||||
</Link>
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={(
|
||||
fallback={
|
||||
<div className="flex h-screen w-full flex-col items-center justify-center gap-5 bg-background px-4 md:px-6">
|
||||
<div className="space-y-2 text-center">
|
||||
<Loader2 className="h-10 w-10 animate-spin" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
}
|
||||
>
|
||||
<ScriptContent />
|
||||
</Suspense>
|
||||
|
||||
+26
-43
@@ -6,7 +6,6 @@ import Link from "next/link";
|
||||
|
||||
import { navbarLinks } from "@/config/site-config";
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||
import { GitHubStarsButton } from "./animate-ui/components/buttons/github-stars";
|
||||
import { Button } from "./animate-ui/components/buttons/button";
|
||||
import MobileSidebar from "./navigation/mobile-sidebar";
|
||||
@@ -34,8 +33,8 @@ function Navbar() {
|
||||
<>
|
||||
<div
|
||||
className={`fixed left-0 top-0 z-50 flex w-screen justify-center px-4 xl:px-0 transition-all duration-300 ${isScrolled
|
||||
? "glass rust-border border-b bg-background/50"
|
||||
: ""
|
||||
? "glass rust-border border-b bg-background/50"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-20 w-full max-w-[1440px] items-center justify-between sm:flex-row">
|
||||
@@ -58,52 +57,36 @@ function Navbar() {
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center gap-2">
|
||||
{navbarLinks.filter(link => !link.external).map(({ href, event, icon, text }) => (
|
||||
<TooltipProvider key={event}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
asChild
|
||||
className="text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300"
|
||||
>
|
||||
<Link href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="ml-2 hidden lg:inline">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs bg-card border-rust-500/30">
|
||||
{text}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Button
|
||||
key={event}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
asChild
|
||||
className="text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300"
|
||||
>
|
||||
<Link href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="ml-2 hidden lg:inline">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex sm:gap-2">
|
||||
<CommandMenu />
|
||||
<GitHubStarsButton username="Heretek-AI" repo="ProxmoxVE" className="hidden md:flex" />
|
||||
{navbarLinks.filter(link => link.external).map(({ href, event, icon, text, mobileHidden }) => (
|
||||
<TooltipProvider key={event}>
|
||||
<Tooltip delayDuration={100}>
|
||||
<TooltipTrigger className={mobileHidden ? "hidden lg:block" : ""}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
asChild
|
||||
className="text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300"
|
||||
>
|
||||
<Link target="_blank" href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="sr-only">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" className="text-xs bg-card border-rust-500/30">
|
||||
{text}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<Button
|
||||
key={event}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
asChild
|
||||
className={`text-muted-foreground hover:text-rust-400 hover:bg-rust-500/10 transition-colors duration-300 ${mobileHidden ? "hidden lg:flex" : ""}`}
|
||||
>
|
||||
<Link target="_blank" href={href} data-umami-event={event}>
|
||||
{icon}
|
||||
<span className="sr-only">{text}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
))}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
+7
-1
@@ -1,4 +1,4 @@
|
||||
import { MessagesSquare, Scroll, FolderOpen, FileCode } from "lucide-react";
|
||||
import { MessagesSquare, Scroll, FolderOpen, FileCode, Terminal } from "lucide-react";
|
||||
import { FaDiscord, FaGithub } from "react-icons/fa";
|
||||
import React from "react";
|
||||
|
||||
@@ -20,6 +20,12 @@ export const navbarLinks = [
|
||||
icon: <FolderOpen className="h-4 w-4" />,
|
||||
text: "Categories",
|
||||
},
|
||||
{
|
||||
href: "/generator",
|
||||
event: "Generator",
|
||||
icon: <Terminal className="h-4 w-4" />,
|
||||
text: "Generator",
|
||||
},
|
||||
{
|
||||
href: "/community",
|
||||
event: "Community",
|
||||
|
||||
+23
-3
@@ -29,12 +29,20 @@ jobs:
|
||||
has_changes: ${{ steps.check.outputs.has_changes }}
|
||||
commit_count: ${{ steps.check.outputs.commit_count }}
|
||||
steps:
|
||||
- name: Validate PAT_WORKFLOW secret
|
||||
run: |
|
||||
if [ -z "${{ secrets.PAT_WORKFLOW }}" ]; then
|
||||
echo "::error::PAT_WORKFLOW secret is not set. Please create a Personal Access Token with 'repo' scope and add it as a repository secret named PAT_WORKFLOW."
|
||||
echo "See: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ env.FORK_BRANCH }}
|
||||
token: ${{ secrets.PAT_WORKFLOW || secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.PAT_WORKFLOW }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
@@ -82,12 +90,24 @@ jobs:
|
||||
if: needs.check-and-sync.outputs.has_changes == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate PAT_WORKFLOW secret
|
||||
run: |
|
||||
if [ -z "${{ secrets.PAT_WORKFLOW }}" ]; then
|
||||
echo "::error::PAT_WORKFLOW secret is not set. Please create a Personal Access Token with 'repo' scope and add it as a repository secret named PAT_WORKFLOW."
|
||||
echo "See: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens"
|
||||
exit 1
|
||||
fi
|
||||
echo "PAT_WORKFLOW secret is configured."
|
||||
echo "Note: For fine-grained PATs, ensure these permissions are set:"
|
||||
echo " - Contents: Read and write"
|
||||
echo " - Pull requests: Read and write"
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ env.FORK_BRANCH }}
|
||||
token: ${{ secrets.PAT_WORKFLOW || secrets.GITHUB_TOKEN }}
|
||||
token: ${{ secrets.PAT_WORKFLOW }}
|
||||
|
||||
- name: Configure Git with merge driver
|
||||
run: |
|
||||
@@ -191,7 +211,7 @@ jobs:
|
||||
|
||||
- name: Create Pull Request
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GH_TOKEN: ${{ secrets.PAT_WORKFLOW }}
|
||||
run: |
|
||||
BRANCH="${{ steps.branch.outputs.branch_name }}"
|
||||
HAS_CONFLICTS="${{ steps.merge.outputs.has_conflicts }}"
|
||||
|
||||
+60
-33
@@ -1,38 +1,65 @@
|
||||
# ---------------------------------------
|
||||
# Treat Shell files as first-class code
|
||||
# ---------------------------------------
|
||||
*.sh linguist-detectable=true
|
||||
*.bash linguist-language=Shell
|
||||
*.func linguist-language=Shell
|
||||
*.install linguist-language=Shell
|
||||
# Git Attributes for Heretek-AI/ProxmoxVE Fork
|
||||
# These attributes configure how git handles merges from upstream
|
||||
# Files with 'merge=ours' will always keep the fork version during merges
|
||||
|
||||
# ---------------------------------------
|
||||
# Treat Golang files as Go (for /api/)
|
||||
api/**/*.go linguist-language=Go
|
||||
# =============================================================================
|
||||
# CUSTOM SCRIPT DIRECTORIES - Always preserve fork versions
|
||||
# =============================================================================
|
||||
|
||||
# ---------------------------------------
|
||||
# Treat frontend as JavaScript/TypeScript (optional)
|
||||
frontend/**/*.ts linguist-language=TypeScript
|
||||
frontend/**/*.js linguist-language=JavaScript
|
||||
# Custom install scripts
|
||||
install/ merge=ours
|
||||
install/*.sh merge=ours
|
||||
|
||||
# ---------------------------------------
|
||||
# Exclude documentation from stats
|
||||
*.md linguist-documentation
|
||||
docs/** linguist-documentation
|
||||
README.md linguist-documentation
|
||||
CONTRIBUTING.md linguist-documentation
|
||||
SECURITY.md linguist-documentation
|
||||
# Custom container scripts
|
||||
ct/ merge=ours
|
||||
ct/*.sh merge=ours
|
||||
ct/headers/ merge=ours
|
||||
ct/headers/* merge=ours
|
||||
|
||||
# ---------------------------------------
|
||||
# Exclude generated/config files
|
||||
*.json linguist-generated
|
||||
frontend/public/json/*.json linguist-generated=false
|
||||
*.lock linguist-generated
|
||||
*.yml linguist-generated
|
||||
*.yaml linguist-generated
|
||||
.github/** linguist-generated
|
||||
.vscode/** linguist-generated
|
||||
# Custom frontend (entire directory)
|
||||
frontend/ merge=ours
|
||||
frontend/** merge=ours
|
||||
|
||||
# ---------------------------------------
|
||||
# Standard text handling
|
||||
* text=auto eol=lf
|
||||
# Custom images
|
||||
misc/images/ merge=ours
|
||||
misc/images/* merge=ours
|
||||
|
||||
# Custom addon tools
|
||||
tools/addon/ merge=ours
|
||||
tools/addon/* merge=ours
|
||||
|
||||
# =============================================================================
|
||||
# WORKFLOW CONFIGURATION - Preserve fork-specific CI/CD
|
||||
# =============================================================================
|
||||
|
||||
# Custom GitHub Actions workflows
|
||||
.github/workflows/ merge=ours
|
||||
.github/workflows/* merge=ours
|
||||
|
||||
# Custom runner configurations
|
||||
.github/runner/ merge=ours
|
||||
.github/runner/** merge=ours
|
||||
|
||||
# =============================================================================
|
||||
# DOCUMENTATION - Preserve fork-specific branding
|
||||
# =============================================================================
|
||||
|
||||
# Custom README with fork branding
|
||||
README.md merge=ours
|
||||
|
||||
# Custom CHANGELOG
|
||||
CHANGELOG.md merge=ours
|
||||
|
||||
# =============================================================================
|
||||
# DEFAULT ATTRIBUTES
|
||||
# =============================================================================
|
||||
|
||||
# Auto-detect text files and perform LF normalization
|
||||
* text=auto
|
||||
|
||||
# Shell scripts should always have LF line endings
|
||||
*.sh text eol=lf
|
||||
|
||||
# GitHub Actions workflows should have LF line endings
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
+259
-124
@@ -1,155 +1,290 @@
|
||||
name: Sync Upstream (Create Pull Request)
|
||||
name: Sync Upstream (Git Merge Strategy)
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Runs automatically every day at 2:00 AM UTC. Adjust as needed.
|
||||
# Runs automatically every day at 2:00 AM UTC
|
||||
- cron: '0 2 * * *'
|
||||
workflow_dispatch: # Allows you to run this manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
# Allows manual trigger from the Actions tab
|
||||
inputs:
|
||||
force_sync:
|
||||
description: 'Force sync even if no new commits detected'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: write # Required to allow the GitHub Actions bot to push commits
|
||||
pull-requests: write # Required to create pull requests
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
env:
|
||||
UPSTREAM_REPO: community-scripts/ProxmoxVE
|
||||
UPSTREAM_BRANCH: main
|
||||
FORK_BRANCH: main
|
||||
|
||||
jobs:
|
||||
check-and-sync:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
has_changes: ${{ steps.check.outputs.has_changes }}
|
||||
commit_count: ${{ steps.check.outputs.commit_count }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ env.FORK_BRANCH }}
|
||||
token: ${{ secrets.PAT_WORKFLOW || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Configure Git
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# Configure the 'ours' merge driver for fork-specific files
|
||||
# This tells git to use our version for files marked with merge=ours in .gitattributes
|
||||
git config merge.ours.name "ours merge driver"
|
||||
git config merge.ours.driver "true"
|
||||
|
||||
- name: Add upstream remote
|
||||
run: |
|
||||
git remote add upstream https://github.com/${{ env.UPSTREAM_REPO }}.git || true
|
||||
git fetch upstream ${{ env.UPSTREAM_BRANCH }}
|
||||
|
||||
- name: Check for new upstream commits
|
||||
id: check
|
||||
run: |
|
||||
# Get the merge base between fork and upstream
|
||||
MERGE_BASE=$(git merge-base HEAD upstream/${{ env.UPSTREAM_BRANCH }} 2>/dev/null || echo "")
|
||||
|
||||
if [ -z "$MERGE_BASE" ]; then
|
||||
echo "No common ancestor found - this may be a fresh sync"
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "commit_count=unknown" >> $GITHUB_OUTPUT
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count commits in upstream that are not in fork
|
||||
UPSTREAM_COMMITS=$(git rev-list $MERGE_BASE..upstream/${{ env.UPSTREAM_BRANCH }} --count 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$UPSTREAM_COMMITS" -eq 0 ] && [ "${{ github.event.inputs.force_sync }}" != "true" ]; then
|
||||
echo "No new commits from upstream. Nothing to sync."
|
||||
echo "has_changes=false" >> $GITHUB_OUTPUT
|
||||
echo "commit_count=0" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "Found $UPSTREAM_COMMITS new commits from upstream."
|
||||
echo "has_changes=true" >> $GITHUB_OUTPUT
|
||||
echo "commit_count=$UPSTREAM_COMMITS" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
sync-and-create-pr:
|
||||
needs: check-and-sync
|
||||
if: needs.check-and-sync.outputs.has_changes == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Fetches all history for all branches and tags
|
||||
# If your default branch is not 'main', change it here:
|
||||
ref: main
|
||||
fetch-depth: 0
|
||||
ref: ${{ env.FORK_BRANCH }}
|
||||
token: ${{ secrets.PAT_WORKFLOW || secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Sync upstream and create pull request
|
||||
- name: Configure Git with merge driver
|
||||
run: |
|
||||
# 1. Configure Git for the Actions bot
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
|
||||
# 2. Add the upstream repository
|
||||
git remote add upstream https://github.com/community-scripts/ProxmoxVE.git
|
||||
git fetch upstream main
|
||||
|
||||
# 3. Check if there are any new commits from upstream
|
||||
UPSTREAM_COMMITS=$(git log HEAD..upstream/main --oneline 2>/dev/null | wc -l)
|
||||
if [ "$UPSTREAM_COMMITS" -eq 0 ]; then
|
||||
echo "No new commits from upstream. Nothing to sync."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Found $UPSTREAM_COMMITS new commits from upstream."
|
||||
|
||||
# 4. Backup ignored directories and files BEFORE creating sync branch
|
||||
echo "Backing up fork-specific directories and files..."
|
||||
mkdir -p /tmp/backup
|
||||
cp -r install /tmp/backup/
|
||||
cp -r ct /tmp/backup/
|
||||
cp -r frontend /tmp/backup/
|
||||
cp -r misc/images /tmp/backup/images
|
||||
cp -r .github/workflows /tmp/backup/workflows
|
||||
cp -r .github/runner /tmp/backup/runner
|
||||
cp -r tools/addon /tmp/backup/addon
|
||||
cp CHANGELOG.md /tmp/backup/
|
||||
cp README.md /tmp/backup/
|
||||
|
||||
# 5. Create a new branch from upstream/main for the PR
|
||||
# Configure the 'ours' merge driver
|
||||
# Files with merge=ours in .gitattributes will automatically keep fork version
|
||||
git config merge.ours.name "ours merge driver"
|
||||
git config merge.ours.driver "true"
|
||||
|
||||
- name: Add upstream remote
|
||||
run: |
|
||||
git remote add upstream https://github.com/${{ env.UPSTREAM_REPO }}.git || true
|
||||
git fetch upstream ${{ env.UPSTREAM_BRANCH }}
|
||||
|
||||
- name: Create sync branch
|
||||
id: branch
|
||||
run: |
|
||||
BRANCH_NAME="upstream-sync-$(date +%Y%m%d-%H%M%S)"
|
||||
echo "Creating sync branch: $BRANCH_NAME"
|
||||
git checkout -b "$BRANCH_NAME" upstream/main
|
||||
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
|
||||
- name: Merge upstream with fork preservation
|
||||
id: merge
|
||||
run: |
|
||||
echo "Starting merge from upstream/${{ env.UPSTREAM_BRANCH }}..."
|
||||
|
||||
# 6. Restore fork-specific files on this branch
|
||||
echo "Restoring fork-specific directories and files..."
|
||||
rm -rf install ct frontend misc/images
|
||||
cp -r /tmp/backup/install .
|
||||
cp -r /tmp/backup/ct .
|
||||
cp -r /tmp/backup/frontend .
|
||||
mkdir -p misc
|
||||
cp -r /tmp/backup/images misc/images
|
||||
rm -rf .github/workflows
|
||||
mkdir -p .github
|
||||
cp -r /tmp/backup/workflows .github/workflows
|
||||
rm -rf .github/runner
|
||||
cp -r /tmp/backup/runner .github/runner
|
||||
rm -rf tools/addon
|
||||
mkdir -p tools
|
||||
cp -r /tmp/backup/addon tools/addon
|
||||
cp /tmp/backup/CHANGELOG.md .
|
||||
cp /tmp/backup/README.md .
|
||||
# Attempt merge with the 'ours' strategy for conflicts
|
||||
# The .gitattributes file configures which files use merge=ours
|
||||
git merge upstream/${{ env.UPSTREAM_BRANCH }} \
|
||||
--no-edit \
|
||||
-m "Merge upstream ${{ env.UPSTREAM_REPO }} into ${{ env.FORK_BRANCH }}" \
|
||||
2>&1 || MERGE_STATUS=$?
|
||||
|
||||
# 7. Stage all restored files
|
||||
git add install ct frontend misc/images .github/workflows .github/runner tools/addon CHANGELOG.md README.md
|
||||
|
||||
# 8. Check for any merge conflicts in other files
|
||||
# Since we're creating from upstream/main, we need to check against main
|
||||
git fetch origin main
|
||||
CONFLICT_FILES=$(git diff --name-only origin/main HEAD | head -50)
|
||||
|
||||
# 9. Commit the sync with preserved files
|
||||
if ! git diff --staged --quiet; then
|
||||
git commit -m "Sync from upstream - preserving fork-specific customizations"
|
||||
if [ "${MERGE_STATUS:-0}" -eq 0 ]; then
|
||||
echo "Merge completed successfully with no conflicts."
|
||||
echo "merge_status=success" >> $GITHUB_OUTPUT
|
||||
echo "has_conflicts=false" >> $GITHUB_OUTPUT
|
||||
elif [ "${MERGE_STATUS:-0}" -eq 1 ]; then
|
||||
echo "Merge has conflicts that need resolution."
|
||||
echo "merge_status=conflicts" >> $GITHUB_OUTPUT
|
||||
echo "has_conflicts=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# List conflicted files
|
||||
CONFLICTS=$(git diff --name-only --diff-filter=U 2>/dev/null || echo "")
|
||||
echo "conflicted_files<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$CONFLICTS" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# Check if conflicts are only in fork-specific files
|
||||
# These should have been handled by merge=ours, but check anyway
|
||||
FORK_FILES=$(git diff --name-only --diff-filter=U \
|
||||
-- 'install/' 'ct/' 'frontend/' 'misc/images/' '.github/workflows/' '.github/runner/' 'tools/addon/' 'README.md' 'CHANGELOG.md' 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$FORK_FILES" ]; then
|
||||
echo "Warning: Fork-specific files have unexpected conflicts:"
|
||||
echo "$FORK_FILES"
|
||||
# For fork files, always take our version
|
||||
for file in $FORK_FILES; do
|
||||
git checkout --ours "$file" 2>/dev/null || true
|
||||
git add "$file" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
# For other conflicts, we leave them for manual resolution in the PR
|
||||
# This allows reviewers to see what needs attention
|
||||
else
|
||||
echo "Merge failed with status: ${MERGE_STATUS}"
|
||||
echo "merge_status=failed" >> $GITHUB_OUTPUT
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Get upstream changes summary
|
||||
id: summary
|
||||
run: |
|
||||
# Get list of upstream commits being merged
|
||||
echo "upstream_commits<<EOF" >> $GITHUB_OUTPUT
|
||||
git log HEAD..upstream/${{ env.UPSTREAM_BRANCH }} --oneline --no-merges | head -30 >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get list of changed files for PR description
|
||||
CHANGED_FILES=$(git diff --name-only origin/main HEAD | head -100)
|
||||
UPSTREAM_LOG=$(git log origin/main..HEAD --oneline | head -20)
|
||||
# Get changed files summary
|
||||
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
|
||||
git diff --name-only HEAD upstream/${{ env.UPSTREAM_BRANCH }} | head -50 >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
# 10. Push the branch
|
||||
git push origin HEAD --force
|
||||
|
||||
# 11. Create PR using GitHub CLI
|
||||
echo "Creating pull request for upstream sync..."
|
||||
gh pr create \
|
||||
--title "🔄 Upstream Sync - $(date +%Y-%m-%d)" \
|
||||
--body "This PR syncs the latest changes from upstream [\`community-scripts/ProxmoxVE\`](https://github.com/community-scripts/ProxmoxVE) while preserving fork-specific customizations.
|
||||
# Count files by category
|
||||
TOTAL_CHANGED=$(git diff --name-only HEAD upstream/${{ env.UPSTREAM_BRANCH }} | wc -l)
|
||||
echo "total_changed=$TOTAL_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
## 📋 Upstream Commits Included
|
||||
\`\`\`
|
||||
$UPSTREAM_LOG
|
||||
\`\`\`
|
||||
- name: Commit merge result
|
||||
if: steps.merge.outputs.merge_status == 'conflicts'
|
||||
run: |
|
||||
# Commit the merge with resolved fork-specific conflicts
|
||||
# Remaining conflicts will be visible in the PR for review
|
||||
git commit -m "Merge upstream with partial conflict resolution" -m "Fork-specific files preserved, other conflicts need review" || true
|
||||
|
||||
## 📁 Changed Files
|
||||
<details>
|
||||
<summary>Click to expand</summary>
|
||||
- name: Push sync branch
|
||||
run: |
|
||||
git push origin ${{ steps.branch.outputs.branch_name }} --force
|
||||
|
||||
\`\`\`
|
||||
$CHANGED_FILES
|
||||
\`\`\`
|
||||
</details>
|
||||
|
||||
## ⚠️ Important
|
||||
Before merging, ensure all fork-specific references are preserved:
|
||||
- \`Heretek-AI\` repository references
|
||||
- Fork-specific workflow files
|
||||
- Custom workflow configurations
|
||||
|
||||
## 🔒 Fork-Specific Files Preserved
|
||||
The following files/directories are preserved from the fork and NOT overwritten by upstream:
|
||||
- \`install/\` directory
|
||||
- \`ct/\` directory
|
||||
- \`frontend/\` directory
|
||||
- \`misc/images/\` directory
|
||||
- \`.github/workflows/\` directory
|
||||
- \`.github/runner/\` directory
|
||||
- \`tools/addon/\` directory
|
||||
- \`CHANGELOG.md\`
|
||||
- \`README.md\`
|
||||
|
||||
## ✅ Checklist Before Merge
|
||||
- [ ] Review all changed files for unexpected modifications
|
||||
- [ ] Verify fork-specific files are intact
|
||||
- [ ] Check for any new upstream files that might conflict with fork customizations
|
||||
- [ ] Test any new scripts or features from upstream
|
||||
|
||||
---
|
||||
*This PR was automatically created by the [upstream-sync workflow](https://github.com/Heretek-AI/ProxmoxVE/blob/main/.github/workflows/upstream-sync.yml).*" \
|
||||
--base main \
|
||||
--head "$BRANCH_NAME" || echo "PR may already exist or no changes to PR"
|
||||
|
||||
echo "✅ Pull request created successfully for upstream sync."
|
||||
|
||||
# Switch back to main
|
||||
git checkout main
|
||||
- name: Create Pull Request
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
BRANCH="${{ steps.branch.outputs.branch_name }}"
|
||||
HAS_CONFLICTS="${{ steps.merge.outputs.has_conflicts }}"
|
||||
CONFLICTED_FILES="${{ steps.merge.outputs.conflicted_files }}"
|
||||
|
||||
# Build PR body
|
||||
PR_BODY="## 🔄 Upstream Sync Summary
|
||||
|
||||
This PR syncs the latest changes from upstream [\`${{ env.UPSTREAM_REPO }}\`](https://github.com/${{ env.UPSTREAM_REPO }}) while preserving fork-specific customizations.
|
||||
|
||||
### 📊 Sync Statistics
|
||||
- **Upstream commits:** ${{ needs.check-and-sync.outputs.commit_count }}
|
||||
- **Files changed:** ${{ steps.summary.outputs.total_changed }}
|
||||
- **Merge conflicts:** ${HAS_CONFLICTS:-none}
|
||||
|
||||
### 📋 Upstream Commits (last 30)
|
||||
\`\`\`
|
||||
${{ steps.summary.outputs.upstream_commits }}
|
||||
\`\`\`
|
||||
|
||||
### 📁 Changed Files
|
||||
<details>
|
||||
<summary>Click to view changed files</summary>
|
||||
|
||||
\`\`\`
|
||||
${{ steps.summary.outputs.changed_files }}
|
||||
\`\`\`
|
||||
</details>
|
||||
"
|
||||
|
||||
# Add conflict warning if needed
|
||||
if [ "$HAS_CONFLICTS" = "true" ]; then
|
||||
PR_BODY="$PR_BODY
|
||||
### ⚠️ Merge Conflicts Detected
|
||||
|
||||
The following files have conflicts that need manual review:
|
||||
\`\`\`
|
||||
$CONFLICTED_FILES
|
||||
\`\`\`
|
||||
|
||||
**Note:** Fork-specific files have been automatically preserved using the \`merge=ours\` strategy configured in \`.gitattributes\`.
|
||||
"
|
||||
else
|
||||
PR_BODY="$PR_BODY
|
||||
### ✅ Clean Merge
|
||||
|
||||
No conflicts detected. Fork-specific files were automatically preserved using the \`merge=ours\` strategy.
|
||||
"
|
||||
fi
|
||||
|
||||
# Add preserved files section
|
||||
PR_BODY="$PR_BODY
|
||||
### 🔒 Fork-Specific Files Preserved
|
||||
|
||||
The following files/directories are configured in \`.gitattributes\` to always keep the fork version:
|
||||
- \`install/\` - Custom install scripts
|
||||
- \`ct/\` - Custom container scripts
|
||||
- \`frontend/\` - Custom frontend
|
||||
- \`misc/images/\` - Custom images
|
||||
- \`.github/workflows/\` - Custom CI/CD workflows
|
||||
- \`.github/runner/\` - Custom runner configurations
|
||||
- \`tools/addon/\` - Custom addon tools
|
||||
- \`README.md\` - Fork-specific documentation
|
||||
- \`CHANGELOG.md\` - Fork-specific changelog
|
||||
|
||||
### ✅ Pre-Merge Checklist
|
||||
- [ ] Review all changed files for unexpected modifications
|
||||
- [ ] Verify fork-specific files are intact
|
||||
- [ ] Resolve any remaining merge conflicts
|
||||
- [ ] Test any new scripts or features from upstream
|
||||
|
||||
---
|
||||
*This PR was automatically created by the [upstream-sync workflow](https://github.com/${{ github.repository }}/blob/main/.github/workflows/upstream-sync.yml).*
|
||||
"
|
||||
|
||||
# Create PR title with conflict indicator
|
||||
if [ "$HAS_CONFLICTS" = "true" ]; then
|
||||
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d) ⚠️ CONFLICTS"
|
||||
else
|
||||
TITLE="🔄 Upstream Sync - $(date +%Y-%m-%d)"
|
||||
fi
|
||||
|
||||
# Create the PR (labels are optional - will fail silently if labels don't exist)
|
||||
gh pr create \
|
||||
--title "$TITLE" \
|
||||
--body "$PR_BODY" \
|
||||
--base "${{ env.FORK_BRANCH }}" \
|
||||
--head "$BRANCH" || echo "PR may already exist"
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
# Abort any pending merge to clean up git state
|
||||
git merge --abort 2>/dev/null || true
|
||||
git checkout ${{ env.FORK_BRANCH }}
|
||||
git branch -D ${{ steps.branch.outputs.branch_name }} 2>/dev/null || true
|
||||
|
||||
+2
@@ -420,6 +420,8 @@ Exercise vigilance regarding copycat or coat-tailing sites that seek to exploit
|
||||
|
||||
</details>
|
||||
|
||||
## 2026-03-15
|
||||
|
||||
## 2026-03-14
|
||||
|
||||
## 2026-03-13
|
||||
|
||||
+17
-3
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generated": "2026-03-12T06:37:25Z",
|
||||
"generated": "2026-03-14T06:30:27Z",
|
||||
"versions": [
|
||||
{
|
||||
"slug": "agregarr",
|
||||
@@ -18,9 +18,23 @@
|
||||
{
|
||||
"slug": "llamacpp",
|
||||
"repo": "ggml-org/llama.cpp",
|
||||
"version": "b8281",
|
||||
"version": "b8329",
|
||||
"pinned": false,
|
||||
"date": "2026-03-12T03:40:09Z"
|
||||
"date": "2026-03-14T04:58:36Z"
|
||||
},
|
||||
{
|
||||
"slug": "localai",
|
||||
"repo": "mudler/LocalAI",
|
||||
"version": "v3.12.1",
|
||||
"pinned": false,
|
||||
"date": "2026-02-21T13:49:24Z"
|
||||
},
|
||||
{
|
||||
"slug": "localrecall",
|
||||
"repo": "mudler/LocalRecall",
|
||||
"version": "v0.5.5",
|
||||
"pinned": false,
|
||||
"date": "2026-02-16T22:38:06Z"
|
||||
},
|
||||
{
|
||||
"slug": "maintainerr",
|
||||
|
||||
+4
-2
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"name": "LocalRecall (In Testing - Borked)",
|
||||
"name": "LocalRecall",
|
||||
"slug": "localrecall",
|
||||
"categories": [20],
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"date_created": "2026-03-14",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
|
||||
+5
-3
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"name": "RAGFlow (In Testing - Borked)",
|
||||
"name": "RAGFlow",
|
||||
"slug": "ragflow",
|
||||
"categories": [20],
|
||||
"categories": [
|
||||
20
|
||||
],
|
||||
"date_created": "2026-03-12",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
@@ -9,7 +11,7 @@
|
||||
"interface_port": 80,
|
||||
"documentation": "https://ragflow.io/docs/dev/",
|
||||
"website": "https://ragflow.io/",
|
||||
"logo": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/ragflow.webp",
|
||||
"logo": "https://raw.githubusercontent.com/infiniflow/ragflow/refs/heads/main/web/public/logo.svg",
|
||||
"config_path": "/opt/ragflow/conf/service_conf.yaml",
|
||||
"description": "RAGFlow is an open-source RAG (Retrieval-Augmented Generation) engine with deep document understanding. It provides a streamlined RAG workflow for businesses of any scale, combining LLM capabilities to provide truthful question-answering with well-founded citations from various complex formatted data.",
|
||||
"install_methods": [
|
||||
|
||||
+18
-1
@@ -95,6 +95,9 @@ msg_ok "Installed Dependencies"
|
||||
# DATABASE SETUP (MariaDB)
|
||||
# ==============================================================================
|
||||
|
||||
# Install MariaDB server first
|
||||
setup_mariadb
|
||||
|
||||
MARIADB_DB_NAME="rag_flow"
|
||||
MARIADB_DB_USER="rag_flow"
|
||||
setup_mariadb_db
|
||||
@@ -121,6 +124,10 @@ REDIS_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16)
|
||||
|
||||
$STD apt-get install -y redis-server
|
||||
|
||||
# Stop Redis if running to apply new configuration
|
||||
systemctl stop redis-server 2>/dev/null || true
|
||||
|
||||
# Write Redis configuration with password
|
||||
cat <<EOF >/etc/redis/redis.conf
|
||||
bind 127.0.0.1
|
||||
port 6379
|
||||
@@ -136,7 +143,17 @@ EOF
|
||||
mkdir -p /var/log/redis
|
||||
chown -R redis:redis /var/log/redis /var/lib/redis
|
||||
|
||||
systemctl enable -q --now redis-server
|
||||
# Enable and start Redis with new configuration
|
||||
systemctl enable -q redis-server
|
||||
systemctl start redis-server
|
||||
|
||||
# Wait for Redis to be ready and verify password works
|
||||
for i in {1..30}; do
|
||||
if redis-cli -a "${REDIS_PASS}" ping 2>/dev/null | grep -q "PONG"; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
msg_ok "Redis Installed"
|
||||
|
||||
# ==============================================================================
|
||||
|
||||
+40
-103
@@ -29,111 +29,48 @@ function update_script() {
|
||||
exit
|
||||
fi
|
||||
|
||||
cd /opt/ragflow || exit
|
||||
LOCAL_VERSION=$(git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||
REMOTE_VERSION=$(git ls-remote origin HEAD 2>/dev/null | awk '{print $1}' || echo "unknown")
|
||||
if check_for_gh_release "ragflow" "infiniflow/ragflow"; then
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop ragflow-task-executor || true
|
||||
systemctl stop ragflow-server || true
|
||||
msg_ok "Stopped Services"
|
||||
|
||||
if [[ "$LOCAL_VERSION" == "$REMOTE_VERSION" ]] || [[ "$REMOTE_VERSION" == "unknown" ]]; then
|
||||
if [[ "$REMOTE_VERSION" == "unknown" ]]; then
|
||||
msg_info "Unable to check for updates. Checking local version..."
|
||||
CURRENT_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "unknown")
|
||||
msg_info "Current version: ${CURRENT_TAG}"
|
||||
fi
|
||||
msg_info "No update required. ${APP} is already up to date."
|
||||
exit 0
|
||||
msg_info "Backing up Data"
|
||||
cp -r /opt/ragflow/conf /opt/ragflow_conf_backup
|
||||
cp -r /opt/ragflow/data /opt/ragflow_data_backup 2>/dev/null || true
|
||||
cp /opt/ragflow/.env /opt/ragflow_env_backup 2>/dev/null || true
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
CLEAN_INSTALL=1 fetch_and_deploy_gh_release "ragflow" "infiniflow/ragflow" "tarball" "latest" "/opt/ragflow"
|
||||
|
||||
msg_info "Reinstalling Python Dependencies"
|
||||
cd /opt/ragflow || exit
|
||||
export UV_SYSTEM_PYTHON=1
|
||||
$STD /usr/local/bin/uv sync --python 3.12 --frozen --index-strategy unsafe-best-match
|
||||
$STD /usr/local/bin/uv run download_deps.py
|
||||
msg_ok "Reinstalled Python Dependencies"
|
||||
|
||||
msg_info "Rebuilding Frontend"
|
||||
cd /opt/ragflow/web || exit
|
||||
$STD npm install
|
||||
$STD npm run build
|
||||
cp -r /opt/ragflow/web/dist/* /var/www/ragflow/
|
||||
msg_ok "Rebuilt Frontend"
|
||||
|
||||
msg_info "Restoring Configuration"
|
||||
cp -r /opt/ragflow_conf_backup/. /opt/ragflow/conf/
|
||||
cp -r /opt/ragflow_data_backup/. /opt/ragflow/data/ 2>/dev/null || true
|
||||
cp /opt/ragflow_env_backup /opt/ragflow/.env 2>/dev/null || true
|
||||
rm -rf /opt/ragflow_conf_backup /opt/ragflow_data_backup /opt/ragflow_env_backup
|
||||
msg_ok "Restored Configuration"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start ragflow-server
|
||||
sleep 5
|
||||
systemctl start ragflow-task-executor
|
||||
msg_ok "Started Services"
|
||||
msg_ok "Updated successfully!"
|
||||
fi
|
||||
|
||||
msg_info "Stopping Services"
|
||||
systemctl stop ragflow-task-executor || true
|
||||
systemctl stop ragflow-server || true
|
||||
msg_ok "Stopped Services"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp -r /opt/ragflow/conf /opt/ragflow_conf_backup
|
||||
cp -r /opt/ragflow/data /opt/ragflow_data_backup 2>/dev/null || true
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
msg_info "Updating ${APP}"
|
||||
$STD git fetch origin
|
||||
$STD git reset --hard origin/main
|
||||
$STD git describe --tags --abbrev=0 > /opt/ragflow/version.txt 2>/dev/null || true
|
||||
msg_ok "Updated ${APP}"
|
||||
|
||||
# Fix: Replace gitee.com URLs with GitHub URLs
|
||||
# RAGFlow's pyproject.toml and uv.lock may reference gitee.com which requires authentication
|
||||
# We replace with GitHub mirror which is publicly accessible
|
||||
if grep -q "gitee.com/infiniflow/graspologic" pyproject.toml 2>/dev/null; then
|
||||
msg_info "Replacing gitee.com URLs in pyproject.toml with GitHub"
|
||||
sed -i 's|gitee.com/infiniflow/graspologic|github.com/infiniflow/graspologic|g' pyproject.toml
|
||||
msg_ok "Fixed graspologic URLs in pyproject.toml"
|
||||
fi
|
||||
if grep -q "gitee.com/infiniflow/graspologic" uv.lock 2>/dev/null; then
|
||||
msg_info "Replacing gitee.com URLs in uv.lock with GitHub"
|
||||
sed -i 's|gitee.com/infiniflow/graspologic|github.com/infiniflow/graspologic|g' uv.lock
|
||||
msg_ok "Fixed graspologic URLs in lock file"
|
||||
fi
|
||||
|
||||
# Fix: Replace Chinese PyPI mirror with standard PyPI
|
||||
# https://github.com/astral-sh/uv/issues/10462
|
||||
# uv records index url into uv.lock but doesn't failover among multiple indexes
|
||||
# RAGFlow uses pypi.tuna.tsinghua.edu.cn which may not have all packages
|
||||
if grep -q "pypi.tuna.tsinghua.edu.cn" pyproject.toml 2>/dev/null; then
|
||||
msg_info "Replacing Chinese PyPI mirror with standard PyPI"
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn|pypi.org|g' pyproject.toml
|
||||
msg_ok "Fixed PyPI index URL in pyproject.toml"
|
||||
fi
|
||||
if grep -q "pypi.tuna.tsinghua.edu.cn" uv.lock 2>/dev/null; then
|
||||
msg_info "Replacing Chinese PyPI mirror in uv.lock with standard PyPI"
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn|pypi.org|g' uv.lock
|
||||
msg_ok "Fixed PyPI index URL in lock file"
|
||||
fi
|
||||
|
||||
# Remove the ragflow_sdk package from pyproject.toml since we only need the
|
||||
# server components. The SDK is a client library for connecting to RAGFlow
|
||||
# from external applications, which is not needed for server-only installations.
|
||||
if grep -q "sdk.python.ragflow_sdk" pyproject.toml 2>/dev/null; then
|
||||
msg_info "Excluding SDK Package from Installation"
|
||||
sed -i '/sdk.python.ragflow_sdk/d' pyproject.toml
|
||||
msg_ok "Excluded ragflow_sdk from installation"
|
||||
fi
|
||||
|
||||
# Fix: Pin MCP version to avoid pyjwt conflict with zhipuai
|
||||
# zhipuai requires pyjwt>=2.8.0,<2.9.0 but mcp>=1.23.0 requires pyjwt>=2.10.1
|
||||
# These constraints are incompatible. However, mcp==1.19.0 doesn't require pyjwt>=2.10.1
|
||||
# By pinning MCP to 1.19.0 (matching upstream's uv.lock), we preserve ZhipuAI functionality
|
||||
# See: https://github.com/MetaGLM/zhipuai-sdk-python-v4/issues/103
|
||||
if grep -q 'mcp>=' pyproject.toml 2>/dev/null; then
|
||||
msg_info "Pinning MCP version to 1.19.0 to preserve ZhipuAI compatibility"
|
||||
sed -i 's/mcp>=1.23.0/mcp==1.19.0/' pyproject.toml
|
||||
msg_ok "Pinned MCP to version 1.19.0"
|
||||
fi
|
||||
|
||||
# Note: We do NOT remove agentrun-sdk from pyproject.toml
|
||||
# These are resolved correctly in the upstream uv.lock file
|
||||
# Removing them would require regenerating the lock file, which causes issues
|
||||
|
||||
# Install Python dependencies using the upstream lock file
|
||||
# The --frozen flag tells uv to use the lock file as-is without re-resolution
|
||||
# This is the official RAGFlow installation method from their documentation
|
||||
# Reference: https://ragflow.io/docs/launch_ragflow_from_source
|
||||
msg_info "Reinstalling Python Dependencies"
|
||||
cd /opt/ragflow || exit
|
||||
export UV_SYSTEM_PYTHON=1
|
||||
$STD /root/.local/bin/uv sync --python 3.12 --frozen --index-strategy unsafe-best-match
|
||||
$STD /root/.local/bin/uv run download_deps.py
|
||||
msg_ok "Reinstalled Python Dependencies"
|
||||
|
||||
msg_info "Restoring Configuration"
|
||||
cp -r /opt/ragflow_conf_backup/. /opt/ragflow/conf/
|
||||
rm -rf /opt/ragflow_conf_backup /opt/ragflow_data_backup
|
||||
msg_ok "Restored Configuration"
|
||||
|
||||
msg_info "Starting Services"
|
||||
systemctl start ragflow-server
|
||||
systemctl start ragflow-task-executor
|
||||
msg_ok "Started Services"
|
||||
|
||||
msg_ok "Updated successfully!"
|
||||
exit
|
||||
}
|
||||
|
||||
|
||||
+13
-16
@@ -88,20 +88,6 @@ function update_script() {
|
||||
msg_ok "Fixed PyPI index URL in lock file"
|
||||
fi
|
||||
|
||||
# Fix: Ensure Python version constraint matches upstream
|
||||
# RAGFlow upstream uses requires-python = ">=3.12,<3.15"
|
||||
# infinity-sdk requires Python >=3.11,<3.14
|
||||
# The intersection is >=3.12,<3.14, but we keep upstream's constraint
|
||||
# and rely on the lock file for correct resolution
|
||||
if grep -q 'requires-python' pyproject.toml 2>/dev/null; then
|
||||
# Only update if it doesn't match upstream
|
||||
if ! grep -q 'requires-python = ">=3.12,<3.15"' pyproject.toml 2>/dev/null; then
|
||||
msg_info "Updating Python version constraint to match upstream"
|
||||
sed -i 's/requires-python\s*=.*/requires-python = ">=3.12,<3.15"/' pyproject.toml
|
||||
msg_ok "Updated Python version constraint"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the ragflow_sdk package from pyproject.toml since we only need the
|
||||
# server components. The SDK is a client library for connecting to RAGFlow
|
||||
# from external applications, which is not needed for server-only installations.
|
||||
@@ -111,7 +97,18 @@ function update_script() {
|
||||
msg_ok "Excluded ragflow_sdk from installation"
|
||||
fi
|
||||
|
||||
# Note: We do NOT remove zhipuai or agentrun-sdk from pyproject.toml
|
||||
# Fix: Pin MCP version to avoid pyjwt conflict with zhipuai
|
||||
# zhipuai requires pyjwt>=2.8.0,<2.9.0 but mcp>=1.23.0 requires pyjwt>=2.10.1
|
||||
# These constraints are incompatible. However, mcp==1.19.0 doesn't require pyjwt>=2.10.1
|
||||
# By pinning MCP to 1.19.0 (matching upstream's uv.lock), we preserve ZhipuAI functionality
|
||||
# See: https://github.com/MetaGLM/zhipuai-sdk-python-v4/issues/103
|
||||
if grep -q 'mcp>=' pyproject.toml 2>/dev/null; then
|
||||
msg_info "Pinning MCP version to 1.19.0 to preserve ZhipuAI compatibility"
|
||||
sed -i 's/mcp>=1.23.0/mcp==1.19.0/' pyproject.toml
|
||||
msg_ok "Pinned MCP to version 1.19.0"
|
||||
fi
|
||||
|
||||
# Note: We do NOT remove agentrun-sdk from pyproject.toml
|
||||
# These are resolved correctly in the upstream uv.lock file
|
||||
# Removing them would require regenerating the lock file, which causes issues
|
||||
|
||||
@@ -122,7 +119,7 @@ function update_script() {
|
||||
msg_info "Reinstalling Python Dependencies"
|
||||
cd /opt/ragflow || exit
|
||||
export UV_SYSTEM_PYTHON=1
|
||||
$STD /root/.local/bin/uv sync --python 3.12 --frozen
|
||||
$STD /root/.local/bin/uv sync --python 3.12 --frozen --index-strategy unsafe-best-match
|
||||
$STD /root/.local/bin/uv run download_deps.py
|
||||
msg_ok "Reinstalled Python Dependencies"
|
||||
|
||||
|
||||
+47
-172
@@ -18,9 +18,10 @@ update_os
|
||||
# This script installs RAGFlow with all dependencies directly on the LXC container:
|
||||
# - MariaDB (MySQL-compatible, metadata storage)
|
||||
# - Elasticsearch 8.11 (document/vector search)
|
||||
# - Redis/Valkey (caching)
|
||||
# - Redis (caching)
|
||||
# - MinIO (object storage)
|
||||
# - Python 3.12 (backend)
|
||||
# - Node.js 22 (frontend build)
|
||||
# - Nginx (frontend reverse proxy)
|
||||
# ==============================================================================
|
||||
|
||||
@@ -91,56 +92,35 @@ $STD apt-get install -y \
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
# ==============================================================================
|
||||
# MARIADB INSTALLATION (MySQL-compatible)
|
||||
# DATABASE SETUP (MariaDB)
|
||||
# ==============================================================================
|
||||
# Using MariaDB instead of MySQL to avoid expired GPG key issues on Debian 13+
|
||||
# MariaDB is fully MySQL-compatible and works with RAGFlow
|
||||
|
||||
msg_info "Installing MariaDB (MySQL-compatible)"
|
||||
$STD apt-get install -y mariadb-server mariadb-client
|
||||
MARIADB_DB_NAME="rag_flow"
|
||||
MARIADB_DB_USER="rag_flow"
|
||||
setup_mariadb_db
|
||||
|
||||
# Wait for MariaDB to be ready
|
||||
for i in {1..30}; do
|
||||
if mysqladmin ping -h localhost --silent 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Generate MariaDB credentials
|
||||
MYSQL_RAGFLOW_USER="rag_flow"
|
||||
MYSQL_RAGFLOW_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16)
|
||||
MYSQL_RAGFLOW_DB="rag_flow"
|
||||
|
||||
msg_info "Creating MariaDB Database and User"
|
||||
$STD mysql -u root -e "CREATE DATABASE \`${MYSQL_RAGFLOW_DB}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||
$STD mysql -u root -e "CREATE USER '${MYSQL_RAGFLOW_USER}'@'localhost' IDENTIFIED BY '${MYSQL_RAGFLOW_PASS}';"
|
||||
$STD mysql -u root -e "GRANT ALL PRIVILEGES ON \`${MYSQL_RAGFLOW_DB}\`.* TO '${MYSQL_RAGFLOW_USER}'@'localhost';"
|
||||
$STD mysql -u root -e "FLUSH PRIVILEGES;"
|
||||
|
||||
# Increase max_allowed_packet for large documents
|
||||
# Configure MariaDB for RAGFlow
|
||||
msg_info "Configuring MariaDB for RAGFlow"
|
||||
$STD mysql -u root -e "SET GLOBAL max_allowed_packet=1073741824;"
|
||||
cat <<EOF >/etc/mysql/mariadb.conf.d/ragflow.cnf
|
||||
[mysqld]
|
||||
[mariadb]
|
||||
max_allowed_packet=1073741824
|
||||
max_connections=900
|
||||
character-set-server=utf8mb4
|
||||
collation-server=utf8mb4_unicode_ci
|
||||
EOF
|
||||
systemctl restart mariadb
|
||||
msg_ok "MariaDB Configured"
|
||||
msg_ok "Configured MariaDB"
|
||||
|
||||
# ==============================================================================
|
||||
# REDIS INSTALLATION
|
||||
# ==============================================================================
|
||||
# Using Redis from Debian repos instead of Valkey to avoid external repo issues
|
||||
|
||||
msg_info "Installing Redis"
|
||||
REDIS_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16)
|
||||
|
||||
$STD apt-get install -y redis-server
|
||||
|
||||
# Configure Redis
|
||||
cat <<EOF >/etc/redis/redis.conf
|
||||
bind 127.0.0.1
|
||||
port 6379
|
||||
@@ -229,19 +209,16 @@ msg_ok "Elasticsearch Installed"
|
||||
# ==============================================================================
|
||||
|
||||
msg_info "Installing MinIO"
|
||||
MINIO_USER="rag_flow"
|
||||
MINIO_PASS=$(openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c16)
|
||||
|
||||
# Download MinIO binary
|
||||
curl -fsSL https://dl.min.io/server/minio/release/linux-amd64/minio -o /usr/local/bin/minio
|
||||
chmod +x /usr/local/bin/minio
|
||||
|
||||
# Create MinIO user and directories
|
||||
useradd -r -s /bin/false minio-user 2>/dev/null || true
|
||||
# Create MinIO directories
|
||||
mkdir -p /var/lib/minio/data
|
||||
chown -R minio-user:minio-user /var/lib/minio
|
||||
|
||||
# Create MinIO service
|
||||
# Create MinIO service (run as root in LXC)
|
||||
cat <<EOF >/etc/systemd/system/minio.service
|
||||
[Unit]
|
||||
Description=MinIO Object Storage
|
||||
@@ -250,9 +227,7 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
User=minio-user
|
||||
Group=minio-user
|
||||
Environment="MINIO_ROOT_USER=${MINIO_USER}"
|
||||
Environment="MINIO_ROOT_USER=rag_flow"
|
||||
Environment="MINIO_ROOT_PASSWORD=${MINIO_PASS}"
|
||||
Environment="MINIO_BROWSER=on"
|
||||
ExecStart=/usr/local/bin/minio server /var/lib/minio/data --console-address ":9001"
|
||||
@@ -264,7 +239,6 @@ LimitNOFILE=65535
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable -q --now minio
|
||||
|
||||
# Wait for MinIO to be ready
|
||||
@@ -274,98 +248,26 @@ for i in {1..30}; do
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# Create bucket for RAGFlow
|
||||
$STD curl -s -X PUT "http://localhost:9000/minio/health/live" || true
|
||||
msg_ok "MinIO Installed"
|
||||
|
||||
# ==============================================================================
|
||||
# RAGFLOW INSTALLATION
|
||||
# ==============================================================================
|
||||
|
||||
msg_info "Downloading RAGFlow"
|
||||
fetch_and_deploy_gh_release "ragflow" "infiniflow/ragflow" "tarball" "v0.24.0" "/opt/ragflow"
|
||||
msg_ok "Downloaded RAGFlow"
|
||||
|
||||
# ==============================================================================
|
||||
# PYTHON ENVIRONMENT
|
||||
# ==============================================================================
|
||||
|
||||
PYTHON_VERSION="3.12" setup_uv
|
||||
|
||||
# Install jemalloc for memory management
|
||||
$STD apt-get install -y libjemalloc-dev
|
||||
|
||||
# Clone RAGFlow repository
|
||||
msg_info "Cloning RAGFlow Repository"
|
||||
cd /opt || exit
|
||||
$STD git clone --depth 1 https://github.com/infiniflow/ragflow.git ragflow
|
||||
cd /opt/ragflow || exit
|
||||
git describe --tags --abbrev=0 > /opt/ragflow/version.txt 2>/dev/null || echo "v0.24.0" > /opt/ragflow/version.txt
|
||||
msg_ok "Cloned RAGFlow Repository"
|
||||
|
||||
# Fix: Replace gitee.com URLs with GitHub URLs
|
||||
# RAGFlow's pyproject.toml and uv.lock may reference gitee.com which requires authentication
|
||||
# We replace with GitHub mirror which is publicly accessible
|
||||
if grep -q "gitee.com/infiniflow/graspologic" pyproject.toml 2>/dev/null; then
|
||||
msg_info "Replacing gitee.com URLs in pyproject.toml with GitHub"
|
||||
sed -i 's|gitee.com/infiniflow/graspologic|github.com/infiniflow/graspologic|g' pyproject.toml
|
||||
msg_ok "Fixed graspologic URLs in pyproject.toml"
|
||||
fi
|
||||
if grep -q "gitee.com/infiniflow/graspologic" uv.lock 2>/dev/null; then
|
||||
msg_info "Replacing gitee.com URLs in uv.lock with GitHub"
|
||||
sed -i 's|gitee.com/infiniflow/graspologic|github.com/infiniflow/graspologic|g' uv.lock
|
||||
msg_ok "Fixed graspologic URLs in lock file"
|
||||
fi
|
||||
|
||||
# Fix: Replace Chinese PyPI mirror with standard PyPI
|
||||
# https://github.com/astral-sh/uv/issues/10462
|
||||
# uv records index url into uv.lock but doesn't failover among multiple indexes
|
||||
# RAGFlow uses pypi.tuna.tsinghua.edu.cn which may not have all packages
|
||||
if grep -q "pypi.tuna.tsinghua.edu.cn" pyproject.toml 2>/dev/null; then
|
||||
msg_info "Replacing Chinese PyPI mirror with standard PyPI"
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn|pypi.org|g' pyproject.toml
|
||||
msg_ok "Fixed PyPI index URL in pyproject.toml"
|
||||
fi
|
||||
if grep -q "pypi.tuna.tsinghua.edu.cn" uv.lock 2>/dev/null; then
|
||||
msg_info "Replacing Chinese PyPI mirror in uv.lock with standard PyPI"
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn|pypi.org|g' uv.lock
|
||||
msg_ok "Fixed PyPI index URL in lock file"
|
||||
fi
|
||||
|
||||
# Fix: Ensure Python version constraint matches upstream
|
||||
# RAGFlow upstream uses requires-python = ">=3.12,<3.15"
|
||||
# infinity-sdk requires Python >=3.11,<3.14
|
||||
# The intersection is >=3.12,<3.14, but we keep upstream's constraint
|
||||
# and rely on the lock file for correct resolution
|
||||
if grep -q 'requires-python' pyproject.toml 2>/dev/null; then
|
||||
# Only update if it doesn't match upstream
|
||||
if ! grep -q 'requires-python = ">=3.12,<3.15"' pyproject.toml 2>/dev/null; then
|
||||
msg_info "Updating Python version constraint to match upstream"
|
||||
sed -i 's/requires-python\s*=.*/requires-python = ">=3.12,<3.15"/' pyproject.toml
|
||||
msg_ok "Updated Python version constraint"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# SDK EXCLUSION
|
||||
# ==============================================================================
|
||||
# Remove the ragflow_sdk package from pyproject.toml since we only need the
|
||||
# server components. The SDK is a client library for connecting to RAGFlow
|
||||
# from external applications, which is not needed for server-only installations.
|
||||
|
||||
msg_info "Excluding SDK Package from Installation"
|
||||
if grep -q "sdk.python.ragflow_sdk" pyproject.toml 2>/dev/null; then
|
||||
sed -i '/sdk.python.ragflow_sdk/d' pyproject.toml
|
||||
msg_ok "Excluded ragflow_sdk from installation"
|
||||
else
|
||||
msg_ok "SDK package not found in pyproject.toml (already excluded or not present)"
|
||||
fi
|
||||
|
||||
# Note: We do NOT remove zhipuai or agentrun-sdk from pyproject.toml
|
||||
# These are resolved correctly in the upstream uv.lock file
|
||||
# Removing them would require regenerating the lock file, which causes issues
|
||||
|
||||
# Install Python dependencies using the upstream lock file
|
||||
# The --frozen flag tells uv to use the lock file as-is without re-resolution
|
||||
# This is the official RAGFlow installation method from their documentation
|
||||
# Reference: https://ragflow.io/docs/launch_ragflow_from_source
|
||||
msg_info "Installing Python Dependencies"
|
||||
cd /opt/ragflow || exit
|
||||
export UV_SYSTEM_PYTHON=1
|
||||
$STD /usr/local/bin/uv sync --python 3.12 --frozen
|
||||
$STD /usr/local/bin/uv sync --python 3.12 --frozen --index-strategy unsafe-best-match
|
||||
$STD /usr/local/bin/uv run download_deps.py
|
||||
msg_ok "Installed Python Dependencies"
|
||||
|
||||
@@ -375,10 +277,8 @@ msg_ok "Installed Python Dependencies"
|
||||
|
||||
msg_info "Creating RAGFlow Configuration"
|
||||
|
||||
# Create configuration directory
|
||||
mkdir -p /opt/ragflow/conf /opt/ragflow/data /opt/ragflow/logs
|
||||
|
||||
# Create service configuration
|
||||
cat <<EOF >/opt/ragflow/conf/service_conf.yaml
|
||||
ragflow:
|
||||
host: 0.0.0.0
|
||||
@@ -387,16 +287,16 @@ admin:
|
||||
host: 0.0.0.0
|
||||
http_port: 9381
|
||||
mysql:
|
||||
name: '${MYSQL_RAGFLOW_DB}'
|
||||
user: '${MYSQL_RAGFLOW_USER}'
|
||||
password: '${MYSQL_RAGFLOW_PASS}'
|
||||
name: 'rag_flow'
|
||||
user: 'rag_flow'
|
||||
password: '${MARIADB_DB_PASS}'
|
||||
host: 'localhost'
|
||||
port: 3306
|
||||
max_connections: 900
|
||||
stale_timeout: 300
|
||||
max_allowed_packet: 1073741824
|
||||
minio:
|
||||
user: '${MINIO_USER}'
|
||||
user: 'rag_flow'
|
||||
password: '${MINIO_PASS}'
|
||||
host: 'localhost:9000'
|
||||
bucket: 'ragflow'
|
||||
@@ -417,7 +317,6 @@ user_default_llm:
|
||||
base_url: 'http://localhost:6380'
|
||||
EOF
|
||||
|
||||
# Create environment file
|
||||
cat <<EOF >/opt/ragflow/.env
|
||||
DOC_ENGINE=elasticsearch
|
||||
DEVICE=cpu
|
||||
@@ -426,13 +325,13 @@ STACK_VERSION=8.11.3
|
||||
ES_HOST=localhost
|
||||
ES_PORT=9200
|
||||
ELASTIC_PASSWORD=${ES_PASS}
|
||||
MYSQL_PASSWORD=${MYSQL_RAGFLOW_PASS}
|
||||
MYSQL_PASSWORD=${MARIADB_DB_PASS}
|
||||
MYSQL_HOST=localhost
|
||||
MYSQL_DBNAME=${MYSQL_RAGFLOW_DB}
|
||||
MYSQL_DBNAME=rag_flow
|
||||
MYSQL_PORT=3306
|
||||
MINIO_HOST=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_USER=${MINIO_USER}
|
||||
MINIO_USER=rag_flow
|
||||
MINIO_PASSWORD=${MINIO_PASS}
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
@@ -456,7 +355,6 @@ msg_ok "Created RAGFlow Configuration"
|
||||
|
||||
msg_info "Creating Systemd Services"
|
||||
|
||||
# RAGFlow Backend Server
|
||||
cat <<EOF >/etc/systemd/system/ragflow-server.service
|
||||
[Unit]
|
||||
Description=RAGFlow Backend Server
|
||||
@@ -465,7 +363,6 @@ Requires=mariadb.service elasticsearch.service redis-server.service minio.servic
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/ragflow
|
||||
Environment=PYTHONPATH=/opt/ragflow
|
||||
Environment=LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/
|
||||
@@ -481,7 +378,6 @@ LimitNOFILE=65535
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
# RAGFlow Task Executor
|
||||
cat <<EOF >/etc/systemd/system/ragflow-task-executor.service
|
||||
[Unit]
|
||||
Description=RAGFlow Task Executor
|
||||
@@ -490,7 +386,6 @@ Requires=mariadb.service elasticsearch.service redis-server.service minio.servic
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=root
|
||||
WorkingDirectory=/opt/ragflow
|
||||
Environment=PYTHONPATH=/opt/ragflow
|
||||
Environment=LD_LIBRARY_PATH=/usr/lib/x86_64-linux-gnu/
|
||||
@@ -505,7 +400,6 @@ LimitNOFILE=65535
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
msg_ok "Created Systemd Services"
|
||||
|
||||
# ==============================================================================
|
||||
@@ -515,17 +409,16 @@ msg_ok "Created Systemd Services"
|
||||
msg_info "Setting up Nginx Frontend"
|
||||
$STD apt-get install -y nginx
|
||||
|
||||
# Build RAGFlow frontend from source (no Docker)
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
|
||||
msg_info "Building RAGFlow Frontend"
|
||||
mkdir -p /var/www/ragflow
|
||||
NODE_VERSION="22" setup_nodejs
|
||||
cd /opt/ragflow/web || exit
|
||||
$STD npm install
|
||||
$STD npm run build
|
||||
cp -r /opt/ragflow/web/dist/* /var/www/ragflow/
|
||||
msg_ok "Built RAGFlow Frontend"
|
||||
|
||||
# Configure Nginx
|
||||
cat <<EOF >/etc/nginx/sites-available/ragflow.conf
|
||||
server {
|
||||
listen 80 default_server;
|
||||
@@ -585,36 +478,6 @@ $STD ln -sf /etc/nginx/sites-available/ragflow.conf /etc/nginx/sites-enabled/
|
||||
$STD systemctl enable -q --now nginx
|
||||
msg_ok "Nginx Frontend Configured"
|
||||
|
||||
# ==============================================================================
|
||||
# SAVE CREDENTIALS
|
||||
# ==============================================================================
|
||||
|
||||
msg_info "Saving Credentials"
|
||||
cat <<EOF >~/ragflow.creds
|
||||
RAGFlow Credentials
|
||||
===================
|
||||
MariaDB Database: ${MYSQL_RAGFLOW_DB}
|
||||
MariaDB User: ${MYSQL_RAGFLOW_USER}
|
||||
MariaDB Password: ${MYSQL_RAGFLOW_PASS}
|
||||
|
||||
Elasticsearch User: elastic
|
||||
Elasticsearch Password: ${ES_PASS}
|
||||
|
||||
Redis Password: ${REDIS_PASS}
|
||||
|
||||
MinIO User: ${MINIO_USER}
|
||||
MinIO Password: ${MINIO_PASS}
|
||||
|
||||
Web Interface: http://<IP>:80
|
||||
API Endpoint: http://<IP>:9380
|
||||
MinIO Console: http://<IP>:9001
|
||||
|
||||
Configuration: /opt/ragflow/conf/service_conf.yaml
|
||||
Environment: /opt/ragflow/.env
|
||||
EOF
|
||||
chmod 600 ~/ragflow.creds
|
||||
msg_ok "Saved Credentials"
|
||||
|
||||
# ==============================================================================
|
||||
# START SERVICES
|
||||
# ==============================================================================
|
||||
@@ -634,12 +497,24 @@ customize
|
||||
cleanup_lxc
|
||||
|
||||
msg_ok "Completed Successfully!\n"
|
||||
LOCAL_IP=$(hostname -I | awk '{print $1}')
|
||||
echo -e "${CREATING}${GN}RAGFlow has been successfully installed!${CL}"
|
||||
echo -e "${INFO}${YW} Access it using the following URL:${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${IP}:80${CL}"
|
||||
echo -e "${INFO}${YW} API endpoint: http://${IP}:9380${CL}"
|
||||
echo -e "${INFO}${YW} MinIO Console: http://${IP}:9001${CL}"
|
||||
echo -e "${INFO}${YW} Credentials saved to: ~/ragflow.creds${CL}"
|
||||
echo -e "${TAB}${GATEWAY}${BGN}http://${LOCAL_IP}:80${CL}"
|
||||
echo -e "${INFO}${YW} API endpoint: http://${LOCAL_IP}:9380${CL}"
|
||||
echo -e "${INFO}${YW} MinIO Console: http://${LOCAL_IP}:9001${CL}"
|
||||
echo -e ""
|
||||
echo -e "${INFO}${YW} Credentials:${CL}"
|
||||
echo -e "${TAB}- MariaDB User: rag_flow"
|
||||
echo -e "${TAB}- MariaDB Password: ${MARIADB_DB_PASS}"
|
||||
echo -e "${TAB}- Elasticsearch Password: ${ES_PASS}"
|
||||
echo -e "${TAB}- Redis Password: ${REDIS_PASS}"
|
||||
echo -e "${TAB}- MinIO User: rag_flow"
|
||||
echo -e "${TAB}- MinIO Password: ${MINIO_PASS}"
|
||||
echo -e ""
|
||||
echo -e "${INFO}${YW} Configuration files:${CL}"
|
||||
echo -e "${TAB}- /opt/ragflow/conf/service_conf.yaml"
|
||||
echo -e "${TAB}- /opt/ragflow/.env"
|
||||
echo -e ""
|
||||
echo -e "${INFO}${YW} Important Notes:${CL}"
|
||||
echo -e "${TAB}- Configure your LLM API key in the web interface"
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
__ __ ____
|
||||
/ /___ _________ _/ /_______ _________ _/ / /
|
||||
/ / __ \/ ___/ __ `/ / ___/ _ \/ ___/ __ `/ / /
|
||||
/ / /_/ / /__/ /_/ / / / / __/ /__/ /_/ / / /
|
||||
/_/\____/\___/\__,_/_/_/ \___/\___/\__,_/_/_/
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@ msg_info "Installing Dependencies"
|
||||
$STD apt-get install -y curl ca-certificates
|
||||
msg_ok "Installed Dependencies"
|
||||
|
||||
GO_VERSION="1.24" setup_go
|
||||
setup_go
|
||||
|
||||
fetch_and_deploy_gh_release "localrecall" "mudler/LocalRecall" "tarball" "latest" "/opt/localrecall"
|
||||
|
||||
|
||||
+19
-2
@@ -311,15 +311,17 @@ if grep -q "gitee.com/infiniflow/graspologic" uv.lock 2>/dev/null; then
|
||||
fi
|
||||
|
||||
# Fix: Replace Chinese PyPI mirror with standard PyPI
|
||||
# https://github.com/astral-sh/uv/issues/10462
|
||||
# uv records index url into uv.lock but doesn't failover among multiple indexes
|
||||
# RAGFlow uses pypi.tuna.tsinghua.edu.cn which may not have all packages
|
||||
if grep -q "pypi.tuna.tsinghua.edu.cn" pyproject.toml 2>/dev/null; then
|
||||
msg_info "Replacing Chinese PyPI mirror with standard PyPI"
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn/simple|pypi.org/simple|g' pyproject.toml
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn|pypi.org|g' pyproject.toml
|
||||
msg_ok "Fixed PyPI index URL in pyproject.toml"
|
||||
fi
|
||||
if grep -q "pypi.tuna.tsinghua.edu.cn" uv.lock 2>/dev/null; then
|
||||
msg_info "Replacing Chinese PyPI mirror in uv.lock with standard PyPI"
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn/simple|pypi.org/simple|g' uv.lock
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn|pypi.org|g' uv.lock
|
||||
msg_ok "Fixed PyPI index URL in lock file"
|
||||
fi
|
||||
|
||||
@@ -337,6 +339,21 @@ if grep -q 'requires-python' pyproject.toml 2>/dev/null; then
|
||||
fi
|
||||
fi
|
||||
|
||||
# ==============================================================================
|
||||
# SDK EXCLUSION
|
||||
# ==============================================================================
|
||||
# Remove the ragflow_sdk package from pyproject.toml since we only need the
|
||||
# server components. The SDK is a client library for connecting to RAGFlow
|
||||
# from external applications, which is not needed for server-only installations.
|
||||
|
||||
msg_info "Excluding SDK Package from Installation"
|
||||
if grep -q "sdk.python.ragflow_sdk" pyproject.toml 2>/dev/null; then
|
||||
sed -i '/sdk.python.ragflow_sdk/d' pyproject.toml
|
||||
msg_ok "Excluded ragflow_sdk from installation"
|
||||
else
|
||||
msg_ok "SDK package not found in pyproject.toml (already excluded or not present)"
|
||||
fi
|
||||
|
||||
# Note: We do NOT remove zhipuai or agentrun-sdk from pyproject.toml
|
||||
# These are resolved correctly in the upstream uv.lock file
|
||||
# Removing them would require regenerating the lock file, which causes issues
|
||||
|
||||
+13
-2
@@ -74,15 +74,17 @@ function update_script() {
|
||||
fi
|
||||
|
||||
# Fix: Replace Chinese PyPI mirror with standard PyPI
|
||||
# https://github.com/astral-sh/uv/issues/10462
|
||||
# uv records index url into uv.lock but doesn't failover among multiple indexes
|
||||
# RAGFlow uses pypi.tuna.tsinghua.edu.cn which may not have all packages
|
||||
if grep -q "pypi.tuna.tsinghua.edu.cn" pyproject.toml 2>/dev/null; then
|
||||
msg_info "Replacing Chinese PyPI mirror with standard PyPI"
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn/simple|pypi.org/simple|g' pyproject.toml
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn|pypi.org|g' pyproject.toml
|
||||
msg_ok "Fixed PyPI index URL in pyproject.toml"
|
||||
fi
|
||||
if grep -q "pypi.tuna.tsinghua.edu.cn" uv.lock 2>/dev/null; then
|
||||
msg_info "Replacing Chinese PyPI mirror in uv.lock with standard PyPI"
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn/simple|pypi.org/simple|g' uv.lock
|
||||
sed -i 's|pypi.tuna.tsinghua.edu.cn|pypi.org|g' uv.lock
|
||||
msg_ok "Fixed PyPI index URL in lock file"
|
||||
fi
|
||||
|
||||
@@ -100,6 +102,15 @@ function update_script() {
|
||||
fi
|
||||
fi
|
||||
|
||||
# Remove the ragflow_sdk package from pyproject.toml since we only need the
|
||||
# server components. The SDK is a client library for connecting to RAGFlow
|
||||
# from external applications, which is not needed for server-only installations.
|
||||
if grep -q "sdk.python.ragflow_sdk" pyproject.toml 2>/dev/null; then
|
||||
msg_info "Excluding SDK Package from Installation"
|
||||
sed -i '/sdk.python.ragflow_sdk/d' pyproject.toml
|
||||
msg_ok "Excluded ragflow_sdk from installation"
|
||||
fi
|
||||
|
||||
# Note: We do NOT remove zhipuai or agentrun-sdk from pyproject.toml
|
||||
# These are resolved correctly in the upstream uv.lock file
|
||||
# Removing them would require regenerating the lock file, which causes issues
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "RAGFlow (In Testing)",
|
||||
"name": "RAGFlow (In Testing - Borked)",
|
||||
"slug": "ragflow",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-12",
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
COMMUNITY_SCRIPTS_URL="${COMMUNITY_SCRIPTS_URL:-https://raw.githubusercontent.com/Heretek-AI/ProxmoxVE/refs/heads/main}"
|
||||
source <(curl -fsSL "${COMMUNITY_SCRIPTS_URL}"/misc/build.func)
|
||||
# Author: BillyOutlast
|
||||
# License: MIT | https://github.com/Heretek-AI/ProxmoxVE/raw/main/LICENSE
|
||||
# Source: https://github.com/mudler/LocalRecall
|
||||
|
||||
APP="localrecall"
|
||||
var_tags="${var_tags:-ai;rag;knowledge-base;vector-db}"
|
||||
var_cpu="${var_cpu:-2}"
|
||||
var_ram="${var_ram:-2048}"
|
||||
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 [[ ! -f /usr/local/bin/localrecall ]]; then
|
||||
msg_error "No ${APP} Installation Found!"
|
||||
exit
|
||||
fi
|
||||
|
||||
if check_for_gh_release "localrecall" "mudler/LocalRecall"; then
|
||||
msg_info "Stopping Service"
|
||||
systemctl stop localrecall
|
||||
msg_ok "Stopped Service"
|
||||
|
||||
msg_info "Backing up Data"
|
||||
cp -r /opt/localrecall/data /opt/localrecall_data_backup 2>/dev/null || true
|
||||
msg_ok "Backed up Data"
|
||||
|
||||
msg_info "Updating LocalRecall"
|
||||
setup_go
|
||||
fetch_and_deploy_gh_release "localrecall" "mudler/LocalRecall" "tarball" "latest" "/opt/localrecall"
|
||||
cd /opt/localrecall || exit
|
||||
$STD go build -o localrecall .
|
||||
mv localrecall /usr/local/bin/localrecall
|
||||
cd / || exit
|
||||
rm -rf /opt/localrecall
|
||||
msg_ok "Updated LocalRecall"
|
||||
|
||||
msg_info "Restoring Data"
|
||||
mkdir -p /opt/localrecall/data
|
||||
mkdir -p /opt/localrecall/assets
|
||||
cp -r /opt/localrecall_data_backup/. /opt/localrecall/data 2>/dev/null || true
|
||||
rm -rf /opt/localrecall_data_backup
|
||||
msg_ok "Restored Data"
|
||||
|
||||
msg_info "Starting Service"
|
||||
systemctl start localrecall
|
||||
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}"
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name": "LocalRecall (In Testing - Borked)",
|
||||
"slug": "localrecall",
|
||||
"categories": [20],
|
||||
"date_created": "2026-03-14",
|
||||
"type": "ct",
|
||||
"updateable": true,
|
||||
"privileged": false,
|
||||
"interface_port": 8080,
|
||||
"documentation": "https://github.com/mudler/LocalRecall#readme",
|
||||
"website": "https://github.com/mudler/LocalRecall",
|
||||
"logo": "https://raw.githubusercontent.com/mudler/LocalRecall/main/static/logo.png",
|
||||
"config_path": "/opt/localrecall/.env",
|
||||
"description": "A lightweight RESTful API for managing knowledge bases and files stored in vector databases. No GPU, internet, or cloud services required. Integrates with LocalAI for embeddings.",
|
||||
"install_methods": [
|
||||
{
|
||||
"type": "default",
|
||||
"script": "ct/localrecall.sh",
|
||||
"resources": {
|
||||
"cpu": 2,
|
||||
"ram": 2048,
|
||||
"hdd": 8,
|
||||
"os": "Debian",
|
||||
"version": "13"
|
||||
}
|
||||
}
|
||||
],
|
||||
"default_credentials": {
|
||||
"username": null,
|
||||
"password": null
|
||||
},
|
||||
"notes": [
|
||||
{
|
||||
"text": "Requires an embedding service (like LocalAI) for full functionality. Set OPENAI_BASE_URL environment variable to point to your embedding service.",
|
||||
"type": "info"
|
||||
},
|
||||
{
|
||||
"text": "Default configuration uses Chromem as the vector database engine. For PostgreSQL support, set VECTOR_ENGINE=postgres and DATABASE_URL environment variables.",
|
||||
"type": "info"
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user