591 lines
18 KiB
Markdown
591 lines
18 KiB
Markdown
# 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. ✓
|