Web UI
For operators · running scans
How-toadler --web boots a small in-process HTTP server and serves a SolidJS
SPA from the same binary — no separate frontend deployment, no extra
process to manage. Once the server is up, kick off scans, watch outcomes
stream in over SSE, persist them to disk, and diff them against earlier
runs.
adler --web # http://127.0.0.1:8080adler --web --web-bind 0.0.0.0:9000 # listen on all interfaces, custom portWhat you get in the browser
Section titled “What you get in the browser”Live scan view
Section titled “Live scan view”Outcomes stream in as they resolve (SSE), grouped by category, with per-row evidence (verdict reason, response snippet, URL) and a one-click retry.
Result rows
Section titled “Result rows”Each row shows the verdict (Found / NotFound / Uncertain), the elapsed
time, the verdict reason for Uncertain rows, and a small transport
chip since v0.10 when the probe used anything other than the default HTTP transport
— impersonate or browser. A * suffix (e.g. browser*) marks an
outcome where the cheap path returned an
Uncertain(cloudflare_challenge | rate_limited) and the router
automatically escalated through the browser. The common Http+0 case
stays uncluttered.
browser* chip on the meta column marks an outcome where the cheap HTTP path returned Uncertain(cloudflare_challenge) and the router automatically escalated.History
Section titled “History”Every finished scan is persisted to ~/.cache/adler/scans/ (oldest 200,
atomic writes). Reopen any past scan via #/scan/<id> deep-links.
Compare with previous
Section titled “Compare with previous”Pick any two persisted scans and diff them side-by-side (#/diff/<a>/<b>);
shows accounts gained / lost / flipped between the two runs. Esc /
back-button exits.
#/diff/<a>/<b>. Accounts gained, lost, and flipped between the two runs surface in three columns.Filters & sort
Section titled “Filters & sort”By verdict, category, presence of evidence, hidden NotFound rows. Preferences
persist to localStorage.
NSFW gate
Section titled “NSFW gate”Off by default; the toggle is hidden behind a confirmation, matching the
CLI’s --nsfw opt-in.
Access engine view since v0.11
Section titled “Access engine view since v0.11”The shield icon in the top bar opens a read-only panel showing what’s
loaded from --proxy-pool (name, country, kind per egress — never
proxy URLs) and --sessions (names only, never header values).
Sensitive material is kept off the HTTP API by design; editing happens
by updating the TOML files and restarting the server.
Per-scan egress subset since v0.11
Section titled “Per-scan egress subset since v0.11”When a pool is loaded, Advanced filters shows an Egress section that
toggles named entries from the pool; the next scan routes through that
subset only. Sites whose access policy can’t be satisfied by the chosen
subset land in Uncertain(geo_unavailable) — same honest verdict as if
no egress matched at all.
JSON API
Section titled “JSON API”The server exposes a small JSON API at /api/* — useful if you want to
drive Adler from a different frontend or a script:
| Method | Path | Purpose |
|---|---|---|
GET | /api/health | Liveness probe. |
GET | /api/sites | Site catalogue available to scans. |
GET | /api/access | Read-only access-engine view (no secrets). |
GET | /api/scans | Recent scans (in-memory + persisted). |
POST | /api/scan | Start a scan; returns a scan_id. |
GET | /api/scan/:id | Final aggregate (or 202 in-progress / 404). |
GET | /api/scan/:id/stream | Server-Sent Events stream of outcomes. |
POST | /api/scan/:id/retry | Re-probe a single site. |
SSE consumers should subscribe to the /stream endpoint and treat each
event as one outcome.
Per-scan egress in POST /api/scan
Section titled “Per-scan egress in POST /api/scan”The request body accepts an optional egress_names: string[] field;
when non-empty, the scan routes through only the named subset of the
pool. Unknown names return a 400 unknown_egress error with the bad
entries enumerated in the message field — a typo shouldn’t silently
turn into “nothing matched”.
POST /api/scan{ "username": "alice", "tag": ["dev"], "egress_names": ["us-residential"]}Deployment
Section titled “Deployment”The bundled SPA is baked into the binary at compile time (rust-embed),
so the deployed unit is just the adler executable plus whatever scan-
cache directory you point it at.
The SolidJS project lives at adler-server/web/; if you build from
source, run npm ci && npm run build there before cargo build — Vite
emits web/dist/, which rust-embed reads directly.
Security notes
Section titled “Security notes”adler --web binds to 127.0.0.1 by default. --web-bind 0.0.0.0:9000
exposes the API on every interface; if you do that, anyone on the
network can reach the JSON API. The access-engine endpoints
deliberately omit proxy URLs and session header values so even an
exposed /api/access won’t leak secrets — but a wide-open POST /api/scan still lets a stranger consume your --proxy-pool and
--browser-budget. Put a reverse proxy with auth in front of any
non-loopback bind.