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
+165
View File
@@ -0,0 +1,165 @@
"""Load unified control-plane environment from file + os.environ."""
from __future__ import annotations
import os
from pathlib import Path
_ENV_OVERRIDE_KEYS = (
"API_KEY",
"SERVER_UI_HOST",
"SERVER_UI_PORT",
"REPO_ROOT",
"DOCKER_GID",
"GPU_FAN_AGENT_URL",
"GPU_FAN_API_HOST",
"GPU_FAN_API_PORT",
"GPU_FAN_HOST",
"GPU_FAN_PORT",
"CURVE_PATH",
"POLL_INTERVAL",
"GPU_INDEX",
"DRY_RUN",
"FILE_EXPLORER_ROOT",
"FILE_EXPLORER_MAX_BYTES",
"CLI_ENABLED",
"CLI_SHELL",
"CLI_DEFAULT_CWD",
"CLI_MAX_SESSIONS",
)
def _parse_env_file(path: Path) -> dict[str, str]:
values: dict[str, str] = {}
if not path.is_file():
return values
try:
text = path.read_text(encoding="utf-8")
except OSError:
# e.g. /opt/control-plane/.env is root-only; systemd still injects via os.environ
return values
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, _, val = line.partition("=")
values[key.strip()] = val.strip()
return values
def _merge_into(target: dict[str, str], source: dict[str, str]) -> None:
for key, val in source.items():
if val:
target[key] = val
def _is_production_stack_dir(stack_dir: Path) -> bool:
try:
return stack_dir.resolve().parts[1:2] == ("opt",)
except IndexError:
return False
def control_plane_env_paths(stack_dir: Path) -> list[Path]:
"""Candidate .env files in load order (earlier = lower priority among files)."""
paths: list[Path] = []
custom = os.environ.get("CONTROL_PLANE_ENV", "").strip()
if custom:
paths.append(Path(custom))
if _is_production_stack_dir(stack_dir):
# Production (/opt/server-ui, /opt/gpu-fan): single canonical file only.
prod = Path("/opt/control-plane/.env")
if prod not in paths:
paths.append(prod)
docker_repo = Path("/repo/stacks/control-plane/.env")
if docker_repo.is_file() and docker_repo not in paths:
paths.append(docker_repo)
return paths
# Dev (repo stacks/*): stacks/control-plane/.env only — no legacy per-service .env.
repo_control_plane = stack_dir.parent / "control-plane" / ".env"
if repo_control_plane.is_file() and repo_control_plane not in paths:
paths.append(repo_control_plane)
docker_repo = Path("/repo/stacks/control-plane/.env")
if docker_repo.is_file() and docker_repo not in paths:
paths.append(docker_repo)
return paths
def _has_stack_compose(repo_root: Path) -> bool:
stacks = repo_root / "stacks"
if not stacks.is_dir():
return False
for name in ("compose.yaml", "docker-compose.yml", "docker-compose.yaml"):
if (stacks / "localai" / name).is_file():
return True
return False
def _production_repo_root_candidates() -> list[Path]:
home = Path.home()
return [
home / "cursor" / "ubuntu-bare-metal",
Path("/data/apps/ubuntu-bare-metal"),
Path("/repo"),
home / "ubuntu-bare-metal",
]
def resolve_repo_root(stack_dir: Path, env: dict[str, str]) -> Path:
"""Resolve ubuntu-bare-metal repo root for compose stack paths."""
for key in ("REPO_ROOT",):
raw = (env.get(key) or os.environ.get(key) or "").strip()
if raw:
candidate = Path(raw).expanduser().resolve()
if _has_stack_compose(candidate):
return candidate
if _is_production_stack_dir(stack_dir):
for candidate in _production_repo_root_candidates():
resolved = candidate.expanduser().resolve()
if _has_stack_compose(resolved):
return resolved
return (stack_dir.parent.parent).resolve()
def api_key_source(stack_dir: Path, values: dict[str, str]) -> str:
"""Human-readable hint for logs (no secret values)."""
if os.environ.get("API_KEY"):
return "systemd/os.environ"
for path in control_plane_env_paths(stack_dir):
parsed = _parse_env_file(path)
if parsed.get("API_KEY"):
return str(path)
if values.get("API_KEY"):
return "merged env"
return "not configured"
def load_control_plane_env(stack_dir: Path) -> dict[str, str]:
"""Merge env files then apply os.environ (systemd EnvironmentFile wins)."""
values: dict[str, str] = {}
for path in control_plane_env_paths(stack_dir):
_merge_into(values, _parse_env_file(path))
for key in _ENV_OVERRIDE_KEYS:
if key in os.environ:
values[key] = os.environ[key]
values.setdefault("GPU_FAN_AGENT_URL", "http://127.0.0.1:18090")
return values
def ensure_control_plane_import_path() -> None:
"""Add /opt/control-plane to sys.path for production imports."""
import sys
opt = "/opt/control-plane"
if opt not in sys.path:
sys.path.insert(0, opt)
repo = Path(__file__).resolve().parent
repo_str = str(repo)
if repo_str not in sys.path:
sys.path.insert(0, repo_str)