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 ..." }
}
]
}
idis generated from the name (slugified, unique) and stays stable across edits.urlmay include a path; that path becomes the landing path of the proxy.open_inisiframe(default) ornewtab.headersare injected into every proxied request to that service.enabled: falsekeeps the entry in the sidebar but the proxy returns403.
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:
Locationheaders on3xxresponses are rewritten back onto the proxy prefix; redirects are not followed, so the browser stays on the proxy.text/htmlresponses are buffered and rewritten; everything else (JSON, CSS, images, downloads) is streamed through unchanged.X-Frame-Optionsand CSPframe-ancestorsare stripped from proxied responses so the iframe can render (disable withUWUDASH_STRIP_FRAME_OPTIONS=false).- Hop-by-hop request/response headers are dropped; per-service
headersare merged into the upstream request. - Request bodies are buffered with a 10 MB cap (
413above 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.