diff --git a/README.md b/README.md new file mode 100644 index 0000000..afc669e --- /dev/null +++ b/README.md @@ -0,0 +1,140 @@ +# 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// ──► upstream origin +``` + +## Run + +```bash +uv sync +uv run uwudash-mk3 +# http://127.0.0.1:8000 +``` + +Development reload: + +```bash +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` | `/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`: + +```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/ -> / +/proxy// -> / +``` + +So a service with `url = http://internal-host:8080/sub/page` is served at +`/proxy//sub/page`, and any link the upstream page emits to +`http://internal-host:8080/...` or `//internal-host:8080/...` is rewritten to +`/proxy//...`. 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.