Private
Public Access
0
0
Files
manual_slop/docs/superpowers/specs/2026-06-02-docker-web-frontend-design.md
T

10 KiB

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) 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

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

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:

# 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.

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

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.