Files
ubuntu-bare-metal/coding-agent/SERVER-UI-ARCHITECTURE-RESEARCH.md
tomasz-syn-grzegorza 359afb3a59 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>
2026-07-05 12:02:04 +00:00

452 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Server UI — architektura uniwersalnego panelu (research DevOps)
**Data:** 2026-07-04
**Host:** gmktec-k11, Ubuntu 26.04, RTX 3090 Ti
**Kontekst:** Czy UI gpu-fan można przenieść do Server UI w Dockerze, skoro daemon NVML musi zostać na hoście (root)? Jak zbudować panel w stylu appliance (Proxmox / TrueNAS) na Ubuntu?
**Powiązane:**
- [`stacks/server-ui/`](../stacks/server-ui/)
- [`stacks/gpu-fan/coding-agent/DOCKER-VS-HOST-REPORT.md`](../stacks/gpu-fan/coding-agent/DOCKER-VS-HOST-REPORT.md)
- [`DOCKER-UI-DEPLOYMENT.md`](DOCKER-UI-DEPLOYMENT.md)
- [`ADR-001-host-agent-control-plane.md`](ADR-001-host-agent-control-plane.md)
---
## 1. Executive summary
**Werdykt:** Przeniesienie **UI** gpu-fan do Server UI przy **daemonie NVML na hoście** to **właściwy, branżowo spójny kierunek** (wzorzec *control plane UI + host agent*). **Nie** pakować sterowania wentylatorami do kontenera Docker.
**Rekomendowana strategia:** **C** — rozdziel gpu-fan na agent localhost (`127.0.0.1:18090`) + zintegrowany panel w Server UI + dockerizacja Server UI z `docker.sock` i proxy do agenta.
**Stan dziś:** Oba panele (gpu-fan :8090, server-ui :8091) działają jako **systemd na hoście**, nie w Dockerze. Cel „wszystkie UI serwerowe w Dockerze” wymaga najpierw dockerizacji server-ui, potem integracji gpu-fan jako host agent.
---
## 2. Pytanie biznesowe
> gpu-fan musi działać na hoście (root, NVML). Czy UI gpu-fan może działać w kontenerze Docker razem z server-ui, żeby wszystkie UI serwerowe zarządzały rootem z jednego miejsca?
**Odpowiedź skrócona:**
| Warstwa | Gdzie | Dlaczego |
|---------|-------|----------|
| Pętla NVML + zapis wentylatorów | **Host, systemd, root** | Jedyny wspierany i bezpieczny model ([gpu-fan report](../stacks/gpu-fan/coding-agent/DOCKER-VS-HOST-REPORT.md)) |
| API agenta (status, krzywa, tryb) | **Host, localhost only** | Most dla UI bez publicznego NVML |
| Web UI (dashboard, wykres, przyciski) | **Server UI (docelowo Docker)** | Jedna przeglądarka, jeden port, jeden API key |
| Start/stop stacków Docker | **Server UI + docker.sock** | Wzorzec Portainer/Dockge |
„UI w Dockerze zarządza rootem” = **proxy HTTP do host agenta**, nie uruchomienie NVML write w kontenerze.
---
## 3. Stan obecny (as-is)
```mermaid
flowchart LR
browser1["Przeglądarka"]
browser2["Przeglądarka"]
gpuFan["gpu-fan.service root :8090"]
serverUI["server-ui systemd :8091"]
dockerStacks["Docker stacks"]
gpu["GPU NVML"]
browser1 --> gpuFan
browser2 --> serverUI
gpuFan --> gpu
serverUI -->|"docker compose CLI"| dockerStacks
serverUI -.->|"link :8090"| gpuFan
```
| Komponent | Runtime | Port | Uprawnienia | Pliki kluczowe |
|-----------|---------|------|-------------|----------------|
| gpu-fan | systemd, `User=root` | 8090 | NVML write + FastAPI + static UI w **jednym** procesie | `stacks/gpu-fan/app.py`, `fan_controller.py` |
| server-ui | systemd | 8091 | `docker compose` subprocess, `nvidia-smi` read-only | `stacks/server-ui/app.py`, `compose_runner.py` |
| comfyui / localai / vllm | Docker | 8188 / 8070 / 8000 | GPU compute | `stacks/*/compose.yaml` |
| npmplus | Docker, `network_mode: host` | 80, 443, 81 | reverse proxy | `stacks/npmplus/compose.yaml` |
### Problemy architektoniczne
1. **Dwa panele** — porty 8090 i 8091, osobne API key, rozproszony UX.
2. **Coupling UI+daemon w gpu-fan**`app.py` uruchamia wątek NVML i uvicorn w jednym procesie; static HTML woła `/api/*` na tym samym origin.
3. **Server-ui nie jest w Dockerze** — mimo nazwy „Docker UI” to FastAPI na hoście (`/opt/server-ui`, `server-ui.service`).
4. **Brak jednego entry pointu** — NPMPlus domyślnie nie proxyuje 8090/8091; użytkownik musi znać wiele URL.
5. **„Zarządzanie rootem”** — server-ui nie może bezpośrednio pisać do NVML; potrzebuje host agent API.
### Wymuszenie root w gpu-fan
```python
# stacks/gpu-fan/app.py (linie 161164)
if os.geteuid() != 0 and not DRY_RUN:
log.error("GPU fan control requires root (NVML write access). Run with sudo.")
sys.exit(1)
```
---
## 4. Analiza coupling gpu-fan (UI + daemon)
### Obecna struktura procesu
```
gpu-fan.service (root)
└── python app.py
├── FanController.run_loop() # wątek daemon — co 2s NVML
└── uvicorn :8090
├── GET / → static/index.html (~750 linii JS)
├── GET /api/status
├── GET/PUT /api/curve
├── POST /api/mode
└── POST /api/reload
```
Frontend (`static/index.html`) używa **względnych** ścieżek (`fetch('/api/status')`) — działa tylko gdy UI i API są na tym samym hoście:porcie.
### Co można rozdzielić bez ryzyka
| Element | Zostaje na hoście | Może do Server UI |
|---------|-------------------|-------------------|
| `fan_controller.py` pętla NVML | **Tak** | Nie |
| FastAPI endpoints `/api/*` | **Tak** (localhost) | Proxy z server-ui |
| `static/index.html` | Nie | **Tak** |
| Graceful shutdown → auto fan | **Tak** (SIGTERM w systemd) | Nie |
### Docelowy podział (agent)
```
gpu-fan-daemon.service (root)
└── python fan_daemon.py # lub app.py --api-only
├── FanController.run_loop()
└── uvicorn 127.0.0.1:18090
└── /api/* only (bez static/)
server-ui (Docker lub systemd)
└── static + proxy /api/gpu-fan/* → http://host.docker.internal:18090/api/*
```
**Krytyczne:** Pętla NVML musi działać **niezależnie** od UI — pad panelu nie może zatrzymać chłodzenia.
---
## 5. Porównanie branżowe
### Proxmox VE (Debian appliance)
| Aspekt | Implementacja | Lekcja |
|--------|---------------|--------|
| Web UI | `pveproxy` na hoście | UI oddzielone od workloadów, ale **nie** w „zwykłym” kontenerze aplikacyjnym |
| Operacje VM/storage | `pvedaemon`, QEMU, LVM na hoście | Privileged zawsze na host kernel |
| Model | Single-node hypervisor | Jeden panel, wiele usług backendowych na hoście |
Proxmox **nie** pakuje hypervisora do Docker — control plane jest częścią OS.
### TrueNAS
| Wariant | UI | Storage / hardware |
|---------|-----|------------------|
| CORE | nginx + middleware na hoście | ZFS w kernelu |
| SCALE | UI w middleware (K8s) | Dane nadal przy host storage stack |
Lekcja: nawet przy „nowoczesnym” SCALE ciężkie operacje zostają przy hoście.
### Cockpit (Ubuntu)
- `cockpit.socket` + moduły na hoście
- Dostęp do `systemd`, sieci, storage przez D-Bus / polkit
- Oficjalny wzorzec Canonical dla headless Ubuntu
Alternatywa: zamiast własnego server-ui — Cockpit + plugin. Minus: brak whitelist stacków AI, polityki GPU, custom NVML fan.
### Portainer / Dockge (homelab Docker)
| Element | Wzorzec |
|---------|---------|
| UI | Kontener |
| Docker API | Mount `/var/run/docker.sock` |
| Host hardware (fan, disk, systemd) | **Poza scope** — nie sterują NVML |
To jest **najbliższy analog** do waszej wizji dla warstwy Docker; gpu-fan wymaga **dodatkowego** host agent (jak brakujący moduł Portainera).
### Macierz porównawcza
| Produkt | UI w kontenerze? | Host privileged ops | Pasuje do gmktec-k11? |
|---------|------------------|----------------------|------------------------|
| Proxmox | Nie (host) | Tak | Wzorzec appliance, za ciężki |
| TrueNAS | Częściowo | Tak | Storage-first, nie AI |
| Cockpit | Nie | Tak | Generyczny, słaba integracja AI stacks |
| Portainer | Tak | Tylko docker.sock | Brak gpu-fan |
| **Server UI (propozycja)** | **Tak (docelowo)** | **Via host agent** | **Dopasowany** |
---
## 6. Macierz strategii (AE)
### A — Status quo (dwa panele, host systemd)
- **Plus:** Działa dziś, zero refactoru
- **Minus:** 8090 + 8091, dwa klucze, rozproszony UX
- **Verdict:** Utrzymanie krótkoterminowe
### B — Split gpu-fan + UI w server-ui, oba systemd na hoście
- **Plus:** Jeden port 8091, mniejszy refactor niż Docker
- **Minus:** Server-ui nadal nie w kontenerze
- **Verdict:** **Dobry krok pośredni** (Faza 12 bez Fazy 3)
### C — Split gpu-fan daemon + server-ui w Dockerze (REKOMENDOWANE)
- **Plus:** Portainer pattern; jeden panel; agent tylko localhost; :8090 znika z LAN
- **Minus:** Dockerfile, mounty, `host.docker.internal` na Linux
- **Verdict:** **Strategia docelowa**
### D — Cockpit zamiast server-ui
- **Plus:** Dojrzały ekosystem Ubuntu
- **Minus:** Custom plugin NVML, brak polityki GPU / whitelist compose
- **Verdict:** Tylko przy rezygnacji z własnego UI
### E — gpu-fan NVML w privileged kontenerze
- **Plus:** Literalnie „wszystko w Dockerze”
- **Minus:** Kruche, `Insufficient Permissions` na GeForce, ryzyko manual fan po crash
- **Verdict:** **Odrzuć** — patrz [`gpu-fan/docs/05-DLACZEGO-NIE-DOCKER.md`](../stacks/gpu-fan/docs/05-DLACZEGO-NIE-DOCKER.md)
**Wybór:** **C**, z opcjonalnym **B** jako pierwsze PR (integracja UI przed dockerizacją).
---
## 7. Architektura docelowa (to-be)
```mermaid
flowchart TB
browser["Przeglądarka"]
npm["NPMPlus :443 opcjonalnie"]
subgraph dockerPlane [Docker control plane]
serverUI["server-ui container :8091"]
end
subgraph hostAgents [Host systemd root]
gpuAgent["gpu-fan-daemon 127.0.0.1:18090"]
futureAgent["host-agent przyszłość"]
end
subgraph workloads [Docker workloads]
comfyui["comfyui"]
localai["localai"]
npmplus["npmplus host network"]
end
gpu["RTX 3090 Ti"]
browser --> npm
npm --> serverUI
browser --> serverUI
serverUI -->|"docker.sock"| workloads
serverUI -->|"proxy /api/gpu-fan"| gpuAgent
serverUI -.->|"future /api/host"| futureAgent
gpuAgent --> gpu
```
### Warstwy odpowiedzialności
| Warstwa | Odpowiedzialność | Technologia |
|---------|------------------|-------------|
| **Presentation** | Jeden dashboard, auth, nawigacja | server-ui |
| **Orchestration** | start/stop compose, logi, polityka GPU | server-ui + `docker.sock` |
| **Host agents** | NVML fan; przyszłe: systemd, `/data` health | systemd, API localhost |
| **Workloads** | AI inference / image gen | istniejące stacki compose |
### Co NIE wchodzi do Server UI (scope)
- Pełne osadzenie ComfyUI / LocalAI SPA w iframe — opcjonalne; dziś wystarczą linki `:8188`, `:8070`
- Zastąpienie NPMPlus — osobny stack reverse proxy
- Pełny hypervisor (VM, ZFS) — poza zakresem ubuntu-bare-metal
---
## 8. Plan implementacji (fazy)
### Faza 1 — Rozdziel gpu-fan (host)
**Cel:** Agent API bez publicznego UI.
| Zadanie | Pliki |
|---------|-------|
| Wydziel tryb API-only lub `fan_daemon.py` | `stacks/gpu-fan/app.py` lub nowy moduł |
| Env: `GPU_FAN_API_HOST=127.0.0.1`, `GPU_FAN_API_PORT=18090` | `.env.example`, `/opt/control-plane/.env` |
| Usuń serwowanie `static/` z produkcji | `app.py` |
| Zaktualizuj `gpu-fan.service` | `gpu-fan.service` |
| Deprecate publiczny :8090 w `enable-lan.sh` | `scripts/enable-lan.sh` |
| Dokumentacja | `docs/`, `README.md` |
**Weryfikacja:**
```bash
curl -s http://127.0.0.1:18090/api/status -H "X-API-Key: $KEY"
ss -tlnp | grep 18090 # 127.0.0.1 only
curl -s -o /dev/null -w '%{http_code}\n' http://192.168.100.90:8090/ # connection refused
```
### Faza 2 — Integracja UI w server-ui
| Zadanie | Pliki |
|---------|-------|
| Przenieś UI gpu-fan do zakładki / podstrony | `server-ui/static/gpu-fan.html` lub sekcja w `index.html` |
| Zmień `fetch` na `/api/gpu-fan/...` | JS frontend |
| Proxy backend | `server-ui/app.py``httpx` lub `requests` do agenta |
| Jednolity `API_KEY` dla użytkownika | `.env` server-ui; agent akceptuje ten sam secret z localhost |
| Usuń link `:8090` | `server-ui/static/index.html` |
**Weryfikacja:** Panel gpu-fan w `:8091`, brak potrzeby otwierania :8090.
### Faza 3 — Dockerize server-ui
| Zadanie | Pliki |
|---------|-------|
| `Dockerfile` | `stacks/server-ui/Dockerfile` |
| `compose.yaml` | mount `docker.sock`, repo, env |
| `extra_hosts: host.docker.internal:host-gateway` | compose |
| Opcja: systemd wrapper `docker compose up -d` | `server-ui.service` lub nowy unit |
| Whitelist w `stacks.yaml` — bez zmian | — |
**Weryfikacja:**
```bash
docker compose --profile server-ui up -d
curl -s http://127.0.0.1:8091/api/health
curl -s http://127.0.0.1:8091/api/stacks
```
### Faza 4 — Jeden punkt wejścia (opcjonalnie)
- NPMPlus proxy host `panel.<domena>``127.0.0.1:8091`
- UFW: zamknij 18090, opcjonalnie 8091 poza LAN
- Patrz BACKLOG P1 — rozdział 08 firewall
### Faza 5 — `host-agent` (przyszłość)
Jeden daemon root agregujący:
- gpu-fan (NVML)
- `systemctl` status wybranych unitów (`gpu-fan`, `server-ui`)
- health mountu `/data`
- wersje driver / CUDA
Server-ui woła `/api/host/*` zamiast wielu portów agentów.
---
## 9. API contract — gpu-fan agent (do proxy)
Agent nasłuchuje na `127.0.0.1:18090`. Server-ui mapuje `/api/gpu-fan/{path}``/api/{path}`.
| Endpoint agenta | Metoda | Opis | Auth |
|-----------------|--------|------|------|
| `/api/status` | GET | Temperatura, fan speeds, tryb, krzywa | `X-API-Key` |
| `/api/curve` | GET | Punkty krzywej | `X-API-Key` |
| `/api/curve` | PUT | Zapis krzywej + tryb curve | `X-API-Key` |
| `/api/mode` | POST | `{"mode":"auto\|curve\|manual","speed":100}` | `X-API-Key` |
| `/api/reload` | POST | Przeładuj `curve.json` | `X-API-Key` |
**Bezpieczeństwo agenta:**
- Bind tylko `127.0.0.1` (nie `0.0.0.0`)
- Akceptuj requesty tylko z loopback lub z zaufanego `GPU_FAN_TRUSTED_PROXY` (server-ui IP w docker bridge — opcjonalnie)
- Ten sam `API_KEY` co server-ui lub osobny `AGENT_KEY` w env server-ui (nie w repo)
**Przykład proxy (szkic Python):**
```python
# server-ui/app.py (szkic — nie zaimplementowano)
GPU_FAN_AGENT = os.environ.get("GPU_FAN_AGENT_URL", "http://127.0.0.1:18090")
@app.api_route("/api/gpu-fan/{path:path}", methods=["GET", "PUT", "POST"])
async def proxy_gpu_fan(path: str, request: Request):
require_mutation_auth(request) # lub require_auth dla GET też
url = f"{GPU_FAN_AGENT}/api/{path}"
# forward method, body, X-API-Key header
```
---
## 10. Szkic `compose.yaml` — server-ui (bez implementacji)
```yaml
# stacks/server-ui/compose.yaml — PROPOZYCJA, nie wdrożono
name: server-ui
services:
server-ui:
build: .
container_name: server-ui
profiles: [server-ui]
restart: unless-stopped
ports:
- "${SERVER_UI_PORT:-8091}:8091"
environment:
- SERVER_UI_HOST=0.0.0.0
- SERVER_UI_PORT=8091
- API_KEY=${API_KEY}
- REPO_ROOT=/repo
- GPU_FAN_AGENT_URL=http://host.docker.internal:18090
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ${REPO_ROOT:-/home/tomasz-syn-grzegorza/cursor/ubuntu-bare-metal}:/repo:ro
extra_hosts:
- "host.docker.internal:host-gateway"
```
**Uwagi:**
- `docker compose` w kontenerze wymaga CLI w obrazie lub wywołania Docker API bezpośrednio (obecnie `compose_runner.py` używa subprocess — działa z mounted sock + `docker` CLI w image).
- Ścieżki `compose_dir` w `stacks.yaml` muszą istnieć w `REPO_ROOT` wewnątrz kontenera (`/repo/stacks/...`).
---
## 11. Ryzyka i mitigacje
| Ryzyko | Prawdopodobieństwo | Mitigacja |
|--------|-------------------|-----------|
| Pad server-ui = brak edycji krzywej | Średnie | Daemon gpu-fan **niezależny** — ostatnia krzywa dalej działa |
| Server-ui w Docker nie widzi ścieżek compose | Wysokie przy złym mount | Mount całego repo; test `REPO_ROOT` w healthcheck |
| `host.docker.internal` niedostępny | Niskie na Docker 20.10+ | `extra_hosts: host-gateway` |
| Dwa API key — UX | Średnie | Jeden klucz w UI; proxy dokleja secret do agenta |
| Regresja graceful shutdown fan | Niskie przy zachowaniu systemd | Nie ruszać `fan_controller.shutdown()`; test po `systemctl stop` |
| npmplus + server-ui port conflict | Brak | 8091 vs 80/443/81 |
---
## 12. Anti-patterns (czego unikać)
1. **NVML fan control w kontenerze Docker** — odrzucone w gpu-fan research.
2. **Jeden monolityczny proces root z UI na 0.0.0.0** — obecny gpu-fan; do refaktoru.
3. **Server-ui bez docker.sock a z pretensją do zarządzania kontenerami** — wymaga sock lub Docker API.
4. **Osadzanie ComfyUI w server-ui zamiast linków** — duży scope, CORS, osobne sesje.
5. **Budowa „Proxmox w Dockerze”** — over-engineering dla single-node AI homelab.
6. **Publiczny port agenta 18090** — tylko localhost.
---
## 13. Autentykacja — model docelowy
| Warstwa | Mechanizm |
|---------|-----------|
| Użytkownik → server-ui | `X-API-Key` (mutacje + opcjonalnie odczyt gpu-fan) |
| server-ui → gpu-fan agent | Ten sam key lub `AGENT_KEY` w env server-ui; request z kontenera przez `host.docker.internal` |
| Użytkownik → ComfyUI/LocalAI | Osobne klucze aplikacji (poza server-ui) |
| NPMPlus → server-ui | TLS + opcjonalnie basic auth na proxy host |
Docelowo: **jeden panel, jeden klucz** do operacji serwerowych (start/stop stacków + gpu-fan).
---
## 14. Checklist dla kolejnego agenta
### Research (ten dokument)
- [x] Raport SERVER-UI-ARCHITECTURE-RESEARCH.md
- [x] ADR-001-host-agent-control-plane.md
- [x] Link w BACKLOG.md
### Implementacja (przyszłe PR)
- [ ] Faza 1: gpu-fan API-only na localhost:18090
- [ ] Faza 2: proxy + UI w server-ui
- [ ] Faza 3: Dockerfile + compose server-ui
- [ ] Faza 4: NPMPlus proxy panel
- [ ] Faza 5: host-agent (opcjonalnie)
---
## 15. Podsumowanie jednym akapitem
Przeniesienie **samego UI** gpu-fan do Server UI w Dockerze, przy **daemonie NVML na hoście**, to właściwy plan zgodny z Portainer (UI w kontenerze) i Proxmox (privileged na hoście). Najpierw rozdziel proces gpu-fan na agent localhost i panel, potem dockerizuj server-ui z `docker.sock` i proxy do agenta; w dłuższej perspektywie rozważ jeden `host-agent` zamiast wielu portów. **Nie** przenoś NVML do kontenera.