Add stack Update, ComfyUI model manager, and slim ComfyUI stack.

Server UI gains Update on stack cards, ComfyUI Models tab with workflow
scan and downloads, and centralized comfyui_config. Model catalog and
download scripts move from stacks/comfyui to server-ui so ComfyUI stays a
minimal Docker wrapper for easier image updates.
This commit is contained in:
tomasz-syn-grzegorza
2026-07-05 18:45:17 +00:00
parent 359afb3a59
commit 73e4fc005e
33 changed files with 3604 additions and 307 deletions
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
# Shared helpers for comfyui-models.catalog.yaml (simple parser, no PyYAML required).
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SERVER_UI_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
CATALOG_FILE="${CATALOG_FILE:-${SERVER_UI_DIR}/comfyui-models.catalog.yaml}"
catalog_list_ids() {
grep '^ - id:' "${CATALOG_FILE}" | awk '{print $3}'
}
catalog_get_field() {
local model_id="$1"
local field="$2"
awk -v id="${model_id}" -v key="${field}:" '
$0 ~ "^ - id: " id "$" { found=1; next }
found && $0 ~ "^ - id:" { exit }
found && index($0, key) == 5 { sub(/^ [^:]+: /, ""); print; exit }
' "${CATALOG_FILE}"
}
catalog_category_dir() {
local category="$1"
case "${category}" in
checkpoint) echo "checkpoints" ;;
lora) echo "loras" ;;
vae) echo "vae" ;;
controlnet) echo "controlnet" ;;
upscale) echo "upscale_models" ;;
latent_upscale) echo "latent_upscale_models" ;;
text_encoder) echo "text_encoders" ;;
diffusion_models) echo "diffusion_models" ;;
clip) echo "clip" ;;
unet) echo "unet" ;;
*) echo "${category}" ;;
esac
}
catalog_dest_path() {
local model_id="$1"
local data_root="${2:-${DATA_ROOT:-/data}}"
local local_path category filename subdir
local_path="$(catalog_get_field "${model_id}" local_path)"
if [[ -n "${local_path}" ]]; then
echo "${local_path}"
return
fi
category="$(catalog_get_field "${model_id}" category)"
filename="$(catalog_get_field "${model_id}" filename)"
subdir="$(catalog_category_dir "${category}")"
echo "${data_root}/apps/comfyui/models/${subdir}/${filename}"
}
catalog_ensure_dirs() {
local data_root="${1:-/data}"
mkdir -p \
"${data_root}/apps/comfyui/storage" \
"${data_root}/apps/comfyui/models/checkpoints" \
"${data_root}/apps/comfyui/models/loras" \
"${data_root}/apps/comfyui/models/vae" \
"${data_root}/apps/comfyui/models/controlnet" \
"${data_root}/apps/comfyui/models/upscale_models" \
"${data_root}/apps/comfyui/models/latent_upscale_models" \
"${data_root}/apps/comfyui/models/text_encoders" \
"${data_root}/apps/comfyui/models/diffusion_models" \
"${data_root}/apps/comfyui/models/clip" \
"${data_root}/apps/comfyui/models/unet" \
"${data_root}/apps/comfyui/cache/hf-hub" \
"${data_root}/apps/comfyui/cache/torch-hub" \
"${data_root}/apps/comfyui/input" \
"${data_root}/apps/comfyui/output" \
"${data_root}/apps/comfyui/custom_nodes" \
"${data_root}/apps/comfyui/workflows"
}
catalog_model_downloaded() {
local model_id="$1"
local dest
dest="$(catalog_dest_path "${model_id}")"
[[ -f "${dest}" ]]
}
load_comfyui_stack_env() {
local repo_root
repo_root="$(cd "${SCRIPT_DIR}/../../../.." && pwd)"
local env_file="${repo_root}/stacks/comfyui/.env"
if [[ -f "${env_file}" ]]; then
set -a
# shellcheck disable=SC1090
source "${env_file}"
set +a
fi
}
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/catalog-lib.sh"
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/download-http-lib.sh"
URL=""
DEST=""
while [[ $# -gt 0 ]]; do
case "$1" in
--url)
URL="${2:-}"
shift 2
;;
--dest)
DEST="${2:-}"
shift 2
;;
*)
echo "Usage: $0 --url URL --dest /absolute/path/to/file"
exit 1
;;
esac
done
if [[ -z "${URL}" || -z "${DEST}" ]]; then
echo "Usage: $0 --url URL --dest /absolute/path/to/file"
exit 1
fi
load_comfyui_stack_env
DATA_ROOT="${DATA_ROOT:-/data}"
MODELS_ROOT="${DATA_ROOT}/apps/comfyui/models"
if [[ "${DEST}" != "${MODELS_ROOT}"/* ]]; then
echo "ERROR: Destination must be under ${MODELS_ROOT}"
exit 1
fi
if [[ -f "${DEST}" ]]; then
echo "Already exists: ${DEST}"
echo ""
echo "Done."
exit 0
fi
mkdir -p "$(dirname "${DEST}")"
download_http_file "${URL}" "${DEST}"
if [[ ! -f "${DEST}" ]]; then
echo "ERROR: Download finished but file missing: ${DEST}"
exit 1
fi
echo ""
echo "Done."
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
# Shared HTTP download helpers for ComfyUI model scripts.
# shellcheck disable=SC2034
download_http_file() {
local url="$1"
local dest="$2"
local data_root="${DATA_ROOT:-/data}"
local dest_dir
dest_dir="$(dirname "${dest}")"
echo "Downloading ${url} ..."
echo "Target: ${dest}"
if [[ -w "${dest_dir}" ]]; then
if [[ -n "${HF_TOKEN:-}" ]]; then
if command -v wget >/dev/null 2>&1; then
wget -c --progress=dot:giga \
--header="Authorization: Bearer ${HF_TOKEN}" \
-O "${dest}" "${url}"
else
curl -fL -C - \
-H "Authorization: Bearer ${HF_TOKEN}" \
-o "${dest}" "${url}"
fi
else
if command -v wget >/dev/null 2>&1; then
wget -c --progress=dot:giga -O "${dest}" "${url}"
else
curl -fL -C - -o "${dest}" "${url}"
fi
fi
return
fi
if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx 'comfyui'; then
local container_dest="/root/ComfyUI/models${dest#"${data_root}/apps/comfyui/models"}"
echo "Host directory not writable — using comfyui container: ${container_dest}"
if [[ -n "${HF_TOKEN:-}" ]]; then
docker exec comfyui wget -c --progress=dot:giga \
--header="Authorization: Bearer ${HF_TOKEN}" \
-O "${container_dest}" "${url}"
else
docker exec comfyui wget -c --progress=dot:giga \
-O "${container_dest}" "${url}"
fi
return
fi
echo "ERROR: Cannot write to ${dest_dir}"
echo "Fix: sudo chown -R \$USER:\$USER ${data_root}/apps/comfyui/models"
echo "Or start ComfyUI: cd stacks/comfyui && ./scripts/start.sh"
exit 1
}
+105
View File
@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/catalog-lib.sh"
if [[ $# -lt 1 ]]; then
echo "Usage: $0 <catalog-model-id>"
echo ""
echo "Available models:"
catalog_list_ids | sed 's/^/ /'
exit 1
fi
MODEL_ID="$1"
if ! catalog_list_ids | grep -qx "${MODEL_ID}"; then
echo "ERROR: Unknown model id: ${MODEL_ID}"
echo "Run: ${SCRIPT_DIR}/list-models.sh"
exit 1
fi
load_comfyui_stack_env
DATA_ROOT="${DATA_ROOT:-/data}"
catalog_ensure_dirs "${DATA_ROOT}"
SOURCE="$(catalog_get_field "${MODEL_ID}" source)"
NAME="$(catalog_get_field "${MODEL_ID}" name)"
DEST="$(catalog_dest_path "${MODEL_ID}" "${DATA_ROOT}")"
echo "=== Download: ${NAME} (${MODEL_ID}) ==="
echo "Source: ${SOURCE}"
echo "Target: ${DEST}"
echo ""
if [[ -f "${DEST}" ]]; then
echo "Already exists: ${DEST}"
echo ""
echo "Done."
exit 0
fi
mkdir -p "$(dirname "${DEST}")"
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/download-http-lib.sh"
case "${SOURCE}" in
huggingface)
HF_REPO="$(catalog_get_field "${MODEL_ID}" hf_repo)"
FILENAME="$(catalog_get_field "${MODEL_ID}" filename)"
DOWNLOAD_URL="$(catalog_get_field "${MODEL_ID}" download_url)"
HF_REVISION="$(catalog_get_field "${MODEL_ID}" hf_revision)"
REVISION="${HF_REVISION:-main}"
if [[ -n "${DOWNLOAD_URL}" ]]; then
URL="${DOWNLOAD_URL}"
elif [[ -n "${HF_REPO}" && -n "${FILENAME}" ]]; then
URL="https://huggingface.co/${HF_REPO}/resolve/${REVISION}/${FILENAME}"
else
echo "ERROR: Catalog entry missing download_url or hf_repo/filename"
exit 1
fi
download_http_file "${URL}" "${DEST}"
;;
url)
DOWNLOAD_URL="$(catalog_get_field "${MODEL_ID}" download_url)"
if [[ -z "${DOWNLOAD_URL}" ]]; then
echo "ERROR: Catalog entry missing download_url"
exit 1
fi
download_http_file "${DOWNLOAD_URL}" "${DEST}"
;;
civitai)
CIVITAI_MODEL_ID="$(catalog_get_field "${MODEL_ID}" civitai_model_id)"
if [[ -z "${CIVITAI_MODEL_ID}" ]]; then
echo "ERROR: Catalog entry missing civitai_model_id"
exit 1
fi
if [[ -z "${CIVITAI_TOKEN:-}" ]]; then
echo "ERROR: CIVITAI_TOKEN not set in stacks/comfyui/.env"
exit 1
fi
curl -fL \
-H "Authorization: Bearer ${CIVITAI_TOKEN}" \
-o "${DEST}" \
"https://civitai.com/api/download/models/${CIVITAI_MODEL_ID}"
;;
*)
echo "ERROR: Unknown source: ${SOURCE}"
exit 1
;;
esac
if [[ ! -f "${DEST}" ]]; then
echo "ERROR: Download finished but file missing: ${DEST}"
exit 1
fi
echo ""
echo "Done."
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "${SCRIPT_DIR}/catalog-lib.sh"
load_comfyui_stack_env
DATA_ROOT="${DATA_ROOT:-/data}"
catalog_ensure_dirs "${DATA_ROOT}"
echo "=== ComfyUI model catalog ==="
echo "Catalog: ${CATALOG_FILE}"
echo "Data: ${DATA_ROOT}"
echo ""
printf "%-24s %-12s %-8s %s\n" "ID" "CATEGORY" "ON DISK" "NAME"
printf "%-24s %-12s %-8s %s\n" "----" "--------" "-------" "----"
while IFS= read -r model_id; do
name="$(catalog_get_field "${model_id}" name)"
category="$(catalog_get_field "${model_id}" category)"
if catalog_model_downloaded "${model_id}"; then
on_disk="yes"
else
on_disk="no"
fi
printf "%-24s %-12s %-8s %s\n" "${model_id}" "${category}" "${on_disk}" "${name}"
done < <(catalog_list_ids)
echo ""
echo "Download on demand: ${SCRIPT_DIR}/download-model.sh <id>"
echo "Target root: ${DATA_ROOT}/apps/comfyui/models/"
+2
View File
@@ -49,6 +49,8 @@ rsync -a --delete \
--exclude '__pycache__' \
"${STACK_DIR}/" "${INSTALL_DIR}/"
install -m 644 "$(dirname "${STACK_DIR}")/control-plane/env_loader.py" "${INSTALL_DIR}/env_loader.py"
python3 -m venv "${INSTALL_DIR}/.venv"
"${INSTALL_DIR}/.venv/bin/pip" install --upgrade pip -q
"${INSTALL_DIR}/.venv/bin/pip" install -r "${INSTALL_DIR}/requirements.txt" -q
@@ -2,17 +2,34 @@
# Print API key + browser instructions (shared by install scripts and show-api-key.sh).
set -euo pipefail
print_api_key_instructions() {
local env_file="${1:-/opt/control-plane/.env}"
local ui_port="${2:-8091}"
_read_api_key() {
local env_file="$1"
local api_key=""
local lan_ip=""
if [[ -r "${env_file}" ]]; then
api_key="$(grep '^API_KEY=' "${env_file}" 2>/dev/null | cut -d= -f2- || true)"
elif [[ -f "${env_file}" ]]; then
api_key="$(sudo grep '^API_KEY=' "${env_file}" 2>/dev/null | cut -d= -f2- || true)"
fi
if [[ -z "${api_key}" ]]; then
local pid
pid="$(pgrep -f '/opt/server-ui/app.py' 2>/dev/null | head -1 || true)"
if [[ -n "${pid}" && -r "/proc/${pid}/environ" ]]; then
api_key="$(tr '\0' '\n' < "/proc/${pid}/environ" 2>/dev/null | grep '^API_KEY=' | cut -d= -f2- || true)"
fi
fi
printf '%s' "${api_key}"
}
print_api_key_instructions() {
local env_file="${1:-/opt/control-plane/.env}"
local ui_port="${2:-8091}"
local dev_env="${3:-}"
local api_key=""
local dev_key=""
local lan_ip=""
local verify_ok=0
api_key="$(_read_api_key "${env_file}")"
lan_ip="$(hostname -I 2>/dev/null | awk '{print $1}')"
[[ -n "${lan_ip}" ]] || lan_ip="<IP-serwera>"
@@ -30,6 +47,33 @@ print_api_key_instructions() {
return 1
fi
if [[ -n "${dev_env}" && -f "${dev_env}" ]]; then
dev_key="$(_read_api_key "${dev_env}")"
if [[ -n "${dev_key}" && "${dev_key}" != "${api_key}" ]]; then
echo "OSTRZEŻENIE: klucz dev różni się od produkcji!"
echo " prod: ${env_file}"
echo " dev: ${dev_env}"
echo " Napraw: sudo bash stacks/server-ui/scripts/setup-control-plane-env.sh"
echo ""
fi
fi
if curl -sf -X POST "http://127.0.0.1:${ui_port}/api/auth/verify" \
-H "Content-Type: application/json" \
-d "{\"api_key\":\"${api_key}\"}" >/dev/null 2>&1; then
verify_ok=1
fi
echo "Plik klucza: ${env_file}"
if [[ "${verify_ok}" -eq 1 ]]; then
echo "Weryfikacja live (server-ui :${ui_port}): OK"
else
echo "Weryfikacja live (server-ui :${ui_port}): FAIL"
echo " Sprawdź: systemctl status server-ui"
echo " Lub: bash stacks/server-ui/scripts/verify-api-key.sh"
fi
echo ""
echo "1. Twój klucz API (skopiuj):"
echo ""
echo " ${api_key}"
@@ -45,13 +89,20 @@ print_api_key_instructions() {
echo " d) Kliknij „Sprawdź klucz” (powinno być: Klucz poprawny)"
echo " e) Dopiero potem Start/Stop, CLI, Pliki, GPU Fan"
echo ""
echo "4. Stary klucz w przeglądarce?"
echo " a) W panelu kliknij „Wyczyść klucz”"
echo " b) Lub F12 → Application → Local Storage → usuń server-ui-api-key"
echo " c) Ctrl+F5, potem kroki 23"
echo ""
echo "Plik klucza (na przyszłość):"
echo " sudo grep ^API_KEY= ${env_file}"
echo ""
echo "══════════════════════════════════════════════"
echo ""
[[ "${verify_ok}" -eq 1 ]]
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
print_api_key_instructions "${1:-/opt/control-plane/.env}" "${2:-8091}"
print_api_key_instructions "${1:-/opt/control-plane/.env}" "${2:-8091}" "${3:-}"
fi
+19 -7
View File
@@ -9,16 +9,28 @@ PROD_ENV="/opt/control-plane/.env"
DEV_ENV="${REPO_ROOT}/stacks/control-plane/.env"
UI_PORT=8091
if [[ -f "${DEV_ENV}" ]]; then
# Production is canonical when installed; dev only as fallback.
if [[ -f "${PROD_ENV}" ]]; then
ENV_FILE="${PROD_ENV}"
UI_PORT="$(grep '^SERVER_UI_PORT=' "${PROD_ENV}" 2>/dev/null | cut -d= -f2- || true)"
if [[ -z "${UI_PORT}" ]]; then
UI_PORT="$(sudo grep '^SERVER_UI_PORT=' "${PROD_ENV}" 2>/dev/null | cut -d= -f2- || true)"
fi
elif [[ -r "${DEV_ENV}" ]]; then
ENV_FILE="${DEV_ENV}"
UI_PORT="$(grep '^SERVER_UI_PORT=' "${DEV_ENV}" 2>/dev/null | cut -d= -f2- || echo 8091)"
fi
# Prefer dev copy when readable (same key after sync); else production.
ENV_FILE="${DEV_ENV}"
if [[ ! -r "${ENV_FILE}" ]]; then
else
ENV_FILE="${PROD_ENV}"
fi
[[ -n "${UI_PORT}" ]] || UI_PORT=8091
# shellcheck source=print-api-key-instructions.sh
source "${SCRIPT_DIR}/print-api-key-instructions.sh"
print_api_key_instructions "${ENV_FILE}" "${UI_PORT}"
if ! print_api_key_instructions "${ENV_FILE}" "${UI_PORT}" "${DEV_ENV}"; then
if [[ -r "${DEV_ENV}" && "${ENV_FILE}" != "${DEV_ENV}" ]]; then
echo "Fallback: odczyt z dev (prod nieczytelny bez sudo): ${DEV_ENV}"
print_api_key_instructions "${DEV_ENV}" "${UI_PORT}" ""
else
exit 1
fi
fi
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Verify API_KEY from /opt/control-plane/.env against live server-ui.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=print-api-key-instructions.sh
source "${SCRIPT_DIR}/print-api-key-instructions.sh"
PROD_ENV="/opt/control-plane/.env"
UI_PORT=8091
if [[ -f "${PROD_ENV}" ]]; then
UI_PORT="$(grep '^SERVER_UI_PORT=' "${PROD_ENV}" 2>/dev/null | cut -d= -f2- || true)"
if [[ -z "${UI_PORT}" ]]; then
UI_PORT="$(sudo grep '^SERVER_UI_PORT=' "${PROD_ENV}" 2>/dev/null | cut -d= -f2- || true)"
fi
fi
[[ -n "${UI_PORT}" ]] || UI_PORT=8091
api_key="$(_read_api_key "${PROD_ENV}")"
if [[ -z "${api_key}" && -r "${SCRIPT_DIR}/../../control-plane/.env" ]]; then
api_key="$(_read_api_key "${SCRIPT_DIR}/../../control-plane/.env")"
fi
if [[ -z "${api_key}" ]]; then
echo "FAIL: brak API_KEY w ${PROD_ENV}"
exit 1
fi
if curl -sf -X POST "http://127.0.0.1:${UI_PORT}/api/auth/verify" \
-H "Content-Type: application/json" \
-d "{\"api_key\":\"${api_key}\"}" >/dev/null; then
echo "OK: klucz z ${PROD_ENV} działa na http://127.0.0.1:${UI_PORT}"
exit 0
fi
echo "FAIL: klucz z ${PROD_ENV} odrzucony przez server-ui (:${UI_PORT})"
echo " Uruchom: bash ${SCRIPT_DIR}/show-api-key.sh"
echo " Sync env: sudo bash ${SCRIPT_DIR}/setup-control-plane-env.sh"
echo " Restart: sudo systemctl restart server-ui"
exit 1