feat(cli): add headroom dashboard and surface the dashboard URL (#1277) (#1292)

## Description

The savings dashboard is served at `GET /dashboard`
(`headroom/proxy/server.py`) but was
effectively undiscoverable: there was no `headroom dashboard` command,
the `wrap` startup banner
only printed `Proxy ready on http://127.0.0.1:PORT` (never the dashboard
URL), and the docs
buried it — so users on current releases didn't know it existed (#1277).
This makes it
discoverable from the CLI, the wrap banner, and the docs.

Closes #1277

## Type of Change

- [x] New feature (non-breaking change that adds functionality)

## Changes Made

- `headroom/cli/proxy.py`: new `headroom dashboard` command — prints
`http://127.0.0.1:<port>/dashboard` and opens it in a browser (stdlib
`webbrowser`); `--no-open`
just prints, `--port`/`HEADROOM_PORT` honored. Headless failures are
swallowed (URL already
  printed).
- `headroom/cli/wrap.py`: print the dashboard URL alongside "Proxy
ready" so every `wrap` surfaces
  it.
- `docs/content/docs/installation.mdx` + `README.md`: document `headroom
dashboard`.
- `docs/content/docs/mcp.mdx`: document the Codex MCP `command:
"headroom"` PATH pitfall (#768) —
a project-venv (`uv add`) install isn't on the host's PATH; install
globally with
  `uv tool install` / pipx, or use an absolute path.
- `tests/test_cli_dashboard.py`: new tests.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [x] New tests added for new functionality

### Test Output

```text
$ python -m pytest tests/test_cli_dashboard.py -q
3 passed

$ python -m ruff check headroom/cli/proxy.py headroom/cli/wrap.py tests/test_cli_dashboard.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows 11, Python 3.13, branch
fix/1277-dashboard-discoverability off
  headroomlabs-ai/main
- Exact command / steps: built the CLI and invoked the new command via
the real entry-point import
(`from headroom.cli.main import main;
main(['dashboard','--no-open','--port','8787'],
standalone_mode=False)`) and checked it is registered (`'dashboard' in
main.commands`).
- Observed result: prints ` Dashboard: http://127.0.0.1:8787/dashboard`,
`'dashboard' in
main.commands` → `True`, exit 0. The three new tests pass (prints URL +
no browser on `--no-open`;
opens the URL by default; a raising `webbrowser.open` does not crash the
command).
- Not tested: did not load the rendered `/dashboard` HTML against a live
proxy in CI — the change
only adds a launcher/printer for the existing route; the route itself is
unchanged.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Parideboy 2026-06-23 02:05:38 +02:00 committed by GitHub
parent b514695efd
commit c10969873b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 101 additions and 0 deletions

View file

@ -98,6 +98,7 @@ headroom proxy --port 8787 # drop-in proxy, zero code changes
# 3 — See the savings
headroom perf
headroom dashboard # live savings dashboard (proxy must be running)
```
Granular extras: `[proxy]`, `[mcp]`, `[ml]`, `[code]`, `[memory]`, `[relevance]`, `[image]`, `[agno]`, `[langchain]`, `[evals]`, `[pytorch-mps]` (Apple-GPU memory-embedder offload — set `HEADROOM_EMBEDDER_RUNTIME=pytorch_mps`). Requires **Python 3.10+**.

View file

@ -271,6 +271,18 @@ Some tests require Rust tooling.
The recommended way to install rust is using `rustup`. You can find the official installation instructions [here](https://rust-lang.org/tools/install/).
## Dashboard
Headroom serves a live savings dashboard while the proxy is running. Open it with:
```bash
headroom dashboard # opens http://localhost:8787/dashboard in your browser
headroom dashboard --no-open # just print the URL
```
Or browse to `http://localhost:8787/dashboard` directly (use `--port` / `HEADROOM_PORT` if you
run the proxy on a different port).
## Next steps
<Cards>

View file

@ -154,6 +154,21 @@ For multiple proxy instances, register one stdio MCP server per proxy URL:
Do not assume that a running proxy exposes an HTTP MCP endpoint at `/mcp`. If `http://127.0.0.1:<port>/mcp` returns `404`, use the stdio configuration above.
### `command: "headroom"` fails to start
The configurations above use `"command": "headroom"`, which only works if the `headroom`
executable is on the PATH your MCP host (Codex, etc.) sees at startup. If you installed Headroom
into a project virtualenv — for example with `uv add headroom-ai` — the CLI lives only inside
that venv, and the host fails at launch with:
```text
MCP client for `headroom` failed to start: MCP startup failed: No such file or directory (os error 2)
```
Install Headroom so it's globally on PATH — `uv tool install "headroom-ai[mcp]"` (or
`pipx install "headroom-ai[mcp]"`) — or replace `"headroom"` with the absolute path to the binary
(`command -v headroom`, or `where headroom` on Windows).
## Cross-tool compatibility
| Tool | MCP Support | Setup |

View file

@ -90,6 +90,32 @@ def _selected_context_tool() -> str:
return raw
@main.command()
@click.option(
"--port",
"-p",
default=8787,
type=int,
envvar="HEADROOM_PORT",
help="Proxy port (default: 8787, env: HEADROOM_PORT)",
)
@click.option("--no-open", is_flag=True, help="Print the URL instead of opening a browser")
def dashboard(port: int, no_open: bool) -> None:
"""Open the Headroom savings dashboard in your browser.
Requires a running proxy (start one with `headroom proxy` or `headroom wrap ...`).
"""
import webbrowser
url = f"http://127.0.0.1:{port}/dashboard"
click.echo(f" Dashboard: {url}")
if not no_open:
try:
webbrowser.open(url)
except Exception: # noqa: BLE001 — headless/no browser: URL already printed
pass
@main.command()
@click.option(
"--host",

View file

@ -2506,6 +2506,7 @@ def _ensure_proxy(
),
)
click.echo(f" Proxy ready on http://127.0.0.1:{port}")
click.echo(f" Dashboard: http://127.0.0.1:{port}/dashboard")
return proc
except RuntimeError as e:
click.echo(f" Error: {e}")

View file

@ -0,0 +1,46 @@
"""Tests for the `headroom dashboard` command (#1277)."""
from __future__ import annotations
import webbrowser
from click.testing import CliRunner
from headroom.cli.main import main
def test_dashboard_no_open_prints_url(monkeypatch):
"""--no-open prints the dashboard URL and never opens a browser."""
opened: list[str] = []
monkeypatch.setattr(webbrowser, "open", lambda u, *a, **k: opened.append(u) or True)
result = CliRunner().invoke(main, ["dashboard", "--no-open", "--port", "9999"])
assert result.exit_code == 0, result.output
assert "http://127.0.0.1:9999/dashboard" in result.output
assert opened == [] # browser must not be launched
def test_dashboard_opens_browser_by_default(monkeypatch):
"""Without --no-open the command opens the URL in a browser."""
opened: list[str] = []
monkeypatch.setattr(webbrowser, "open", lambda u, *a, **k: opened.append(u) or True)
result = CliRunner().invoke(main, ["dashboard", "--port", "1234"])
assert result.exit_code == 0, result.output
assert opened == ["http://127.0.0.1:1234/dashboard"]
def test_dashboard_browser_failure_is_swallowed(monkeypatch):
"""A headless box where webbrowser.open raises must not crash the command."""
def _boom(*_a, **_k):
raise RuntimeError("no display")
monkeypatch.setattr(webbrowser, "open", _boom)
result = CliRunner().invoke(main, ["dashboard"])
assert result.exit_code == 0, result.output
assert "/dashboard" in result.output