- embedded React admin UI at /admin/ui (go:embed), login with ADMIN_SECRET via signed HttpOnly session cookie, X-ADMIN-KEY still works for scripts - admin API: overview stats, indexing queue, addon management (priority, reindex, delete/restore/purge, bulk), whitelist CRUD, duplicate hashes, traffic, metrics explorer, services, events/audit log, system info - built-in metrics: request latency per route, visitors, top addons and searches, indexer/scraper counters; minute buckets in the DB with hourly rollups and retention; optional Prometheus /metrics (METRICS_TOKEN) - POST /back/telemetry for indexers/scrapers (metrics, events, heartbeat) and a stdlib-only Go client in telemetry/ - auth hardening: empty secrets no longer allow access, constant-time compare, CORS not applied to /admin and /back, TRUSTED_PROXIES setting - Dockerfile builds the UI and no longer bakes config.yaml into the image; CI builds the UI, runs tests and builds arm64 with CGO
5.1 KiB
Admin dashboard, metrics and telemetry
The API binary serves an admin dashboard at /admin/ui/ (/admin redirects there).
It is a React app in web/, built into web/dist and embedded with go:embed.
Access
- Sign in with
ADMIN_SECRET. The server sets an HttpOnly,SameSite=Strictsession cookie (ADMIN_SESSION_TTL_HOURS, default 7 days). - Rotating
ADMIN_SECRETsigns every session out. - If
ADMIN_SECRETis empty, the dashboard and admin API are disabled. - Set
TRUSTED_PROXIESbehind Traefik/Dokploy. Without it, client IPs can be spoofed viaX-Forwarded-For, which affects visitor counts and login throttling.
Admin API
The JSON API lives under /admin/api. Scripts can call it with the
X-ADMIN-KEY: <ADMIN_SECRET> header instead of a cookie. Cookie requests that
change data must also send X-Admin-CSRF: 1.
| Area | Endpoints |
|---|---|
| Stats | GET overview, GET overview/duplicates, GET traffic?from&to, GET top?kind=addon|search&days |
| Queue | GET queue, POST queue/reset-stuck |
| Addons | GET addons?q&status&type&sort&order&limit&offset, GET addons/:id, GET addons/:id/files |
| Addon actions | POST addons/:id/{priority|to-be-indexed|reindex|reset-indexing|recheck|restore} with body {"value": bool}, DELETE addons/:id, DELETE addons/:id/purge?confirm=<id>, POST bulk/addons |
| Whitelist | GET whitelist, POST whitelist with body {"hashes":[], "text":"", "note":""}, DELETE whitelist/:id, GET hashes/:hash |
| Metrics | GET metrics/catalog, GET metrics/query?service&name&agg&groupBy&match&from&to&step |
| Services | GET services, GET services/events, DELETE services/:id |
| System | GET system, POST system/maintenance with body {"action": "refresh-stats|flush-metrics|rollup|purge-deleted-files"} |
The legacy endpoint POST /admin/addWhitelistedHash (form field hash, X-ADMIN-KEY) still works.
Metrics
- Every request is timed. Latency is recorded per route as a histogram, plus status class, caller group and web vs direct client.
- The handlers also emit metrics for indexer claims and submissions, job duration, scraper creates/updates/deletes, Discord webhooks and admin actions.
- Every minute, gauges sample the queue depth and total addon count.
Storage:
- Data is aggregated in memory per minute and flushed to the database every
minute (
metric_seriesandmetric_pointstables). - Minute data is rolled up hourly.
- Retention is configurable:
METRICS_MINUTE_RETENTION_DAYS(default 7) andMETRICS_HOUR_RETENTION_DAYS(default 180).
Website visitors are counted as a salted hash of IP and user agent. The salt
rotates daily and only lives in memory, so the count is approximate. The most
looked-up addons and search queries are stored per day in daily_top_items.
Prometheus:
- Set
METRICS_TOKENto exposeGET /metricsin the Prometheus text format. - Authenticate with
Authorization: Bearer <token>. - This lets you add an external Prometheus/Grafana later.
Telemetry from indexers and scrapers
Services report to POST /back/telemetry with X-SECRET-KEY. Every push is
also a heartbeat. An instance is shown as offline after three missed
heartbeatIntervalSec.
{
"service": "indexer", "instance": "indexer-1", "version": "1.4.0",
"startedAt": 1727950000, "heartbeatIntervalSec": 15, "status": "ok",
"metrics": [
{"name": "jobs_total", "type": "counter", "value": 3, "labels": {"result": "ok"}},
{"name": "data_window_bytes", "type": "gauge", "value": 7.1e9},
{"name": "download_seconds", "type": "summary", "count": 3, "sum": 41.2, "min": 8.1, "max": 20.3}
],
"events": [{"level": "warn", "message": "data limit reached", "fields": {"limitGB": 50}}]
}
Metric types:
counter: the increase since the last push.gauge: the current value.summary: pre-aggregated values from the client.
Limits:
- Names must match
^[a-z_][a-z0-9_]{0,63}$. - At most 8 labels per metric.
- At most 1000 metrics and 200 events per push.
- At most 200 series per metric and 5000 external series in total. Anything over these limits is dropped and counted.
A stdlib-only Go client is in telemetry/, as its own module:
import "gitea.tbdevent.eu/TBD/reforger_crawler_main/telemetry"
c := telemetry.New(telemetry.Config{BaseURL: apiURL, SecretKey: secret, Service: "indexer", Version: version})
c.Start(ctx)
defer c.Close(context.Background())
c.Counter("addons_indexed_total", "result", "ok").Inc()
c.Summary("download_seconds").Observe(took.Seconds())
c.Gauge("data_window_bytes").Set(float64(used))
c.Event(telemetry.Warn, "data limit reached", nil)
Reported metrics show up in the dashboard under Services and in the Metrics explorer.
Development
# API on SQLite (overrides config.yaml)
DB_DRIVER=sqlite SQLITE_PATH=dev.db SECRET_KEY=dev ADMIN_SECRET=devadminsecret1234 go run .
# UI with hot reload, proxied to :8083
npm --prefix web install
npm --prefix web run dev # http://localhost:5173/admin/ui/
# production build (needed before `go build` to embed the UI)
npm --prefix web run build
If the UI was not built, the binary still compiles and serves a placeholder page.