09 - CLI, status API and web UI¶
CLI¶
Single binary tapyr, built with cobra. Global flags: --config <path> (repeatable, files or directories; env TAPYR_CONFIG, a list separated by the OS path list separator, : on Unix and ; on Windows), --log-level, --log-format (env TAPYR_LOG_LEVEL, TAPYR_LOG_FORMAT). Flags win over environment variables.
Common exit codes: 0 success, 1 usage error or internal error, 2 invalid configuration (including missing files and YAML syntax errors), 3 infeasible plan.
| Command | Purpose | Exit codes |
|---|---|---|
tapyr run |
Run the collector (server, scheduler, outputs) | 0 on clean shutdown, 1 on fatal error |
tapyr validate [--check-secrets] [--provider-path DIR ...] |
Load, schema-validate, semantic-validate, compile CEL; optionally resolve secrets | 0 valid, 2 invalid (errors with file:line:column) |
tapyr plan [--fanout tmpl=N ...] [--output table\|json] [--provider-path DIR ...] |
Compute effective intervals and projected consumption per quota scope, offline. Uses the state store inventory and run history if present | 0 feasible, 3 infeasible |
tapyr sim ... |
Run provider simulators (see 08-simulator.md) |
|
tapyr record --job J [--job ...] --out DIR [--anonymize FILE] [--provider-path DIR ...] |
Run jobs once and record their responses, anonymized (see 08-simulator.md) |
0 ok, 1 error, 2 invalid configuration |
tapyr schema [--kind K] [--out dir] |
Print or write the JSON Schemas embedded in the binary | |
tapyr provider list |
List built-in and user provider packages with versions | |
tapyr provider lint <dir> [--update] |
Validate a provider package, lint its dashboards and run its contract tests (--update rewrites tests/expected.prom) |
0 ok, 2 errors |
tapyr version |
Version, revision, Go version |
--provider-path (validate, plan, record, and sim) loads the provider packages of a directory in addition to the built-in ones and to the providerPaths of the configuration, after the latter: it serves a package being written, before it is built in or referenced by a configuration.
Signals for run: SIGHUP reloads the configuration (validated first; an invalid config is rejected and the running config kept); SIGINT/SIGTERM trigger graceful shutdown.
tapyr plan reads run history and inventory from the state store when the file exists (read-only); otherwise it uses estimate.pagesPerRun and the --fanout hints. Quotas are evaluated over a full window from now: plan answers "does this configuration fit", while the running engine replans with the remaining budget of the current window.
Example tapyr plan output, for the example configuration of the aruba-central-classic package (a job stretched to fit a quota shows its stretch factor and reason):
QUOTA SCOPE WINDOW BUDGET PROJECTED RESERVE STATUS
aruba-central-classic/daily 24h 4500 529 500 ok (11.8%)
account=campus
job aruba-aps desired 5m effective 5m 288 runs x 1 page = 302
job aruba-gateways desired 15m effective 15m 96 runs x 1 page = 101
job aruba-sites desired 1h effective 1h 24 runs x 1 page = 25
job aruba-switches desired 15m effective 15m 96 runs x 1 page = 101
HTTP server¶
One listener (settings.server.listen, default 127.0.0.1:9790) serving:
| Path | Content |
|---|---|
/metrics |
Collected data (prometheus output) |
/metrics/self |
Self-metrics |
/healthz, /readyz |
Health |
/api/v1/... |
Status API (read-only, JSON) |
/ui/ |
Embedded web UI (redirect / to /ui/) |
Optional basic auth (settings.server.basicAuth) protects /api and /ui (and optionally /metrics* with settings.server.basicAuth.protectMetrics: true). Put Tapyr behind a reverse proxy for TLS, or set settings.server.tls (certFile, keyFile) to serve HTTPS directly.
Status API¶
Read-only, JSON, versioned under /api/v1. It is the only interface the UI uses. An OpenAPI 3 description is maintained by hand in internal/api/openapi.yaml and served at /api/v1/openapi.yaml.
| Endpoint | Content |
|---|---|
GET /api/v1/status |
Version, uptime, config hash, last reload result and errors, readiness reasons, plan feasibility |
GET /api/v1/providers |
Providers with package version, accounts count, health summary |
GET /api/v1/accounts / {name} |
Accounts (no secrets), targets, labels, circuit state, auth status |
GET /api/v1/jobs / {name} |
Effective config (after defaults and template expansion), desired and effective intervals, instances, last run summary, next run |
GET /api/v1/jobs/{name}/instances |
Fan-out instances with their variables and last result |
GET /api/v1/jobs/{name}/runs?instance= |
Recent runs with statistics and redacted errors |
GET /api/v1/budgets |
Every limit and scope key: kind, max, used, remaining (internal and observed), external usage, reserve, window bounds, projection |
GET /api/v1/budgets/{limit}/timeline?scope= |
{limit} is provider/limit, URL-encoded (meraki-dashboard%2Fdaily). Consumption time series for the current and previous window (per-minute buckets from the ledger) |
GET /api/v1/plan |
The current plan (same data as tapyr plan) |
GET /api/v1/graph |
Nodes and edges: provider, account, target, job, job instance groups, output |
GET /api/v1/outputs |
Outputs with queue state and last send results |
GET /api/v1/debug/jobs/{name}/last?instance= |
Last raw response pages (redacted, truncated to settings.server.debug.maxBodyBytes, default 1 MiB) and the samples produced. Only when settings.server.debug.enabled |
GET /api/v1/events |
Server-Sent Events stream: run finished, plan changed, circuit changed, config reloaded |
Rules: never expose secrets; redact headers as in logs; paginate list endpoints (?limit=&cursor=); stable field names (snake_case); ISO 8601 timestamps.
Implementation notes¶
internal/apiserves the API from a read-onlySourceinterface implemented bytapyr run; it never reaches into the collector state directly. The OpenAPI description (OpenAPI 3.1) is maintained by hand; a test checks that every route is documented and that every response of the test suite validates against its schema.- Pagination:
limit(default 100, at most 1000) and an opaquecursor; responses of list endpoints are{"items": [...], "next_cursor": "..."},next_cursoromitted on the last page. - Errors are
{"error": {"code": "not_found", "message": "..."}}with the matching HTTP status. - Budget timelines use buckets of 1 minute for windows up to one day and of 1 hour beyond; the current window's buckets are persisted with the ledger, the previous window's are kept in memory only.
- Basic auth compares in constant time and answers
401withWWW-Authenticate;/healthzand/readyzare never protected. Withtls, the listener serves HTTPS only.
Web UI (read-only)¶
Inspired by the Grafana Alloy UI: see what is configured and how it behaves, never edit.
Technology¶
React + Vite + TypeScript, compiled to static assets in ui/dist/app, embedded with go:embed by ui/embed.go and served by internal/ui. Graph rendering with @xyflow/react. Budget timelines and gauges are hand-written SVG. No server-side rendering. In development, Vite proxies /api to a running tapyr run.
The UI is built in CI and in make build; a binary built without UI assets serves a minimal page explaining how to build them.
Pages¶
- Overview: health summary, plan feasibility banner, providers and accounts cards with status, quota gauges (used / projected / limit per window), recent errors.
- Graph: provider -> account -> (targets) -> inventory jobs -> metric jobs -> outputs. Node color by health. Click a node to open its detail panel. Fan-out shown as grouped nodes with instance counts.
- Job detail: effective config (YAML view), desired vs effective interval with stretch reason, instances table, recent runs (result, duration, requests, pages, retries, samples), errors.
- Budgets: one card per limit and scope: timeline of consumption over the window with projection line, reserve, external usage, window reset time. Rate limits show wait time and 429s.
- Plan: the plan table, sortable, with stretched jobs highlighted.
- Outputs: queue fill, send rate, failures, last success.
- Live debug (only when enabled): pick a job instance, see the last raw response (pretty JSON, collapsible) side by side with the samples produced. Refresh on new run (SSE).
Non-functional¶
- Responsive down to tablet width; usable on a phone for the overview.
- Light and dark themes (follows the system preference).
- Live updates via SSE, with polling fallback.
- No external network calls (fonts and assets bundled).
- Accessible: keyboard navigation, sufficient contrast, no information conveyed by color alone.
Implementation notes¶
- Serving. The UI is served under
/ui/(/redirects there) withContent-Security-Policy: default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors 'none'(theCSPconstant ofinternal/ui), so that the browser itself refuses any external call;settings.server.ui.enabled: falseremoves it. Hashed assets are cached for a year,index.htmlis never cached. A binary built without the UI serves a page explainingmake ui. - Events.
GET /api/v1/eventsis a Server-Sent Events stream ofrun_finished(job, instance, result, duration, samples),plan_changed,circuit_changed(account, target, state) andconfig_reloaded(result, error), eachdataa JSON object withtime. A comment line is sent every 15 seconds to keep proxies from closing the stream. Slow clients lose events rather than slowing the collector: the UI refetches what it shows on every event and polls every 15 seconds when the stream is down. - Debug. With
settings.server.debug.enabled, the collector keeps, per job instance and in memory only, the pages of its last run (status, theContent-Typeand quota headers, the body truncated tomaxBodyBytes) and the samples it produced. Bodies go through the log redactor, so configured secrets and tokens never appear. Without it, the endpoint answers404with the codedebug_disabled. - Look. The colors, fonts and shapes follow tapyr.eu: background
#f2f8fb/#0b1824, surface#ffffff/#102335, ink#0f2a3f/#e4f1f7, cyan#3aa9d6/#5cc3ea, sun#ffc845/#ffd166, red#cc2f35/#ff7a7a; Bricolage Grotesque for titles, JetBrains Mono for data. States always carry a word or an icon next to their color.