initial import: read-only gufo + vLLM dashboard
Build and Push Container / build-and-push (push) Successful in 38s

Single-file FastAPI proxy plus a no-CDN vanilla-JS page for watching a gufo
server and a vLLM server side by side: one Prometheus parser (class Prom) with
per-engine field maps, measured Δcounter/Δt rates for engines that export no
rate gauge, and strict read-only GET-only routing. Engines are opt-in via
*_BASE_URL — unset means no panel, no poll, and a 404 {"configured": false}
without dialling.

Includes the container image and CI so the dashboard can be deployed as an
image rather than a checkout:

- Containerfile: uv-locked two-stage build on python:3.14-slim, non-root
  (uid 10001), binds 8090 (8000 is vLLM's port in this fleet), healthcheck on
  /api/config so a dead engine never marks the dashboard unhealthy. No frontend
  build stage — static/index.html is bundler-free by design and is copied as-is.
- .gitea/workflows/build-and-push.yml: publishes
  gitea.reeseapps.com/Experimental/llm-monitor on main + release tags.

114 tests, no network and no real engines required.

LLM-Generated: true
This commit is contained in:
2026-10-16 12:49:45 -04:00
commit 48bc92b9af
19 changed files with 6069 additions and 0 deletions
+59
View File
@@ -0,0 +1,59 @@
# syntax=docker/dockerfile:1
# llm_monitor — production image (Podman/Docker compatible).
#
# Stage 1 (python): install the locked dependencies with uv (deps only — the app is
# a single module, there is nothing to build or install).
# Stage 2 (runtime): slim, non-root, no build tools.
#
# There is deliberately NO frontend build stage: static/index.html is hand-rolled vanilla
# JS + inline SVG with no bundler and no CDN (the engines live on an internet-disconnected
# podman network, so a CDN asset would be a dead request and a blank chart). The file is
# copied verbatim and served as-is.
#
# podman build -t localhost/llm-monitor:main -f Containerfile .
# podman run -d --name llm-monitor -p 8090:8090 \
# -e GUFO_BASE_URL=http://deskwork-ai:8731 \
# -e VLLM_BASE_URL=http://driveripper-ai:8000 \
# localhost/llm-monitor:main
#
# Both env vars are optional — unset means this deployment does not run that engine, the
# proxy answers 404 {"configured": false} without dialling, and the page drops the panel.
# Nothing is baked into the image: no host guesses, no keys.
#
# Both stages must stay on the SAME Python (3.14): stage 1 builds the venv that stage 2
# copies verbatim, so a mismatch would ship a venv the runtime interpreter cannot import.
# requires-python is >=3.10; 3.14 is what the box that runs this is on.
# ---------- Stage 1: python dependencies ----------
FROM docker.io/python:3.14-slim AS python
WORKDIR /app
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
ENV UV_LINK_MODE=copy \
UV_PYTHON_DOWNLOADS=0 \
UV_PROJECT_ENVIRONMENT=/app/.venv
# Lockfile first so dependency layers cache independently of the app source.
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-cache
# ---------- Stage 2: runtime ----------
FROM docker.io/python:3.14-slim AS runtime
ENV PATH="/app/.venv/bin:$PATH" \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1
RUN useradd --create-home --uid 10001 monitor
WORKDIR /app
COPY --from=python /app/.venv /app/.venv
COPY app.py ./app.py
COPY static ./static
RUN chown -R monitor:monitor /app
USER monitor
# The dashboard listens on 8090 on purpose: 8000 is vLLM's port in this fleet and 8731 is
# gufo's, so binding either would collide with an engine (or scrape itself).
EXPOSE 8090
ENV DASHBOARD_HOST=0.0.0.0 \
DASHBOARD_PORT=8090
# /api/config answers 200 with no upstream configured and never dials an engine, so a dead
# gufo or vLLM can never make the dashboard container unhealthy.
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD ["python", "-c", "import sys, httpx; sys.exit(0 if httpx.get('http://127.0.0.1:8090/api/config', timeout=4).status_code == 200 else 1)"]
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8090"]