Commit graph

7 commits

Author SHA1 Message Date
Rod Boev
78591545ce
fix: publish headroom-opencode in release workflow (#2372)
## Description

`headroom-opencode` is documented as an npm package, but the release
workflow never published it, so installs failed with a registry 404 even
though the plugin source already lived under `plugins/opencode`. This
wires the existing package into the npm release path, keeps its version
synced with root releases, and adds release guards for the new package.
Closes #76.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [x] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- added `headroom-opencode` to the npm release workflow, including
release-version stamping and `headroom-ai` dependency rewrite before
publish
- added `plugins/opencode/package.json` to release-please and local
version-sync guards
- synced the source opencode package version to the current release line
and documented the new npm package in the release docs
- added focused release workflow and version-sync tests for the opencode
package
- aligned the two failing dashboard Playwright tests with the current
Session/Lifetime split and `/stats-lifetime` fixture contract

## Testing

- [x] Unit tests pass (`uv run pytest scripts/tests/test_version_sync.py
-q`, `uv run pytest tests/test_release_workflows.py -q -k
'publish_npm_rewrites_opencode_dependency_after_version_and_before_publish
or opencode_source_dependency_matches_lockfile_registry_range or
release_please_manifest_config_consistency'`)
- [x] Unit tests pass (`uv run pytest
tests/test_dashboard_cache_lifetime_playwright.py
tests/test_dashboard_cache_ttl_playwright.py -q`)
- [x] Linting passes (`uv run ruff check scripts/verify-versions.py
scripts/version-sync.py scripts/tests/test_version_sync.py
tests/test_release_workflows.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
$ uv run pytest scripts/tests/test_version_sync.py -q
8 passed, 1 warning in 0.51s

$ uv run pytest tests/test_release_workflows.py -q -k 'publish_npm_rewrites_opencode_dependency_after_version_and_before_publish or opencode_source_dependency_matches_lockfile_registry_range or release_please_manifest_config_consistency'
2 passed, 38 deselected, 1 warning in 0.07s

$ uv run pytest tests/test_dashboard_cache_lifetime_playwright.py tests/test_dashboard_cache_ttl_playwright.py -q
4 passed, 1 warning in 4.04s

$ uv run ruff check scripts/verify-versions.py scripts/version-sync.py scripts/tests/test_version_sync.py tests/test_release_workflows.py
All checks passed!

$ npm ci && npm run build  (plugins/opencode)
Build success; dist/index.js, dist/entry.opencode.js, and DTS outputs emitted
```

## Real Behavior Proof

- Environment: Windows, Python 3.11.15, Node v24.15.0, npm 11.16.0
- Exact command / steps: inspected `.github/workflows/release.yml`,
updated the npm publish path for `plugins/opencode`, aligned the two
failing dashboard Playwright tests with the current Session/Lifetime
split, then ran the focused pytest commands above plus `npm ci && npm
run build` in `plugins/opencode`
- Observed result: the release workflow now versions and publishes
`headroom-opencode`, release-please and version-sync track
`plugins/opencode/package.json`, the dashboard tests now fetch durable
cache and setup-url data from the Lifetime view, and the opencode
package still builds locally from source
- Not tested: GitHub Package Registry publish

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

`CHANGELOG.md` is unchanged because release-please owns changelog
generation here.

---------

Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-08-11 23:56:40 -05:00
Tejas Chopra
7c9b046595
fix(dashboard): serve tailwind/htmx/alpine locally instead of from CDNs (#2734)
## Description

The dashboard loaded all three of its front-end dependencies from
third-party CDNs at page load:

```html
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js" defer></script>
```

Microsoft Edge's Tracking Prevention classifies `unpkg.com` as a tracker
and blocks it by default on Windows; locked-down corporate proxies block
both hosts. On those machines none of the three scripts executed — no
Tailwind CSS, no htmx polling, no Alpine bindings, plus an uncaught
`ReferenceError: tailwind is not defined` from the inline
`tailwind.config` assignment at `dashboard.html:21`. The dashboard
rendered blank. Reported from a Windows user's console:

```text
Tracking Prevention blocked access to storage for https://unpkg.com/htmx.org@1.9.10.
Tracking Prevention blocked access to storage for https://unpkg.com/alpinejs@3.13.3/dist/cdn.min.js.
```

This vendors the three files and serves them from the proxy, so the
dashboard has no external network dependency at all.

Note for anyone triaging the same report: the `cdn.tailwindcss.com
should not be used in production` line in that console output is **not**
related. It is an unconditional `console.warn` in the Tailwind Play CDN
build (no hostname guard), so it fires on every load, localhost
included, and it still fires now that the bundle is self-hosted.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Vendored
`headroom/dashboard/static/{tailwind.min.js,htmx.min.js,alpine.min.js}`
— Tailwind Play CDN 3.4.17, htmx 1.9.10, Alpine 3.13.3, byte-for-byte as
published.
- `headroom/dashboard/__init__.py`: added `STATIC_DIR`.
- `headroom/proxy/server.py`: mounted `/dashboard/static`, registered
**before** `register_provider_routes`' catch-all so the asset requests
are not tunneled to the wrapped upstream provider (same ordering
constraint as the `/favicon.ico` route, GH #1787). `check_dir=False` so
a missing assets directory 404s the dashboard JS rather than aborting
proxy startup.
- `headroom/dashboard/templates/{dashboard,settings}.html`: script `src`
→ `/dashboard/static/…`.
- `NOTICE`: MIT / 0BSD attribution for the three vendored bundles.
- `tests/test_dashboard_static_assets.py`: new.

No packaging change needed — `[tool.maturin]` includes everything under
`headroom/`, so the wheel picks the assets up. Wheel grows ~498 KB (407
KB of that is the Tailwind Play bundle).

## Testing

- [x] Unit tests pass (`pytest`) — targeted, see note under *Not tested*
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ python -m pytest tests/test_dashboard_static_assets.py tests/test_proxy_settings_endpoints.py -q
tests/test_dashboard_static_assets.py ......                             [ 21%]
tests/test_proxy_settings_endpoints.py ......................            [100%]
============================== 28 passed in 4.47s ==============================

$ ruff check .
All checks passed!

$ ruff format --check headroom/proxy/server.py headroom/dashboard/__init__.py tests/test_dashboard_static_assets.py
3 files already formatted

$ mypy headroom
Success: no issues found in 509 source files
```

## Real Behavior Proof

- **Environment:** macOS 15 (Darwin 25.4.0), Python 3.12.6, headless
Chromium via Playwright, proxy served in-process with
`create_app(ProxyConfig(optimize=False, cache_enabled=False,
log_full_messages=True))` on `:8787`.
- **Exact command / steps:** loaded `/dashboard` and
`/dashboard/settings` with `wait_until="networkidle"`, then asserted the
globals exist, that Tailwind actually generated CSS (computed style of a
`px-3` element), and recorded every non-localhost request plus all
`pageerror`/`console.error` events.
- **Observed result:**

```text
/dashboard          | alpine: True | tailwind css: True | external: none | errors: none
/dashboard/settings | alpine: True | tailwind css: True | external: none | errors: none

/dashboard                        200 text/html; charset=utf-8  191549
/dashboard/static/tailwind.min.js 200 text/javascript; charset=utf-8  407279
/dashboard/static/htmx.min.js     200 text/javascript; charset=utf-8   47755
/dashboard/static/alpine.min.js   200 text/javascript; charset=utf-8   43441

feed-toggle visible: True
alpine loaded: True  htmx: True  tailwind: True
tailwind applied (px-3 padding): 12px
external hosts: none
console errors: none
```

Zero external requests on either page, so the Edge/firewall failure mode
is structurally gone rather than worked around.

- **Not tested:**
- No Windows machine available — the fix is verified as "makes zero
external requests", which is the property the Windows failure depended
on, but it has not been confirmed against Edge with Tracking Prevention
on. Worth a check by someone on Windows before release.
  - Full `pytest` suite not run (targeted runs only); CI covers it.
- `tests/test_dashboard/test_live_feed.py` still has 2 failures, both
pre-existing and unrelated: those tests need a manually started proxy on
`:8787` with `--log-messages`, and `test_live_feed_button_exists`
asserts `is_visible()` with no wait for the `/stats` poll that flips
`log_full_messages`. The other 2 in that file pass against this change,
which is itself end-to-end evidence that Alpine and htmx work from the
vendored bundles.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md`

## Additional Notes

- No issue number: reported directly rather than filed, so `Closes #` is
omitted. Closed #22 ("Dashboard is not working") and closed #533
(Windows cp949 `get_dashboard_html()`) are different failures.
- **Docs checklist item is N/A** — nothing user-facing changes; the
dashboard URL and behaviour are identical.
- Deliberately **not** switching to a real Tailwind CLI build. It would
cut 407 KB to ~20 KB and silence the production warning, but it puts
Node in the release path and silently leaves any class added to the
2,713-line template unstyled with no CI guard. The Play bundle behaves
exactly as it does today, just served locally. Worth revisiting if wheel
size becomes a problem (note the PyPI project-size ceiling).
- Upgrades are now manual: bumping these three means re-downloading the
files. Pinned versions are recorded in `NOTICE`.
2026-08-03 06:07:27 -07:00
Tejas Chopra
e0ce4b1d48
fix: remove rtk and lean-ctx CLI context tools (#2677)
## Description

Removes both third-party CLI context tools — **rtk** and **lean-ctx** —
and with them the context-tool selector itself. Headroom no longer
downloads, installs or configures either one, and there is no
replacement.

The previous pass (#2344) gated only three entry points inside
`headroom/cli/wrap.py`. That left the feature reachable in practice:

| Gap | Effect |
|---|---|
| `scripts/install.sh:1544`, `install.ps1:1681` | Ran `rtk init --global
--auto-patch` from bash/PowerShell, **bypassing the Python gate
entirely** — `curl \| sh` still wrote a Claude Code `PreToolUse` hook
regardless of `HEADROOM_RTK` |
| `wrap.py` `_setup_context_tool_for_agent` | **`wrap openhands` was
broken by default**: `rtk_required=True` met a gate returning `None` →
`SystemExit(1)`. Invisible because all 8 openhands tests patched
`_ensure_rtk_binary` to a fake path |
| `proxy/helpers.py`, `subscription/tracker.py` | Proxy shelled out to
`rtk gain` from `/stats`, the dashboard and `headroom perf`; the tracker
polled it per contribution (`_RTK_WIRING_DEFAULT = "enabled"`) |
| No cleanup path | Nothing removed artifacts an earlier default had
installed, so a machine that once ran the old default kept rtk in the
loop forever (#1669, #1955) |

Also worth noting: the rtk binary download had **no SHA or signature
verification** — only `rtk --version` as a smoke test.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [x] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [x] Code refactoring (no functional changes)

## Changes Made

**Removed** — `headroom/rtk/` and `headroom/lean_ctx/` packages,
`headroom/cli/wrap_rtk_metrics.py`, `_selected_context_tool` /
`_setup_context_tool_for_agent` / `_VALID_CONTEXT_TOOLS`, the `--rtk` /
`--no-rtk` / `--no-project-rtk` / `--keep-rtk` flags across all 18 wrap
subcommands, `HEADROOM_RTK*`, the proxy-side `rtk gain` polling, the
dashboard CLI-filtering panel (rows + all 8 `cliFiltering*` Alpine
getters), `paths.rtk_path()` / `lean_ctx_path()`, the SDK path helpers,
`benchmarks/rtk_loop_learn_eval.py`, and the `headroom/rtk/**` CI path
filters.

**Fails loudly, not silently** — `--context-tool` / `--no-context-tool`
/ `HEADROOM_CONTEXT_TOOL` are kept solely to error out. They live in
shell profiles, aliases and CI jobs, and accepting them as a no-op would
read as Headroom having quietly stopped working. The installers reject
them too, which matters more than it looks: their arg parsers forward
the first unknown flag **and everything after it** to the wrapped tool,
so a leftover `--no-rtk` would have silently swallowed a following
`--port` and then been ignored downstream.

**New `headroom/context_tool_cleanup.py`** — deleting the code cannot
help a machine that already ran the old default, since the hooks,
binaries and injected guidance are durable on disk.
`purge_context_tool_artifacts()` runs once per `wrap`/`unwrap` and
removes the registered hook entries, the generated hook scripts, the
Headroom-managed `~/.local/bin` symlinks, the vendored
`~/.headroom/bin/{rtk,lean-ctx}` binaries, the `lean-ctx` MCP server
entry and the marker-fenced instruction blocks. Deliberately
conservative: idempotent, **skips** a malformed config rather than
overwriting it, and only unlinks a symlink resolving inside Headroom's
own bin dir so a user's own build is untouched. It reports on
**stderr**, because `wrap/unwrap openclaw --prepare-only` emit
machine-readable JSON on stdout as their entire contract. Skipped for
`wrap selfheal` (runs from a SessionStart hook; must not race Claude
Code's writer for `~/.claude.json`) and for `--help`, which must stay
read-only.

**Client-config hardening** (discovered while investigating a "corrupted
Serena settings file" report) — `wrap.py` reset a settings file to `{}`
when an existing file would not parse, then wrote that back. One
hand-edited typo or a transient `EACCES`/`EINTR` on a valid file
destroyed the user's `permissions`, `env` and `hooks`, on **every
`headroom wrap claude`**. It now refuses to write. Separately,
`fsutil.write_text` is now atomic (temp file + `fsync` + `os.replace`),
fixing all 14 non-atomic client-config writes at once; it follows
symlinks rather than replacing them (dotfile managers) and preserves an
existing file's mode.

**Deliberately kept** — `rtk` stays in the wrapper-peel list in
`transforms/content_router.py`. It sits beside `sudo`/`env`/`timeout` as
shell-command grammar, so `rtk cat f` is still classified as a file read
for anyone running their own rtk install, which the purge intentionally
leaves alone.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ ruff check headroom/ tests/ e2e/ --exclude headroom/dashboard/templates
All checks passed!

$ ruff format --check headroom/ tests/ e2e/ --exclude headroom/dashboard/templates
1255 files already formatted

$ mypy headroom/
Success: no issues found in 508 source files

$ pytest tests/test_context_tool_cleanup.py -q
11 passed

$ pytest tests/test_fsutil.py -q
12 passed

$ pytest tests/test_cli/test_wrap_codex.py -q            # 89 tests
89 passed in 431.68s
$ pytest tests/test_cli/test_wrap_opencode.py -q
39 passed in 257.46s
$ pytest tests/test_cli/test_wrap_helpers.py -q
45 passed
$ pytest tests/test_paths.py -q
75 passed
$ pytest tests/test_cli/test_unwrap_claude.py -q
14 passed
$ pytest tests/test_proxy_savings_history.py -q
39 passed
$ pytest tests/test_cli/test_wrap_copilot.py -q
27 passed
$ pytest tests/test_cli/test_wrap_zcode.py -q
20 passed
$ pytest tests/test_subscription_tracker.py -q
9 passed
$ pytest tests/test_proxy_dashboard_stats_cache.py -q
5 passed, 1 skipped
```

Repo-wide grep for 14 removed symbols (`headroom.rtk`,
`headroom.lean_ctx`, `_ensure_rtk_binary`, `_selected_context_tool`,
`_get_context_tool_stats`, `rtk_path`, `lean_ctx_path`,
`wrap_rtk_metrics`, `HEADROOM_RTK`, `cli_tokens_avoided`,
`tokens_saved_rtk`, …) across `*.py`, `*.ts`, `*.sh`, `*.ps1`, `*.yml`,
`*.html`: **zero hits**.

Notable test changes: `test_wrap_openhands.py` no longer patches
`_ensure_rtk_binary` and asserts `wrap openhands --prepare-only` exits 0
unpatched — the regression that was previously masked.
`test_wrap_continue.py` and `test_wrap_hintfile_agents.py` were removed
(every test drove RTK instruction injection). A new
`test_subscription_tracker.py::test_load_state_written_before_cli_context_tools_were_removed`
proves a pre-removal `subscription_state.json` still loads.

## Real Behavior Proof

- **Environment:** macOS 15.4 (darwin 25.4.0), Python 3.12.6, Headroom @
this branch, real `~/.headroom` and `~/.claude` on the dev machine.
- **Exact command / steps and observed result:**

```text
# 1. Retired flag fails loudly instead of silently no-op'ing
$ headroom wrap codex --prepare-only --context-tool rtk
Error: CLI context tools (rtk, lean-ctx) have been removed from Headroom: they
rewrote shell commands through a third-party binary Headroom no longer manages.
Drop --context-tool / --no-context-tool and unset HEADROOM_CONTEXT_TOOL;
`headroom wrap` uninstalls what they left behind on first run.

$ HEADROOM_CONTEXT_TOOL=lean-ctx headroom wrap codex --prepare-only
Error: CLI context tools (rtk, lean-ctx) have been removed from Headroom: ...

# 2. install.sh rejects the retired flags (extracted parse_wrap_args harness)
['--no-rtk', '--port', '9999']   rc=1  ERROR: CLI context tools ... Drop --no-rtk
['--context-tool=rtk']           rc=1  ERROR: CLI context tools ... Drop --context-tool
$ bash -n scripts/install.sh   # syntax OK

# 3. Purge ran against the real machine, which had all the orphaned artifacts
$ python -c "from headroom.context_tool_cleanup import purge_context_tool_artifacts; ..."
  removed ~/.headroom/bin/lean-ctx        (51 MB)
  removed ~/.headroom/bin/rtk             (7.7 MB)
  removed ~/.local/bin/rtk                (symlink into ~/.headroom/bin)
  removed ~/.claude/hooks/rtk-rewrite.sh
  removed 8 lean-ctx-* hook scripts
# ~/.claude.json afterwards: 90 top-level keys, 19 projects, mcpServers unchanged
# → ~59 MB reclaimed, no unrelated key touched

# 4. stdout stays machine-readable while the purge reports (planted a fake artifact)
$ headroom wrap openclaw --prepare-only --gateway-provider-id codex >out 2>err
$ cat out
{"enabled":true,"config":{"proxyPort":8787,...}}     # parses as JSON
$ cat err
Retired CLI context tool cleanup: removed /Users/tcms/.headroom/bin/rtk

# 5. --help is inert (planted artifact survives), a real run purges
$ headroom wrap codex --help   → artifact survived: CORRECT
$ headroom wrap openclaw --prepare-only → purged: CORRECT

# 6. MCP purge dry-run against a copy of the real 82 KB ~/.claude.json
top-level keys 90 -> 90;  projects 19 -> 19;  LOST keys: none
all content outside mcpServers byte-identical: True
```

Dashboard rendered via the Playwright test after the panel removal:
"Token Savings" shows only `Proxy 0 (0.0%)` / `Of total wire: 36.86%`,
and "Token Usage" reads Before Compression → Proxy Removed → After
Compression with no "Filtered (this session)" row. Nothing below the
removed panel broke.

- **Not tested:** Windows and Linux (macOS only) — `install.ps1` is
verified by brace-balance and inspection, not executed, since no `pwsh`
is available locally. The wrap e2e suite (`e2e/wrap/run.py`) was updated
but not run; it needs the Docker e2e image. `serena project index`
interaction is exercised in the stacked base PR.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I did **not** edit `CHANGELOG.md` — it is generated by
release-please from my Conventional Commit PR title (a CI guard enforces
this)

## Additional Notes

**Stacked on #2676** (`tejas/serena-config-bootstrap`) — please merge
that first; this PR's base should then be retargeted to `main`, or it
will read as containing that fix too.

**Breaking-change migration for users:**
- Drop `--rtk`, `--no-rtk`, `--no-project-rtk`, `--keep-rtk`,
`--context-tool`, `--no-context-tool` from any alias, script or CI job,
and unset `HEADROOM_RTK*` / `HEADROOM_CONTEXT_TOOL`. They now error
rather than being ignored, so the failure is immediate and
self-explaining.
- Previously-installed artifacts are purged automatically on the next
`wrap`/`unwrap`; no manual cleanup needed.
- `headroom perf --json` no longer carries a `cli_filtering` key, and
`/stats` no longer returns a `context_tool` section.

**Docs:** `docs/rtk-architecture.md` deleted; RTK/lean-ctx removed from
`README.md`,
`docs/content/docs/{configuration,opencode,grok-build,docker-install,filesystem-contract}.mdx`,
`docs/observability.md` and the matching `wiki/` pages.
`REALIGNMENT/09-phase-G-rtk-observability.md` is marked SUPERSEDED
rather than deleted, to keep the planning record.

**Follow-ups not in scope:** `_emit_wrap_interrupted` was deleted as
dead code — its only caller was the `except KeyboardInterrupt` guarding
the binary download, so with no download there is nothing slow left to
interrupt.
2026-07-30 22:59:41 -07:00
Rod Boev
53a465b121
fix(proxy): subtract cache write premiums from net savings (#1800)
## Description

Cache stats already calculate both prompt-cache read savings and
cache-write premium cost, but the exported `net_savings_usd` field used
gross read savings alone. That made cache-heavy token-mode workloads
look profitable even when extra cache writes offset or exceeded the read
discount. This updates existing cache cost accounting so provider and
total `net_savings_usd` subtract write premiums while keeping gross
savings and write premium fields visible. Refs #327.

The scope follows doublefx's controlled measurement in
https://github.com/headroomlabs-ai/headroom/issues/327#issuecomment-4683604089,
which showed token-mode compression increasing cache write volume and
billed cost while dashboard token savings looked positive.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Subtract cache write premiums from provider-level cache
`net_savings_usd`.
- Subtract aggregate cache write premiums from total cache
`net_savings_usd`.
- Keep gross `savings_usd` and `write_premium_usd` visible for dashboard
and telemetry consumers.
- Add focused regressions for provider net, total net, and
zero-write-premium preservation.
- Update the dashboard cache TTL fixture to match the corrected net
value.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_proxy_cache_ttl_metrics.py
tests/test_dashboard_cache_ttl_playwright.py
tests/test_proxy_dashboard_stats_cache.py -q`)
- [x] Linting passes (`uv run ruff check headroom/proxy/cost.py
tests/test_proxy_cache_ttl_metrics.py
tests/test_dashboard_cache_ttl_playwright.py
tests/test_proxy_dashboard_stats_cache.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_proxy_cache_ttl_metrics.py tests/test_dashboard_cache_ttl_playwright.py tests/test_proxy_dashboard_stats_cache.py -q
28 passed, 2 skipped, 1 warning in 32.75s

uv run pytest tests/test_proxy_cache_ttl_metrics.py -q -k keeps_net_equal_without_write_premium
1 passed, 16 deselected in 0.15s

uv run ruff check headroom/proxy/cost.py tests/test_proxy_cache_ttl_metrics.py tests/test_dashboard_cache_ttl_playwright.py tests/test_proxy_dashboard_stats_cache.py
All checks passed!
```

## Real Behavior Proof

- Environment: Windows, Python through the project `uv` environment.
- Exact command / steps: run the cache net-savings regressions against
base and head.
- Observed result: base reports provider net as `0.0036` instead of
`0.0021` and total net as `0.0046` instead of `0.0031`; head passes the
focused cache metrics suite and preserves `net_savings_usd ==
savings_usd` when there is no write premium.
- Not tested: broader cache-hit-rate tuning, prompt-cache policy
changes, and live provider billing.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

No changelog entry is needed because this corrects existing stats fields
rather than adding a new command or control. Type checking was not part
of the focused local validation for this Python-only fix. Dashboard
Playwright coverage is CI-owned locally; the import-gated file was
included in the focused pytest command and skipped because Playwright is
not installed in this environment.
2026-07-07 23:24:41 -05:00
Rod Boev
e035aefce2
fix(dashboard): derive per-project setup URL from live origin (#1511)
## Description

The Per-Project Savings empty state currently shows a hardcoded
`ANTHROPIC_BASE_URL: http://127.0.0.1:8787/p/<project-name>`. When the
proxy listens on a fallback or custom port, users can copy a broken
setup URL from the dashboard. This change derives the hint from the
browser's live origin and keeps the existing `/p/<project-name>` suffix
used by per-project savings. Closes #1508.

Related context: #1406 made non-default proxy ports a normal path, which
makes the hardcoded dashboard hint user-visible more often.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)

## Changes Made

- Replace the static Per-Project Savings setup hint with an Alpine
`x-text` binding that uses `window.location.origin`.
- Preserve the `/p/<project-name>` suffix so the displayed path shape
stays aligned with the existing per-project routing contract.
- Add a Playwright regression that loads the dashboard from non-default
origins and asserts the empty state follows the active page origin
instead of `8787`.

## Testing

- [x] Unit tests pass (`uv run pytest
tests/test_dashboard_cache_ttl_playwright.py -k
"per_project_setup_url_uses_current_origin" -v`)
- [x] Unit tests pass (`uv run pytest
tests/test_owned_asset_encoding.py::test_get_dashboard_html_reads_as_utf8
tests/test_proxy_dashboard_stats_cache.py::test_dashboard_uses_cached_stats_and_lazy_history_feed_polling
-v`)
- [x] Linting passes (`uv run ruff check
tests/test_dashboard_cache_ttl_playwright.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [x] Manual testing performed

### Test Output

```text
uv run pytest tests/test_dashboard_cache_ttl_playwright.py -k "per_project_setup_url_uses_current_origin" -v
tests/test_dashboard_cache_ttl_playwright.py::test_dashboard_per_project_setup_url_uses_current_origin PASSED [100%]
================= 1 passed, 1 deselected, 1 warning in 0.82s ==================

uv run pytest tests/test_owned_asset_encoding.py::test_get_dashboard_html_reads_as_utf8 tests/test_proxy_dashboard_stats_cache.py::test_dashboard_uses_cached_stats_and_lazy_history_feed_polling -v
tests/test_owned_asset_encoding.py::test_get_dashboard_html_reads_as_utf8 PASSED [ 50%]
tests/test_proxy_dashboard_stats_cache.py::test_dashboard_uses_cached_stats_and_lazy_history_feed_polling PASSED [100%]
======================== 2 passed, 1 warning in 0.16s =========================

uv run ruff check tests/test_dashboard_cache_ttl_playwright.py
All checks passed!
```

## Real Behavior Proof

- Environment: Playwright Chromium dashboard harness, dashboard template
served through the existing route interception used by the dashboard
tests, no live provider required.
- Exact command / steps: `uv run pytest
tests/test_dashboard_cache_ttl_playwright.py -k
"per_project_setup_url_uses_current_origin" -v`
- Observed result: `http://127.0.0.1:8788/dashboard` passed with the new
current-origin assertion, and `origin/main` failed the same assertion
because the page still rendered `ANTHROPIC_BASE_URL:
http://127.0.0.1:8787/p/<project-name>`. A separate browser check
against `http://headroom.local:9393/dashboard` also passed on the
patched branch.
- Not tested: full live `headroom proxy --port 8788` browser validation,
unless it is run during implementation.

## Review Readiness

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

## Checklist

- [x] My code follows the project's style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable

## Additional Notes

`CHANGELOG.md` is intentionally unchanged because this repo generates
changelog entries from conventional commits. The documentation checkbox
is satisfied by correcting the in-dashboard setup instruction.
2026-06-30 14:24:00 -05:00
Focused Instability
b7350aa29c
ci: run dashboard playwright tests in a dedicated job (#921)
## Summary

Closes #920. Follow-up noted in #915.

The dashboard Playwright tests guard on
`pytest.importorskip("playwright...")` and no CI job installs
playwright, so they have skipped on every CI run since they were added —
which is how the bitrot fixed in #915 went unnoticed.

This adds a `test-dashboard-ui` job to `ci.yml`, same shape as
`test-agno`:

- installs the prebuilt wheel `[dev]` + playwright, then `playwright
install --with-deps chromium`
- runs `pytest tests/test_dashboard_*_playwright.py` — the stub-based
tests only (all routes mocked via `page.route`, no network); the glob
also picks up the CVC panel tests from #913 once that merges
- sets `HEADROOM_PLAYWRIGHT_ARTIFACT_DIR` and uploads the captured
dashboard screenshots as a workflow artifact (7-day retention), so every
CI run leaves a visual record of the rendered dashboard

Deliberately excluded: `tests/test_dashboard/test_live_feed.py` — it
navigates to a live proxy on `localhost:8787` and would fail on a runner
with nothing listening. The main test shards keep skipping playwright
tests (playwright stays uninstalled there), so nothing double-runs.

## Testing

- `yaml.safe_load` parses the workflow; the `workflow-validation` CI job
(actionlint + act) runs on this PR since it touches `ci.yml`
- The test this job will run passes locally:
`tests/test_dashboard_cache_ttl_playwright.py` — 1 passed (chromium)
- This PR's own CI run exercises the new job end-to-end
2026-06-12 10:20:37 -05:00
JerrettDavis
a6787556be Add observed cache TTL metrics and dashboard coverage 2026-04-06 21:13:30 -05:00