Add Homepage dashboard + dual-export OpenCode traces

Homepage as the front door: single page at framework:7575 with one tile
per service, live widgets where the upstream supports it (Ollama loaded
models, container state via docker.sock, etc.), bookmarks for reference
docs. Config files are pyinfra-managed — source of truth lives in
compose/homepage/, sync by editing there and re-running ./run.sh.

OpenCode plugin now dual-exports spans to Phoenix and OpenLIT in
parallel. Phoenix remains the per-trace waterfall view; OpenLIT picks
up the same data for fleet-level metrics. Each destination has its own
batch processor so a hiccup at one doesn't block the other.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-08 12:00:05 -04:00
parent f23f7b8cc9
commit 178d7d3c0f
11 changed files with 269 additions and 16 deletions

View File

@@ -42,14 +42,14 @@ Or run it ephemerally without installing: `uvx pyinfra inventory.py deploy.py`.
Requires a reboot to activate — pyinfra rewrites `/etc/default/grub`
and runs `update-grub`, but won't reboot for you.
- `/models/<vendor>/` layout
- `/srv/docker/{llama,vllm,ollama,openwebui,beszel,openlit,phoenix,openhands}/docker-compose.yml`
- `/srv/docker/{llama,vllm,ollama,openwebui,beszel,openlit,phoenix,openhands,homepage}/docker-compose.yml`
dropped in, not auto-started — you edit the model path then
`docker compose up -d`
(OpenWebUI needs no edits — it's pre-configured to find Ollama at
`host.docker.internal:11434` and uses `searxng.n0n.io` for web search)
(Beszel hub at :8090, OpenLIT UI at :3001, Phoenix UI at :6006,
OpenHands UI at :3030 — see "Monitoring stack" and "Agent harnesses"
below)
OpenHands UI at :3030, Homepage at :7575 — see "Monitoring stack",
"Agent harnesses", and "Front door" below)
If a previous run installed the native llama.cpp build / full ROCm /
native Ollama, those are auto-cleaned the next time `./run.sh` runs.
@@ -80,8 +80,27 @@ Top of `deploy.py`:
https://github.com/Umio-Yasuno/amdgpu_top/releases.
Compose images in
`compose/{llama,vllm,ollama,openwebui,beszel,openlit,phoenix,openhands}.yml`
— pin tags here.
`compose/{llama,vllm,ollama,openwebui,beszel,openlit,phoenix,openhands,homepage}.yml`
— pin tags here. Homepage's tile/layout config is in `compose/homepage/`
(`services.yaml`, `settings.yaml`, etc.); edit there, `./run.sh`, restart
the homepage container.
## Front door
- **Homepage** (`/srv/docker/homepage`, http://framework:7575) — single
page with one tile per service in this stack, with native widgets
pulling live state (loaded models from Ollama, container status from
the docker socket, etc.). Bookmarks for the reference docs across the
bottom. Use as the default landing page on this network.
Bring-up: `cd /srv/docker/homepage && docker compose up -d`. No
first-run setup — it reads `/srv/docker/homepage/config/*.yaml` and
renders.
To customize: edit `pyinfra/framework/compose/homepage/*.yaml` in the
repo, run `./run.sh`, then `docker compose restart homepage` on the
box. Direct edits to `/srv/docker/homepage/config/*.yaml` will be
overwritten on the next pyinfra deploy.
## Monitoring stack

View File

@@ -0,0 +1,28 @@
# Homepage — service launcher + status dashboard for the localgenai stack.
# https://gethomepage.dev
#
# Single tile per service, with live widgets (loaded models, container
# state, request rates) where the upstream supports it. Reads its config
# from /srv/docker/homepage/config; pyinfra ships the initial files there
# and leaves them alone afterwards (edit them directly to add bookmarks,
# tweak the layout, etc).
#
# Bring-up: `docker compose up -d`. UI at http://framework:7575.
services:
homepage:
image: ghcr.io/gethomepage/homepage:latest
container_name: homepage
restart: unless-stopped
ports:
# 7575 picked to avoid the soup of 30xx ports already in use
# (OpenWebUI 3000, OpenLIT 3001, OpenHands 3030).
- "7575:3000"
volumes:
- /srv/docker/homepage/config:/app/config
# Read-only docker socket so homepage can render container status
# (running / stopped, CPU, memory) on each tile.
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
# Required when accessed from a host other than localhost (i.e.
# over Tailscale by hostname). Comma-separated list; * matches all.
HOMEPAGE_ALLOWED_HOSTS: "framework:7575,framework,localhost:7575,localhost,*"

View File

@@ -0,0 +1,22 @@
# https://gethomepage.dev/configs/bookmarks/
- Reference:
- Strix Halo wiki:
- abbr: SH
href: https://strixhalo.wiki
- kyuz0 backend benchmarks:
- abbr: KB
href: https://kyuz0.github.io/amd-strix-halo-toolboxes/
- llama.cpp:
- abbr: LC
href: https://github.com/ggml-org/llama.cpp
- OpenCode:
- abbr: OC
href: https://opencode.ai
- Phoenix:
- abbr: PX
href: https://arize.com/docs/phoenix

View File

@@ -0,0 +1,8 @@
# Docker integration. https://gethomepage.dev/configs/docker/
#
# `localhost-docker` is the name services.yaml refers to via
# `server: localhost-docker`. Homepage uses the mounted /var/run/docker.sock
# (read-only) to render container state on each tile.
localhost-docker:
socket: /var/run/docker.sock

View File

@@ -0,0 +1,80 @@
# Service tiles for the localgenai stack. Edit in place — pyinfra
# ships this once and never overwrites.
#
# Widget reference: https://gethomepage.dev/widgets/
- Inference:
- Ollama:
icon: ollama.svg
href: http://framework:11434
description: Local model server (Qwen3-Coder-30B and friends)
server: localhost-docker
container: ollama
widget:
type: ollama
url: http://framework:11434
- llama.cpp:
icon: si-llama
href: http://framework:8080
description: Vulkan-backed llama.cpp server (gfx1151)
server: localhost-docker
container: llama
# No native widget; a ping check confirms liveness.
widget:
type: customapi
url: http://framework:8080/health
refreshInterval: 30000
mappings:
- field: status
label: Status
- vLLM:
icon: mdi-server-network
href: http://framework:8000
description: Batched OpenAI-compatible serving (ROCm)
server: localhost-docker
container: vllm
- Agent UIs:
- OpenWebUI:
icon: open-webui.svg
href: http://framework:3000
description: Chat UI in front of Ollama, with SearXNG search
server: localhost-docker
container: openwebui
- OpenHands:
icon: mdi-robot
href: http://framework:3030
description: Autonomous coding agent (loopback — needs SSH tunnel)
server: localhost-docker
container: openhands
- Observability:
- Beszel:
icon: beszel.svg
href: http://framework:8090
description: Host + container + AMD GPU dashboard
server: localhost-docker
container: beszel
- OpenLIT:
icon: mdi-chart-line-variant
href: http://framework:3001
description: LLM fleet metrics (cost, tokens, latency)
server: localhost-docker
container: openlit
- Phoenix:
icon: arize-phoenix.svg
href: http://framework:6006
description: Per-trace agent waterfall / flamegraph
server: localhost-docker
container: phoenix
- External:
- SearXNG:
icon: searxng.svg
href: https://searxng.n0n.io
description: Self-hosted metasearch (used by OpenWebUI + OpenCode)

View File

@@ -0,0 +1,26 @@
# Top-level dashboard settings. https://gethomepage.dev/configs/settings/
title: localgenai
favicon: https://gethomepage.dev/favicon.ico
theme: dark
color: slate
headerStyle: clean
language: en
target: _blank
# Layout: keep the four service groups in a 3-column grid each so a tile
# fits comfortably without wrapping on a 1440-wide laptop.
layout:
Inference:
style: row
columns: 3
Agent UIs:
style: row
columns: 3
Observability:
style: row
columns: 3
External:
style: row
columns: 3

View File

@@ -0,0 +1,19 @@
# Top-of-page widgets. https://gethomepage.dev/widgets/info/
- search:
provider: custom
url: https://searxng.n0n.io/search?q=
target: _blank
focus: true
- resources:
label: framework
cpu: true
memory: true
disk: /
- datetime:
text_size: xl
format:
timeStyle: short
dateStyle: medium

View File

@@ -338,6 +338,7 @@ for svc in (
"openlit",
"phoenix",
"openhands",
"homepage",
):
files.directory(
name=f"compose/{svc} dir",
@@ -441,6 +442,34 @@ files.directory(
_sudo=True,
)
# Homepage config. The compose loop above only copies homepage.yml; the
# YAML config files live in compose/homepage/ on the source side and at
# /srv/docker/homepage/config/ on the box. Source-of-truth is the repo —
# `./run.sh` syncs the config files. Edits should happen here in the
# repo, not on the box, since pyinfra will overwrite drift.
files.directory(
name="Homepage config dir",
path=f"{COMPOSE_DIR}/homepage/config",
group="docker",
mode="2775",
_sudo=True,
)
for cfg in (
"services.yaml",
"settings.yaml",
"widgets.yaml",
"docker.yaml",
"bookmarks.yaml",
):
files.put(
name=f"homepage config: {cfg}",
src=f"compose/homepage/{cfg}",
dest=f"{COMPOSE_DIR}/homepage/config/{cfg}",
group="docker",
mode="664",
_sudo=True,
)
# --- Cleanup of artifacts from the prior native-build deploy ----------------
# All idempotent — `present=False` is a no-op when the target is absent.