289 lines
10 KiB
Markdown
289 lines
10 KiB
Markdown
# Docker Container & Web-Hosted ImGui Frontend
|
|
|
|
**Date:** 2026-06-02
|
|
**Status:** Draft (pending review)
|
|
**Reference:** https://imgui-bundle.pages.dev/explorer/ — imgui-bundle web backend via Hello ImGui
|
|
|
|
---
|
|
|
|
## Context & Motivation
|
|
|
|
The user wants to deploy Manual Slop on Unraid (a home server OS) and access the GUI via a web browser. The goal is for agents to operate on projects hosted on the home server, with the user monitoring/controlling via web browser.
|
|
|
|
Current state:
|
|
- `sloppy.py` is a desktop GUI (ImGui via imgui-bundle + Python)
|
|
- `src/api_hooks.py` provides a FastAPI/Uvicorn headless service on `:8999` for external automation
|
|
- The app is Windows-oriented (PowerShell subprocesses, `pywin32` for window frame)
|
|
|
|
Target state:
|
|
- Docker container with the full app
|
|
- Web browser shows the ImGui GUI in real-time
|
|
- Agents can interact via the existing Hook API on `:8999`
|
|
- The user's Unraid server can host multiple project directories
|
|
|
|
imgui-bundle's web backend ([reference](https://imgui-bundle.pages.dev/explorer/)) uses a server-side render with a client-side WebGL display. The Hello ImGui runner pairs a Python render loop with a JavaScript WebGL canvas via WebSocket.
|
|
|
|
---
|
|
|
|
## Scope
|
|
|
|
### In Scope
|
|
|
|
- `Dockerfile` — Container build for the Manual Slop app
|
|
- `docker-compose.yml` — Multi-container deployment for Unraid
|
|
- `scripts/docker_build.sh` — Build helper
|
|
- `scripts/docker_run.sh` — Run helper with env var wiring
|
|
- `docs/guide_docker_deployment.md` — Unraid setup guide
|
|
- `tests/test_docker_build.py` — Opt-in Docker build test
|
|
|
|
### Out of Scope
|
|
|
|
- Migrating `sloppy.py` to use the imgui-bundle web backend (the web backend is an alternative to the desktop backend; switching is a significant refactor and may be deferred)
|
|
- Multi-user authentication (single-user deployment)
|
|
- Cloud-specific deployment (AWS, GCP) — Unraid is the target
|
|
- TLS termination (assumed handled by a reverse proxy like Traefik or Caddy)
|
|
|
|
---
|
|
|
|
## Design
|
|
|
|
### Architecture: V2 — Server-side Python + WebGL client (via WebSocket)
|
|
|
|
```
|
|
┌─────────────────────────────────────────────┐
|
|
│ Docker Container (unraid:manual_slop:latest) │
|
|
│ │
|
|
│ ┌────────────────────────────────────┐ │
|
|
│ │ Python app │ │
|
|
│ │ - ImGui renders to framebuffer │ │
|
|
│ │ - Hello ImGui web backend: │ │
|
|
│ │ - Python: render loop │ │
|
|
│ │ - WebSocket: frame deltas │ │
|
|
│ │ - HTTP: serves JS client │ │
|
|
│ │ - HookServer on :8999 │ │
|
|
│ └────────────────────────────────────┘ │
|
|
│ │
|
|
│ Exposed ports: │
|
|
│ - 8080: Web client (HTTP + WS) │
|
|
│ - 8999: Hook API │
|
|
│ │
|
|
│ Volumes: │
|
|
│ - /projects: project workspaces │
|
|
│ - /config: app state, presets, personas │
|
|
└─────────────────────────────────────────────┘
|
|
↑ ↑
|
|
│ Browser (Chrome, Firefox) │ Agent (curl, scripts)
|
|
│ WebSocket for live frames │ HTTP for state
|
|
```
|
|
|
|
### Dockerfile
|
|
|
|
```dockerfile
|
|
FROM python:3.11-slim
|
|
|
|
# System deps
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
git curl ca-certificates \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Install uv
|
|
RUN pip install uv
|
|
|
|
# App setup
|
|
WORKDIR /app
|
|
COPY pyproject.toml uv.lock ./
|
|
RUN uv sync --frozen
|
|
|
|
COPY . .
|
|
|
|
# Volumes
|
|
RUN mkdir -p /projects /config
|
|
VOLUME ["/projects", "/config"]
|
|
|
|
# Expose
|
|
EXPOSE 8080 8999
|
|
|
|
# Health check
|
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
|
CMD curl -f http://127.0.0.1:8999/status || exit 1
|
|
|
|
# Entrypoint
|
|
ENTRYPOINT ["uv", "run", "sloppy.py", "--enable-test-hooks", "--web-host=0.0.0.0", "--web-port=8080"]
|
|
```
|
|
|
|
### `docker-compose.yml`
|
|
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
manual_slop:
|
|
build: .
|
|
image: manual_slop:latest
|
|
container_name: manual_slop
|
|
ports:
|
|
- "8999:8999" # Hook API (host)
|
|
- "8080:8080" # Web client (host)
|
|
volumes:
|
|
- /mnt/user/projects:/projects:rw # Unraid project share
|
|
- /mnt/user/appdata/manual_slop:/config:rw # App state
|
|
environment:
|
|
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
|
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
|
- DEEPSEEK_API_KEY=${DEEPSEEK_API_KEY}
|
|
- MINIMAX_API_KEY=${MINIMAX_API_KEY}
|
|
restart: unless-stopped
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://127.0.0.1:8999/status"]
|
|
interval: 30s
|
|
timeout: 5s
|
|
retries: 3
|
|
start_period: 30s
|
|
```
|
|
|
|
### Entry Point Changes (`sloppy.py`)
|
|
|
|
The current `sloppy.py` launches the desktop GUI. For web mode, we need:
|
|
|
|
```python
|
|
# In sloppy.py
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser()
|
|
# ... existing args ...
|
|
parser.add_argument("--web-host", default=None, help="Enable web mode and bind to this host")
|
|
parser.add_argument("--web-port", type=int, default=8080, help="Web mode port")
|
|
args = parser.parse_args()
|
|
|
|
if args.web_host is not None:
|
|
from imgui_bundle import hello_imgui
|
|
runner_params = hello_imgui.RunnerParams()
|
|
runner_params.app_window_params.borderless = False
|
|
runner_params.imgui_window_params.default_imgui_window_type = ... # web backend
|
|
hello_imgui.run(runner_params)
|
|
else:
|
|
# Existing desktop launch
|
|
...
|
|
```
|
|
|
|
The imgui-bundle web backend is selected by the Hello ImGui runner. The exact config is per the [imgui-bundle explorer docs](https://imgui-bundle.pages.dev/explorer/).
|
|
|
|
### `docs/guide_docker_deployment.md`
|
|
|
|
A complete Unraid setup guide:
|
|
|
|
- Prerequisites (Unraid version, Docker template)
|
|
- Building the image
|
|
- Configuring volumes and env vars
|
|
- Accessing the web client (URL, browser requirements)
|
|
- Agent interaction examples (curl, Python script)
|
|
- Backup and restore of /config
|
|
- Updating the image
|
|
|
|
### `tests/test_docker_build.py`
|
|
|
|
```python
|
|
import os
|
|
import subprocess
|
|
import time
|
|
|
|
import pytest
|
|
import requests
|
|
|
|
IMAGE_NAME = "manual_slop:test"
|
|
CONTAINER_NAME = "manual_slop_test"
|
|
WEB_PORT = 8080
|
|
HOOK_PORT = 8999
|
|
|
|
|
|
@pytest.mark.docker
|
|
def test_docker_container_starts_and_serves(tmp_path):
|
|
"""Build the Docker image, run the container, verify web client + hook API."""
|
|
if os.environ.get("RUN_DOCKER_TEST") != "1":
|
|
pytest.skip("Set RUN_DOCKER_TEST=1 to enable")
|
|
|
|
if not _docker_available():
|
|
pytest.skip("Docker not available")
|
|
|
|
# Build
|
|
result = subprocess.run(
|
|
["docker", "build", "-t", IMAGE_NAME, "."],
|
|
capture_output=True, text=True, timeout=300,
|
|
)
|
|
assert result.returncode == 0, f"Docker build failed: {result.stderr}"
|
|
|
|
# Run
|
|
subprocess.run(["docker", "rm", "-f", CONTAINER_NAME], capture_output=True)
|
|
result = subprocess.run([
|
|
"docker", "run", "-d",
|
|
"--name", CONTAINER_NAME,
|
|
"-p", f"{WEB_PORT}:8080",
|
|
"-p", f"{HOOK_PORT}:8999",
|
|
IMAGE_NAME,
|
|
], capture_output=True, text=True, timeout=30)
|
|
assert result.returncode == 0, f"Docker run failed: {result.stderr}"
|
|
|
|
try:
|
|
# Wait for hook API
|
|
start = time.time()
|
|
ready = False
|
|
while time.time() - start < 60:
|
|
try:
|
|
r = requests.get(f"http://127.0.0.1:{HOOK_PORT}/status", timeout=1)
|
|
if r.status_code == 200:
|
|
ready = True
|
|
break
|
|
except (requests.ConnectionError, requests.Timeout):
|
|
pass
|
|
time.sleep(1)
|
|
|
|
assert ready, "Container did not start hook API within 60s"
|
|
|
|
# Verify web client is served
|
|
r = requests.get(f"http://127.0.0.1:{WEB_PORT}/", timeout=5)
|
|
assert r.status_code == 200
|
|
assert b"<html" in r.content.lower() or b"<!doctype" in r.content.lower()
|
|
|
|
finally:
|
|
subprocess.run(["docker", "rm", "-f", CONTAINER_NAME], capture_output=True)
|
|
|
|
|
|
def _docker_available() -> bool:
|
|
result = subprocess.run(["docker", "version"], capture_output=True)
|
|
return result.returncode == 0
|
|
```
|
|
|
|
---
|
|
|
|
## File Structure
|
|
|
|
- `Dockerfile` — NEW
|
|
- `docker-compose.yml` — NEW
|
|
- `scripts/docker_build.sh` — NEW
|
|
- `scripts/docker_run.sh` — NEW
|
|
- `docs/guide_docker_deployment.md` — NEW
|
|
- `tests/test_docker_build.py` — NEW
|
|
- `sloppy.py` — MODIFY: add `--web-host` and `--web-port` args
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
- `docker build -t manual_slop:latest .` succeeds on a clean machine
|
|
- `docker compose up` starts the container, and `:8999/status` returns 200 within 60s
|
|
- `curl http://localhost:8080/` returns the web client HTML
|
|
- An agent can `curl http://localhost:8999/api/gui/mma_status` and get a valid response
|
|
- The user can navigate to the web UI in a browser and see the ImGui panels
|
|
- File operations on `/projects` persist across container restarts
|
|
- Env vars for API keys are not committed to the image (use runtime env)
|
|
|
|
---
|
|
|
|
## Risks
|
|
|
|
1. **imgui-bundle web backend maturity:** The web backend is less battle-tested than the desktop backend. There may be rendering quirks, input latency, or unsupported features. Mitigation: this is experimental; expect to iterate.
|
|
2. **Headless rendering in container:** Some ImGui features (e.g., font hinting) may need extra config for headless rendering. Mitigation: test early in development; fall back to Xvfb + noVNC if web backend is too immature.
|
|
3. **WebSocket bandwidth:** Streaming frame deltas requires consistent network. On flaky networks, the user experience degrades. Mitigation: implement client-side prediction or reduce frame rate.
|
|
4. **Container size:** Python + uv + all deps can produce a 1-2GB image. Mitigation: use multi-stage builds; pin Python deps for reproducibility.
|
|
5. **Unraid-specific quirks:** Unraid uses a specific Docker storage driver and may have path mapping edge cases. Mitigation: test on the actual Unraid deployment; document the path mapping clearly.
|