mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-27 14:17:10 -04:00
## 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:
parent
b514695efd
commit
c10969873b
6 changed files with 101 additions and 0 deletions
|
|
@ -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+**.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
46
tests/test_cli_dashboard.py
Normal file
46
tests/test_cli_dashboard.py
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue