# Docker & Web Frontend Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Containerize Manual Slop for Unraid deployment. Add a web frontend via the imgui-bundle web backend (per the [explorer docs](https://imgui-bundle.pages.dev/explorer/)). Preserve the existing Hook API on :8999 for agent access.

**Architecture:** Python 3.11-slim base image with `uv` for dependency management. The container runs `sloppy.py` in web mode (via a new `--web-host` / `--web-port` arg), which uses the imgui-bundle Hello ImGui web backend to render ImGui to a WebGL canvas in the browser. Two exposed ports: 8080 (web client) and 8999 (hook API). Volumes for project data and app state.

**Tech Stack:** Docker, docker-compose, Python 3.11, imgui-bundle (web backend), FastAPI/Uvicorn (existing, for hooks)

- **[checkpoint: 99a84bd]**

- **No subagents.**
- **Pre-edit checkpoint:** `git add .` before any file edit.
- **Per-file atomic commits.**
- **Commit message format:** `<type>(<scope>): <imperative description>`.
- **Git note format:** 3-8 line rationale per commit.
- **Style baseline:** 1-space indent, no comments, type hints.

---

## File Structure

| File | Action | Responsibility |
|---|---|---|
| `Dockerfile` | Create | Container build for Manual Slop |
| `docker-compose.yml` | Create | Multi-container orchestration for Unraid |
| `scripts/docker_build.sh` | Create | Build helper |
| `scripts/docker_run.sh` | Create | Run helper with env var wiring |
| `sloppy.py` | Modify | Add `--web-host` and `--web-port` args |
| `docs/guide_docker_deployment.md` | Create | Unraid setup guide |
| `tests/test_docker_build.py` | Create | Opt-in Docker build test |
| `pyproject.toml` | Modify | Add `docker` marker |

---

## Task 1: Add web args to `sloppy.py`

**Files:**
- Modify: `sloppy.py`

- [ ] **Step 1.1: Pre-edit checkpoint**

```powershell
git -C C:\projects\manual_slop add .
```

- [ ] **Step 1.2: Read current `sloppy.py`**

Use `manual-slop_read_file` to see current contents.

- [ ] **Step 1.3: Add the new args**

Find the existing argument parser setup and add the web args. If there isn't an argparse, look for how args are handled. Add:

```python
# In sloppy.py, after existing argument definitions
import argparse

parser = argparse.ArgumentParser(description="Manual Slop entry point")
# ... existing args ...
parser.add_argument("--web-host", default=None, help="Enable web mode and bind to this host (e.g., 0.0.0.0)")
parser.add_argument("--web-port", type=int, default=8080, help="Web mode port (default: 8080)")
parser.add_argument("--enable-test-hooks", action="store_true", help="Enable the HookServer on :8999 for external automation")
args = parser.parse_args()
```

If the existing entry point uses a different mechanism, integrate the args there.

- [ ] **Step 1.4: Add the web mode branch**

After argument parsing, before the existing main launch, add:

```python
if args.web_host is not None:
    from imgui_bundle import hello_imgui
    from src.api_hooks import HookServer
    
    if args.enable_test_hooks:
        hook_server = HookServer()
        hook_server.start()
    
    runner_params = hello_imgui.RunnerParams()
    runner_params.app_window_params.window_title = "Manual Slop (Web)"
    runner_params.app_window_params.borderless = True
    runner_params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_docker_space
    runner_params.app_window_params.restore_previous_window_size = True
    
    from src.gui_2 import App
    app = App()
    hello_imgui.run(runner_params, lambda: app.render_frame())
```

- [ ] **Step 1.5: Commit**

```bash
git -C C:\projects\manual_slop add sloppy.py
git -C C:\projects\manual_slop commit -m "feat(sloppy): add --web-host and --web-port args for web mode"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Adds --web-host (enables web mode) and --web-port (default 8080) args. When --web-host is set, launches via Hello ImGui web backend with full-screen docking. Preserves --enable-test-hooks for agent access via :8999." $_ }
```

---

## Task 2: Create the Dockerfile

**Files:**
- Create: `Dockerfile`

- [ ] **Step 2.1: Pre-edit checkpoint**

```powershell
git -C C:\projects\manual_slop add .
```

- [ ] **Step 2.2: Create the Dockerfile**

```dockerfile
FROM python:3.11-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    git curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

RUN pip install uv

WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen

COPY . .

RUN mkdir -p /projects /config
VOLUME ["/projects", "/config"]

EXPOSE 8080 8999

HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
    CMD curl -f http://127.0.0.1:8999/status || exit 1

ENTRYPOINT ["uv", "run", "sloppy.py", "--enable-test-hooks", "--web-host=0.0.0.0", "--web-port=8080"]
```

- [ ] **Step 2.3: Add a `.dockerignore`**

```dockerignore
# .dockerignore
.git
__pycache__
*.pyc
.pytest_cache
.ruff_cache
.venv
.env
tests/artifacts/
tests/logs/
logs/
md_gen/
*.log
```

- [ ] **Step 2.4: Commit**

```bash
git -C C:\projects\manual_slop add Dockerfile .dockerignore
git -C C:\projects\manual_slop commit -m "feat(docker): add Dockerfile and .dockerignore for containerized deployment"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Python 3.11-slim base, uv for dep management, copies pyproject + uv.lock first for layer caching. Exposes 8080 (web) and 8999 (hooks). Volumes for /projects and /config. Healthcheck on hook status. .dockerignore excludes test artifacts and git." $_ }
```

---

## Task 3: Create the docker-compose.yml

**Files:**
- Create: `docker-compose.yml`

- [ ] **Step 3.1: Pre-edit checkpoint**

```powershell
git -C C:\projects\manual_slop add .
```

- [ ] **Step 3.2: Create the compose file**

```yaml
version: '3.8'

services:
  manual_slop:
    build: .
    image: manual_slop:latest
    container_name: manual_slop
    ports:
      - "8999:8999"
      - "8080:8080"
    volumes:
      - /mnt/user/projects:/projects:rw
      - /mnt/user/appdata/manual_slop:/config:rw
    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
```

- [ ] **Step 3.3: Commit**

```bash
git -C C:\projects\manual_slop add docker-compose.yml
git -C C:\projects\manual_slop commit -m "feat(docker): add docker-compose.yml for Unraid deployment"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Two ports (8999 hooks, 8080 web), two volumes (projects, appdata config), env vars for 4 providers, healthcheck on hook status. Unraid-friendly paths." $_ }
```

---

## Task 4: Create the build/run scripts

**Files:**
- Create: `scripts/docker_build.sh`
- Create: `scripts/docker_run.sh`

- [ ] **Step 4.1: Pre-edit checkpoint**

```powershell
git -C C:\projects\manual_slop add .
```

- [ ] **Step 4.2: Create `scripts/docker_build.sh`**

```bash
#!/usr/bin/env bash
# scripts/docker_build.sh
# Build the Manual Slop Docker image.
set -euo pipefail

cd "$(dirname "$0")/.."

docker build -t manual_slop:latest .
```

- [ ] **Step 4.3: Create `scripts/docker_run.sh`**

```bash
#!/usr/bin/env bash
# scripts/docker_run.sh
# Run the Manual Slop container.
set -euo pipefail

cd "$(dirname "$0")/.."

docker compose up -d
```

- [ ] **Step 4.4: Make scripts executable and commit**

```bash
git -C C:\projects\manual_slop add scripts/docker_build.sh scripts/docker_run.sh
git -C C:\projects\manual_slop commit -m "feat(docker): add build and run shell scripts"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Two thin shell scripts: build (docker build -t manual_slop:latest .) and run (docker compose up -d). Will be made executable in chmod step during deployment." $_ }
```

---

## Task 5: Add the `docker` marker to `pyproject.toml`

**Files:**
- Modify: `pyproject.toml`

- [ ] **Step 5.1: Pre-edit checkpoint**

```powershell
git -C C:\projects\manual_slop add .
```

- [ ] **Step 5.2: Add the marker**

Find the markers list (from Task 1 of the clean-install plan) and add:

```toml
markers = [
    "integration: integration tests requiring live GUI",
    "strict: tests that require strict mode",
    "clean_install: clean install verification (opt-in via RUN_CLEAN_INSTALL_TEST=1)",
    "docker: docker build and run test (opt-in via RUN_DOCKER_TEST=1)",
]
```

- [ ] **Step 5.3: Commit**

```bash
git -C C:\projects\manual_slop add pyproject.toml
git -C C:\projects\manual_slop commit -m "test(pytest): add docker marker"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Registers the docker marker. Tests using Docker can be filtered with -m docker or -m 'not docker'." $_ }
```

---

## Task 6: Write the Docker build test

**Files:**
- Create: `tests/test_docker_build.py`

- [ ] **Step 6.1: Pre-edit checkpoint**

```powershell
git -C C:\projects\manual_slop add .
```

- [ ] **Step 6.2: Create the test file**

```python
# tests/test_docker_build.py
import os
import shutil
import subprocess
import time

import pytest
import requests


IMAGE_NAME = "manual_slop:test"
CONTAINER_NAME = "manual_slop_test_container"
WEB_PORT = 18080
HOOK_PORT = 18999


@pytest.mark.docker
def test_docker_image_builds(tmp_path):
    """Build the Docker image. Slow; opt-in."""
    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 in this environment")

    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    result = subprocess.run(
        ["docker", "build", "-t", IMAGE_NAME, "."],
        cwd=repo_root,
        capture_output=True, text=True, timeout=600,
    )
    assert result.returncode == 0, f"Docker build failed: {result.stderr}"


@pytest.mark.docker
def test_docker_container_starts_and_responds():
    """Run the container, verify web and hook endpoints respond."""
    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 in this environment")

    _cleanup_container()

    repo_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
    result = subprocess.run(
        [
            "docker", "run", "-d",
            "--name", CONTAINER_NAME,
            "-p", f"{WEB_PORT}:8080",
            "-p", f"{HOOK_PORT}:8999",
            IMAGE_NAME,
        ],
        cwd=repo_root,
        capture_output=True, text=True, timeout=30,
    )
    assert result.returncode == 0, f"Docker run failed: {result.stderr}"

    try:
        start = time.time()
        ready = False
        while time.time() - start < 90:
            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 hook API did not respond within 90s"

        r = requests.get(f"http://127.0.0.1:{WEB_PORT}/", timeout=5)
        assert r.status_code == 200
        body = r.content.lower()
        assert b"<html" in body or b"<!doctype" in body, "Web endpoint did not return HTML"

    finally:
        _cleanup_container()


def _docker_available() -> bool:
    return shutil.which("docker") is not None


def _cleanup_container() -> None:
    subprocess.run(
        ["docker", "rm", "-f", CONTAINER_NAME],
        capture_output=True,
    )
```

- [ ] **Step 6.3: Run in skip mode**

```powershell
uv run pytest tests/test_docker_build.py -v
```

Expected: 2 skipped.

- [ ] **Step 6.4: Commit**

```bash
git -C C:\projects\manual_slop add tests/test_docker_build.py
git -C C:\projects\manual_slop commit -m "test(docker): add opt-in build and container-run tests"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Two opt-in tests: build image (RUN_DOCKER_TEST=1), run container and verify hook + web endpoints. Skipped by default. Docker daemon required." $_ }
```

---

## Task 7: Write the Unraid deployment guide

**Files:**
- Create: `docs/guide_docker_deployment.md`

- [ ] **Step 7.1: Pre-edit checkpoint**

```powershell
git -C C:\projects\manual_slop add .
```

- [ ] **Step 7.2: Create the guide**

```markdown
# Docker Deployment Guide (Unraid)

[Top](../README.md) | [Architecture](guide_architecture.md) | [Tools & IPC](guide_tools.md)

---

## Overview

This guide covers deploying Manual Slop on Unraid (or any Docker host) using the containerized image. The deployment provides:
- A web-accessible ImGui GUI (browser-based, no local display required)
- The Hook API on `:8999` for agent access
- Persistent volumes for projects and app state

## Prerequisites

- Unraid 6.10+ (or any Docker host with compose support)
- A project share mounted at `/mnt/user/projects` (or edit `docker-compose.yml` to match your path)
- API keys for the providers you want to use

## Building the Image

From the repo root:
```bash
docker build -t manual_slop:latest .
```

Or use the helper:
```bash
./scripts/docker_build.sh
```

## Running the Container

Edit `docker-compose.yml` to set your volume paths and provider keys (via `.env` file or environment).

```bash
# Create a .env file with your API keys
cat > .env <<EOF
GEMINI_API_KEY=your-key-here
ANTHROPIC_API_KEY=your-key-here
DEEPSEEK_API_KEY=your-key-here
MINIMAX_API_KEY=your-key-here
EOF

# Start the container
docker compose up -d
```

## Accessing the GUI

Open a browser and navigate to:
- `http://<your-unraid-ip>:8080` for the web client
- `http://<your-unraid-ip>:8999/status` for the hook API health check

The web client renders the ImGui panels via WebGL. The Hello ImGui web backend streams frame deltas over WebSocket.

## Agent Access

Agents interact with the running container via the Hook API on `:8999`. Examples:

```bash
# Check status
curl http://<your-unraid-ip>:8999/status

# Get MMA state
curl http://<your-unraid-ip>:8999/api/gui/mma_status
```

See [guide_tools.md](guide_tools.md) for the full Hook API reference.

## Volumes

- `/projects` — Mounted from `/mnt/user/projects` by default. Your project workspaces live here. The `manual_slop.toml` per project is in this directory.
- `/config` — Mounted from `/mnt/user/appdata/manual_slop` by default. App state: presets, personas, log directory, workspace profiles.

## Updating

```bash
git pull
docker build -t manual_slop:latest .
docker compose up -d
```

## Backup

Back up `/config` to preserve presets, personas, and workspace profiles. Back up `/projects/<project>/conductor/` to preserve track history.

## Troubleshooting

- **Port conflicts:** Edit `docker-compose.yml` to change the host port (e.g., `"18080:8080"` to use 18080 on the host).
- **Permission errors:** Ensure the Unraid share has write permissions for the container's UID.
- **Hook API not responding:** Check `docker logs manual_slop` for the startup output. The hook server should log "HookServer started on :8999".
```

- [ ] **Step 7.3: Commit**

```bash
git -C C:\projects\manual_slop add docs/guide_docker_deployment.md
git -C C:\projects\manual_slop commit -m "docs(docker): add Unraid deployment guide"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Unraid-specific setup guide: prerequisites, build, run via compose, web access URLs, agent access via hook API, volumes, updating, backup, troubleshooting." $_ }
```

---

## Task 8: Phase Completion Verification

- [ ] **Step 8.1: Verify all files exist and are syntactically valid**

```powershell
# Python syntax
python -c "import ast; ast.parse(open('sloppy.py').read())"

# YAML syntax (if PyYAML available)
python -c "import yaml; yaml.safe_load(open('docker-compose.yml').read())"

# Dockerfile syntax (if hadolint available)
# hadolint Dockerfile || echo "hadolint not installed, skipping"

# Markdown lint (skip if not configured)
```

- [ ] **Step 8.2: Run the test suite in skip mode**

```powershell
uv run pytest tests/test_docker_build.py -v
```

Expected: 2 skipped.

- [ ] **Step 8.3: If Docker is available, run the tests**

```powershell
RUN_DOCKER_TEST=1 uv run pytest tests/test_docker_build.py -v
```

- [ ] **Step 8.4: Create the checkpoint commit**

```bash
git -C C:\projects\manual_slop commit --allow-empty -m "conductor(checkpoint): Docker & web frontend complete"
git -C C:\projects\manual_slop log -1 --format='%H' | ForEach-Object { git -C C:\projects\manual_slop notes add -m "Track complete. Dockerfile, docker-compose.yml, build/run scripts, --web-host arg, Unraid deployment guide, opt-in Docker build test. imgui-bundle web backend integration pending Hello ImGui runner config tuning." $_ }
```

---

## Self-Review

- **Spec coverage:** All design sections have tasks. ✓
- **Placeholder scan:** All code blocks are complete. ✓
- **Type consistency:** Args named consistently (`--web-host`, `--web-port`, `--enable-test-hooks`). ✓
- **Risk acknowledged:** The web backend integration is experimental and may need iteration after testing. The checkpoint note flags this. ✓
