feat(proxy): add a read-only dashboard at /_slp/ #17

Merged
rcsheets merged 1 commit from feat/dashboard into main 2026-10-02 05:19:45 +00:00
Owner

What

A read-only status page at /_slp/ for a person: what SLP is doing, how it is configured, and what it has been up to lately.

  • Now: routes serving, requests in flight, and per route whether it is serving or refusing, the upstream model name and where it came from, and the last probe result.
  • Configured: each route's timeout, retry, aggregate, expect/gating, backend or edition, plus version, listen address, uptime, logging, and the allow list.
  • Lately: requests per minute over the last hour (served / turned away / failed), the last 50 requests, and the last 30 discovery events.

The SLP version is in the tab title, beside the heading, and in the settings. The page refreshes every 5 seconds, pauses while text is selected, and works as a snapshot without scripts.

Access

dashboard:
  enabled: true            # default; false makes /_slp/ a 404
  allow:                   # default: RFC 1918
    - 10.0.0.0/8
    - 172.16.0.0/12
    - 192.168.0.0/16
  • Behind Caddy every request arrives from a private address, so the peer alone proves nothing. The peer must be allowed and so must every address in X-Forwarded-For, X-Real-IP, and Forwarded; one that does not parse is a refusal.
  • Loopback is not in the default, so curl localhost:8080/_slp/ on the SLP host is a 404 until 127.0.0.0/8 is added.
  • A refused client gets the same 404 as an SLP with the dashboard off; the log says why, at most once a minute.
  • A bad or empty allow list is a startup error.

What it does not show

  • No upstream URLs, hosts, or ports (the ?debug rule).
  • Nothing a client sent: no addresses, headers, bodies, or inbound X-Request-Id; a request matching no route is "unknown model", not the name.
  • No error text: failures are labeled, and the request id on the page finds the detail in the log.

GET-only, served under a content security policy with no inline script or style.

Worth a look in review

  • The README's "no admin surface" / "no admin UI" lines are kept, with wording added that the dashboard is read-only and its allow list is a network boundary, not a login.
  • The recent history is fixed-size in-memory buffers, recorded only while the dashboard is enabled and gone on restart. Routing reads none of it.
  • A proxy that forwards without setting any of the three headers leaves SLP nothing to check; the README says to not route /_slp/ there or turn the dashboard off.
  • config.Route and proxy.Route gain EditionOf.
  • The dashboard distinguishes a backend timeout from an unreachable backend; the slp_requests_total label is unchanged.

Testing

go vet and go test -race ./... pass. New tests cover the allow-list table (including forged and unparseable forwarding headers), refusal being indistinguishable from a disabled dashboard, and a leak test that drives marked secrets through the proxy and asserts none reach the page. Also run against fake backends and checked in headless Chromium at desktop, dark, and phone widths; not yet run against a real deployment behind Caddy.

🤖 Generated with Claude Code

## What A read-only status page at `/_slp/` for a person: what SLP is doing, how it is configured, and what it has been up to lately. - **Now**: routes serving, requests in flight, and per route whether it is serving or refusing, the upstream model name and where it came from, and the last probe result. - **Configured**: each route's timeout, `retry`, `aggregate`, `expect`/gating, backend or edition, plus version, listen address, uptime, logging, and the allow list. - **Lately**: requests per minute over the last hour (served / turned away / failed), the last 50 requests, and the last 30 discovery events. The SLP version is in the tab title, beside the heading, and in the settings. The page refreshes every 5 seconds, pauses while text is selected, and works as a snapshot without scripts. ## Access ```yaml dashboard: enabled: true # default; false makes /_slp/ a 404 allow: # default: RFC 1918 - 10.0.0.0/8 - 172.16.0.0/12 - 192.168.0.0/16 ``` - Behind Caddy every request arrives from a private address, so the peer alone proves nothing. The peer must be allowed **and** so must every address in `X-Forwarded-For`, `X-Real-IP`, and `Forwarded`; one that does not parse is a refusal. - Loopback is not in the default, so `curl localhost:8080/_slp/` on the SLP host is a 404 until `127.0.0.0/8` is added. - A refused client gets the same 404 as an SLP with the dashboard off; the log says why, at most once a minute. - A bad or empty `allow` list is a startup error. ## What it does not show - No upstream URLs, hosts, or ports (the `?debug` rule). - Nothing a client sent: no addresses, headers, bodies, or inbound `X-Request-Id`; a request matching no route is "unknown model", not the name. - No error text: failures are labeled, and the request id on the page finds the detail in the log. GET-only, served under a content security policy with no inline script or style. ## Worth a look in review - The README's "no admin surface" / "no admin UI" lines are kept, with wording added that the dashboard is read-only and its allow list is a network boundary, not a login. - The recent history is fixed-size in-memory buffers, recorded only while the dashboard is enabled and gone on restart. Routing reads none of it. - A proxy that forwards without setting any of the three headers leaves SLP nothing to check; the README says to not route `/_slp/` there or turn the dashboard off. - `config.Route` and `proxy.Route` gain `EditionOf`. - The dashboard distinguishes a backend timeout from an unreachable backend; the `slp_requests_total` label is unchanged. ## Testing `go vet` and `go test -race ./...` pass. New tests cover the allow-list table (including forged and unparseable forwarding headers), refusal being indistinguishable from a disabled dashboard, and a leak test that drives marked secrets through the proxy and asserts none reach the page. Also run against fake backends and checked in headless Chromium at desktop, dark, and phone widths; not yet run against a real deployment behind Caddy. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(proxy): add a read-only dashboard at /_slp/
All checks were successful
ci / check (pull_request) Successful in 1m27s
78a8ad3f1b
/metrics is for Prometheus and ?debug is for curl; neither is a page a
person can open to see what SLP is doing. /_slp/ is that page: which routes
are serving and which are refusing, the upstream model name each sends and
where it came from, how each is configured, requests in flight, an hour of
requests per minute, the last 50 requests, and the last 30 discovery events.
The version is in the tab title, beside the heading, and in the settings.

It is on by default and served only to RFC 1918 clients. dashboard.enabled:
false turns it off; dashboard.allow replaces the default list with CIDRs or
bare addresses, and a list that does not parse, or is empty, is a startup
error. Loopback is deliberately not in the default: a reverse proxy on the
same host makes every client look like 127.0.0.1.

SLP sits behind Caddy, where every request arrives from a private address,
so the peer address alone proves nothing. The peer must be allowed, and so
must every address in X-Forwarded-For, X-Real-IP, and Forwarded. Those
headers are only ever used to refuse -- a client can add entries but cannot
remove the one its proxy appends -- and one that does not parse is a
refusal. A client that is not allowed gets the same 404 as an SLP with no
dashboard; the log says why, at most once a minute.

The allow list is a network boundary, not authentication, so the page is
built to carry nothing worth more than that. It keeps the rule ?debug and
the error path already keep -- no upstream URLs, hosts, or ports -- and adds
one: nothing a client sent is shown. No client addresses, headers, bodies,
or inbound request ids, and a request that matched no route is "unknown
model", not the name it asked for. Failures are labeled, never quoted; the
transport error stays in the log, and the request id on the page finds it.

It is GET-only and served under a content security policy with no inline
script or style, so the chart is server-rendered SVG attributes. The page
refreshes itself every five seconds, holds still while text is selected,
and is a plain snapshot with scripts off.

The history is a few fixed-size buffers in memory, recorded only while the
dashboard is enabled and gone on restart. Routing reads none of it.

Routes gain EditionOf so the page can say what an edition is an edition of.
The dashboard tells a backend timeout from an unreachable backend; the
slp_requests_total label for both is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Collaborator

Automated review by pr-reviewer v0.52.3 | Safety Check | Ministral 3 Instruct | tracking id r-bf3ec8-12f727
This is an AI-generated review and may contain mistakes.

Status: ❌ Failed


This review couldn't be completed: that model isn't loaded on the inference service right now, and no alternate model was able to review it either. Consider splitting this PR into smaller changes. Tracking id r-bf3ec8-12f727.

Comment @pr-reviewer-bot retry to try again.

<!-- pr-reviewer:review --> *Automated review by [pr-reviewer](https://git.brooktrails.org/brooktrails/pr-reviewer) v0.52.3 | Safety Check | Ministral 3 Instruct | tracking id `r-bf3ec8-12f727`* *This is an AI-generated review and may contain mistakes.* **Status:** ❌ Failed --- This review couldn't be completed: that model isn't loaded on the inference service right now, and no alternate model was able to review it either. Consider splitting this PR into smaller changes. Tracking id `r-bf3ec8-12f727`. Comment `@pr-reviewer-bot retry` to try again.
rcsheets deleted branch feat/dashboard 2026-10-02 05:19:45 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
brooktrails/slp!17
No description provided.