Initial import: bare-metal stacks, Server UI, GPU fan, tutorials.

Infrastructure configs for GMKtec K11 (Docker, vLLM, LocalAI, ComfyUI,
control-plane, gpu-fan agent, Server UI with CLI/file explorer/GPU fan curve).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
tomasz-syn-grzegorza
2026-07-05 12:02:04 +00:00
commit 359afb3a59
153 changed files with 18169 additions and 0 deletions
+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
# Configure gpu-fan agent for localhost + ensure API key. UI is in Server UI :8091.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
SERVER_UI_DIR="$(cd "${STACK_DIR}/../server-ui" && pwd)"
CONTROL_PLANE_ENV="/opt/control-plane/.env"
if [[ "${EUID}" -ne 0 ]]; then
echo "Run as root: sudo ${SCRIPT_DIR}/enable-lan.sh"
exit 1
fi
"${SCRIPT_DIR}/install.sh"
set_env_var() {
local file="$1" key="$2" val="$3"
if grep -q "^${key}=" "${file}"; then
sed -i "s|^${key}=.*|${key}=${val}|" "${file}"
else
echo "${key}=${val}" >> "${file}"
fi
}
for pair in "GPU_FAN_API_HOST=127.0.0.1" "GPU_FAN_API_PORT=18090"; do
set_env_var "${CONTROL_PLANE_ENV}" "${pair%%=*}" "${pair#*=}"
done
if ! grep -q '^API_KEY=.\{8,\}' "${CONTROL_PLANE_ENV}"; then
KEY="$(openssl rand -hex 16)"
set_env_var "${CONTROL_PLANE_ENV}" "API_KEY" "${KEY}"
echo "Generated new API_KEY in ${CONTROL_PLANE_ENV}"
else
echo "API_KEY already set in ${CONTROL_PLANE_ENV}"
fi
systemctl restart gpu-fan
sleep 2
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
echo ""
echo "gpu-fan agent: $(systemctl is-active gpu-fan)"
echo "Agent API: http://127.0.0.1:18090 (localhost only)"
echo ""
echo "GPU Fan panel (LAN):"
echo " http://${LAN_IP:-<server-ip>}:8091 → zakładka GPU Fan"
echo ""
echo "API key (Server UI + gpu-fan):"
echo " grep ^API_KEY= ${CONTROL_PLANE_ENV}"
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
SERVER_UI_DIR="$(cd "${STACK_DIR}/../server-ui" && pwd)"
INSTALL_DIR="/opt/gpu-fan"
CONFIG_DIR="/etc/gpu-fan"
SERVICE_NAME="gpu-fan.service"
AGENT_PORT=18090
CONTROL_PLANE_ENV="/opt/control-plane/.env"
if [[ "${EUID}" -ne 0 ]]; then
echo "Run as root: sudo ${SCRIPT_DIR}/install.sh"
exit 1
fi
echo "=== GPU Fan Control — install ==="
bash "${SERVER_UI_DIR}/scripts/setup-control-plane-env.sh"
apt-get update -qq
apt-get install -y python3-venv python3-pip
mkdir -p "${INSTALL_DIR}" "${CONFIG_DIR}"
rsync -a --delete \
--exclude '.venv' \
--exclude '.env' \
--exclude '__pycache__' \
"${STACK_DIR}/" "${INSTALL_DIR}/"
if [[ ! -f "${CONFIG_DIR}/curve.json" ]]; then
cp "${INSTALL_DIR}/curve.default.json" "${CONFIG_DIR}/curve.json"
echo "Installed default curve: ${CONFIG_DIR}/curve.json"
fi
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
install -m 644 "${INSTALL_DIR}/gpu-fan.service" "/etc/systemd/system/${SERVICE_NAME}"
systemctl daemon-reload
systemctl enable "${SERVICE_NAME}"
systemctl restart "${SERVICE_NAME}"
sleep 2
API_KEY_VAL="$(grep '^API_KEY=' "${CONTROL_PLANE_ENV}" | cut -d= -f2- || true)"
LAN_IP="$(hostname -I 2>/dev/null | awk '{print $1}')"
echo ""
echo "Installed to ${INSTALL_DIR}"
echo "Service: $(systemctl is-active "${SERVICE_NAME}" 2>/dev/null || echo unknown)"
echo "Env: ${CONTROL_PLANE_ENV}"
echo "Curve config: ${CONFIG_DIR}/curve.json"
echo "Agent API: 127.0.0.1:${AGENT_PORT} (localhost only)"
echo ""
echo "GPU Fan UI (Server UI, LAN):"
echo " http://${LAN_IP:-<server-ip>}:8091/#gpu-fan"
echo ""
echo "API key:"
echo " grep ^API_KEY= ${CONTROL_PLANE_ENV}"
echo ""
if ss -tlnp 2>/dev/null | grep -q ":${AGENT_PORT}"; then
echo "Port ${AGENT_PORT}: listening"
if [[ -n "${API_KEY_VAL}" ]]; then
STATUS="$(curl -sf "http://127.0.0.1:${AGENT_PORT}/api/status" -H "X-API-Key: ${API_KEY_VAL}" 2>/dev/null | head -c 120 || true)"
echo "Agent status: ${STATUS}..."
fi
else
echo "WARN: port ${AGENT_PORT} not listening — check: journalctl -u ${SERVICE_NAME} -n 20"
fi
echo ""
echo "Logs:"
echo " journalctl -u ${SERVICE_NAME} -f"
echo ""
echo "Pełna instalacja (gpu-fan + Server UI):"
echo " sudo ${SERVER_UI_DIR}/scripts/install-control-plane.sh"
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Self-test: curve logic, NVML read, API (dry-run). Full fan write needs sudo.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${STACK_DIR}"
mkdir -p config
cp -n curve.default.json config/curve.json 2>/dev/null || true
export CURVE_PATH="${STACK_DIR}/config/curve.json"
export DRY_RUN=true
export GPU_FAN_PORT=18090
echo "=== 1. Curve logic ==="
.venv/bin/python -c "
from fan_controller import parse_curve, interpolate_speed
pts = parse_curve({'30':50,'40':65,'50':80,'55':90,'60':100,'70':100})
assert interpolate_speed(45, pts) in (72, 73)
assert interpolate_speed(70, pts) == 100
print('OK')
"
echo "=== 2. NVML read ==="
.venv/bin/python -c "
import pynvml
pynvml.nvmlInit()
h = pynvml.nvmlDeviceGetHandleByIndex(0)
t = pynvml.nvmlDeviceGetTemperature(h, pynvml.NVML_TEMPERATURE_GPU)
print(f'GPU temp: {t}C')
pynvml.nvmlShutdown()
print('OK')
"
echo "=== 3. API dry-run (background) ==="
.venv/bin/python app.py &
APP_PID=$!
trap 'kill $APP_PID 2>/dev/null || true' EXIT
sleep 2
STATUS=$(curl -sf "http://127.0.0.1:${GPU_FAN_PORT}/api/status")
echo "$STATUS" | .venv/bin/python -m json.tool | head -20
TARGET=$(echo "$STATUS" | .venv/bin/python -c "import sys,json; print(json.load(sys.stdin)['target_speed_pct'])")
TEMP=$(echo "$STATUS" | .venv/bin/python -c "import sys,json; print(json.load(sys.stdin)['temperature_c'])")
echo "Temp=${TEMP}C target=${TARGET}%"
curl -sf -X PUT "http://127.0.0.1:${GPU_FAN_PORT}/api/curve" \
-H 'Content-Type: application/json' \
-d '{"points":[{"temp":30,"speed":50},{"temp":40,"speed":65},{"temp":50,"speed":80},{"temp":55,"speed":90},{"temp":60,"speed":100},{"temp":70,"speed":100}]}' \
| .venv/bin/python -m json.tool >/dev/null
echo "Curve PUT: OK"
curl -sf -X POST "http://127.0.0.1:${GPU_FAN_PORT}/api/mode" \
-H 'Content-Type: application/json' \
-d '{"mode":"manual","speed":100}' >/dev/null
echo "Mode manual 100%: OK"
kill $APP_PID 2>/dev/null || true
trap - EXIT
echo ""
if sudo -n true 2>/dev/null; then
echo "=== 4. Fan write (root) ==="
sudo -n env CURVE_PATH="$CURVE_PATH" .venv/bin/python -c "
from pathlib import Path
from fan_controller import FanController
c = FanController(Path('$CURVE_PATH'), 0, 2.0)
c.init_nvml()
c.set_mode('manual', 50)
m = c.update_once()
print(f\"Applied: {m['temperature_c']}C -> {m['target_speed_pct']}%\")
c.set_mode('auto')
c.update_once()
c.shutdown()
print('OK')
"
else
echo "=== 4. Fan write (root) — SKIP (sudo needs password) ==="
echo " Run: sudo scripts/install.sh && sudo systemctl start gpu-fan"
fi
echo ""
echo "All automated checks passed."
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
CONTROL_PLANE_ENV="${STACK_DIR}/../control-plane/.env"
EXAMPLE="${STACK_DIR}/../control-plane/.env.example"
cd "${STACK_DIR}"
if [[ ! -f "${CONTROL_PLANE_ENV}" ]]; then
cp "${EXAMPLE}" "${CONTROL_PLANE_ENV}"
echo "Created ${CONTROL_PLANE_ENV} from example"
fi
set -a
# shellcheck disable=SC1091
source "${CONTROL_PLANE_ENV}"
set +a
export CONTROL_PLANE_ENV="${CONTROL_PLANE_ENV}"
CURVE_PATH="${CURVE_PATH:-/etc/gpu-fan/curve.json}"
export CURVE_PATH
if [[ ! -f "${CURVE_PATH}" ]]; then
if [[ "${EUID}" -eq 0 ]]; then
mkdir -p "$(dirname "${CURVE_PATH}")"
cp curve.default.json "${CURVE_PATH}"
else
mkdir -p "${STACK_DIR}/config"
CURVE_PATH="${STACK_DIR}/config/curve.json"
export CURVE_PATH
[[ -f "${CURVE_PATH}" ]] || cp curve.default.json "${CURVE_PATH}"
echo "Using local curve: ${CURVE_PATH}"
fi
fi
if [[ ! -d .venv ]]; then
python3 -m venv .venv
.venv/bin/pip install -q -r requirements.txt
fi
if [[ "${EUID}" -ne 0 ]]; then
echo "ERROR: Fan control requires root. Use:"
echo " sudo -E ${SCRIPT_DIR}/start.sh"
echo "Or install system-wide:"
echo " sudo ${SCRIPT_DIR}/install.sh && sudo systemctl start gpu-fan"
exit 1
fi
if systemctl is-active --quiet gpu-fan 2>/dev/null; then
echo "ERROR: gpu-fan.service is already running (systemd)."
echo " Use: sudo systemctl stop gpu-fan"
echo " Or: sudo journalctl -u gpu-fan -f"
echo ""
echo "Do not run scripts/start.sh alongside systemd — one instance only."
exit 1
fi
PORT="${GPU_FAN_API_PORT:-18090}"
HOST="${GPU_FAN_API_HOST:-127.0.0.1}"
if ss -tln "sport = :${PORT}" 2>/dev/null | grep -q LISTEN; then
echo "ERROR: Port ${PORT} is already in use."
echo ""
ss -tlnp "sport = :${PORT}" 2>/dev/null || ss -tln "sport = :${PORT}"
echo ""
echo "Common causes:"
echo " - Suspended foreground start (Ctrl+Z) — run: jobs -l && kill %<n>"
echo " - Orphan process — run: sudo ${SCRIPT_DIR}/status.sh"
echo " - systemd still stopping — wait a few seconds"
exit 1
fi
echo "=== GPU Fan Control (foreground) ==="
echo "Agent API: http://${HOST}:${PORT}"
echo "Curve: ${CURVE_PATH}"
echo ""
echo "Stop with Ctrl+C (not Ctrl+Z — suspended process keeps the port)."
echo ""
exec .venv/bin/python app.py
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Diagnose gpu-fan port conflicts and running instances.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
STACK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
OPT_ENV="/opt/control-plane/.env"
REPO_ENV="${STACK_DIR}/../control-plane/.env"
echo "=== gpu-fan status ==="
echo ""
if systemctl list-unit-files gpu-fan.service &>/dev/null; then
echo "systemd:"
systemctl is-active gpu-fan 2>/dev/null && systemctl status gpu-fan --no-pager -l 2>/dev/null | head -8 || echo " inactive"
else
echo "systemd: gpu-fan.service not installed"
fi
echo ""
echo "Ports 80908099:"
if ss -tlnp 2>/dev/null | grep -E ':809[0-9]' ; then
:
elif ss -tln 2>/dev/null | grep -E ':809[0-9]' ; then
echo " (run as root for process names: sudo ss -tlnp)"
else
echo " (none listening)"
fi
echo ""
echo "gpu-fan processes:"
pgrep -af '/opt/gpu-fan/.venv/bin/python|stacks/gpu-fan/.venv/bin/python|gpu-fan/app.py' 2>/dev/null \
|| echo " (none)"
echo ""
echo "Shell suspended jobs (Ctrl+Z):"
if jobs -l 2>/dev/null | grep -q .; then
jobs -l
echo " Kill with: kill %<job-number>"
else
echo " (none in this shell)"
fi
echo ""
echo "GPU_FAN_API_PORT config:"
[[ -f "${OPT_ENV}" ]] && echo " /opt/control-plane/.env: $(grep '^GPU_FAN_API_PORT=' "${OPT_ENV}" || echo '(not set)')"
[[ -f "${REPO_ENV}" ]] && echo " repo control-plane: $(grep '^GPU_FAN_API_PORT=' "${REPO_ENV}" || echo '(not set)')"
echo " Note: production uses /opt/control-plane/.env (shared with Server UI)"
echo ""
if [[ "${1:-}" == "--cleanup" ]]; then
if [[ "${EUID}" -ne 0 ]]; then
echo "ERROR: --cleanup requires root: sudo ${SCRIPT_DIR}/status.sh --cleanup"
exit 1
fi
echo "=== cleanup ==="
systemctl stop gpu-fan 2>/dev/null || true
pkill -f '/opt/gpu-fan/.venv/bin/python /opt/gpu-fan/app.py' 2>/dev/null || true
pkill -f 'stacks/gpu-fan/.venv/bin/python app.py' 2>/dev/null || true
sleep 0.5
if ss -tln 2>/dev/null | grep -qE ':809[0-9]'; then
echo "WARNING: ports still in use:"
ss -tlnp 2>/dev/null | grep -E ':809[0-9]' || ss -tln | grep -E ':809[0-9]'
echo "Check suspended jobs in other shells: jobs -l"
else
echo "Ports 809x are free."
fi
echo ""
echo "Start production instance:"
echo " sudo systemctl start gpu-fan"
echo "Or foreground debug (systemd stopped):"
echo " sudo ${SCRIPT_DIR}/start.sh"
fi