Files
uwudash_mk3/README.md
T

4.5 KiB

UwuDash MK3

A single Python app that is both the frontend and the backend for a dashboard of internal services. The sidebar has one button per service; clicking it loads the service in an iframe through the dashboard's built-in reverse proxy, so the upstream services never need to be public.

browser ──► uwudash (host:port) ──► /proxy/<id>/<path> ──► upstream origin

Run

uv sync
uv run uwudash-mk3
# http://127.0.0.1:8000

Development reload:

uv run uwudash-mk3 --reload

Command line flags

Flag Default Meaning
--host 127.0.0.1 bind address
--port 8000 bind port
--data-dir data directory holding services.json
--services-file <data-dir>/services.json explicit store path
--reload off uvicorn reload on source changes

Environment variables

Variable Default Meaning
UWUDASH_HOST 127.0.0.1 bind address
UWUDASH_PORT 8000 bind port
UWUDASH_DATA_DIR data data directory
UWUDASH_SERVICES_FILE data/services.json services store path
UWUDASH_PROXY_TIMEOUT 30.0 upstream request timeout (seconds)
UWUDASH_STRIP_FRAME_OPTIONS true strip X-Frame-Options and CSP frame-ancestors from proxied responses

CLI flags override environment variables.

Services

Services live in data/services.json:

{
  "version": 1,
  "services": [
    {
      "id": "grafana",
      "name": "Grafana",
      "url": "http://10.6.9.199:3000/dashboards",
      "description": "Metrics dashboards",
      "icon": "G",
      "open_in": "iframe",
      "enabled": true,
      "headers": { "Authorization": "Bearer ..." }
    }
  ]
}
  • id is generated from the name (slugified, unique) and stays stable across edits.
  • url may include a path; that path becomes the landing path of the proxy.
  • open_in is iframe (default) or newtab.
  • headers are injected into every proxied request to that service.
  • enabled: false keeps the entry in the sidebar but the proxy returns 403.

Edit them from the Settings page in the UI, or via the API.

How the proxy works

Only the upstream origin (scheme://host:port) is mapped onto the dashboard:

/proxy/<id>            -> <origin>/
/proxy/<id>/<path>     -> <origin>/<path>

So a service with url = http://internal-host:8080/sub/page is served at /proxy/<id>/sub/page, and any link the upstream page emits to http://internal-host:8080/... or //internal-host:8080/... is rewritten to /proxy/<id>/.... Relative links need no rewriting.

Details:

  • Location headers on 3xx responses are rewritten back onto the proxy prefix; redirects are not followed, so the browser stays on the proxy.
  • text/html responses are buffered and rewritten; everything else (JSON, CSS, images, downloads) is streamed through unchanged.
  • X-Frame-Options and CSP frame-ancestors are stripped from proxied responses so the iframe can render (disable with UWUDASH_STRIP_FRAME_OPTIONS=false).
  • Hop-by-hop request/response headers are dropped; per-service headers are merged into the upstream request.
  • Request bodies are buffered with a 10 MB cap (413 above it).
  • WebSocket upgrades are rejected with 400 - the proxy is HTTP only.

API

Method Path Purpose
GET /api/services list services
POST /api/services create
GET /api/services/{id} read
PATCH / PUT /api/services/{id} update
DELETE /api/services/{id} delete
GET /api/services/{id}/health upstream reachability check
GET /api/config effective config
GET /health dashboard health
ANY /proxy/{id} and /proxy/{id}/{path} reverse proxy

Layout

src/uwudash_mk3/
  config.py      env-driven Config dataclass
  models.py      Service / ServiceInput / ServicePatch + URL helpers
  store.py       JSON file store (atomic writes, lock)
  proxy.py       reverse proxy: header filtering, HTML/redirect rewriting
  api.py         /api routes
  app.py         FastAPI factory, static mount, proxy routes
  main.py        CLI entrypoint
  templates/     index.html
  static/        styles.css, app.js

Limitations

  • No authentication on the dashboard itself - it is meant for a trusted LAN.
  • No WebSocket proxying.
  • Upstream cookies work, but they are scoped to the dashboard origin, so cross-service cookie sharing is possible if two services share a proxy host.