Files
ilbinek 748db94d55
Cross Compile Go / build (push) Canceled after 0s
shared hashes: separate background job with visible state
- compute duplicate-hash statistics in their own job (startup, hourly,
  on demand) so they no longer block the table stats
- single-pass query over DISTINCT (hash, addon_id), 15 min timeout
- record errors and panics; never stay stuck in a computing state
- whitelist page polls while computing, shows errors and a Recompute
  button; tab is linkable via ?tab=shared
2026-10-04 03:16:29 +02:00

5.2 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=Strict session cookie (ADMIN_SESSION_TTL_HOURS, default 7 days).
  • Rotating ADMIN_SECRET signs every session out.
  • If ADMIN_SECRET is empty, the dashboard and admin API are disabled.
  • Set TRUSTED_PROXIES behind Traefik/Dokploy. Without it, client IPs can be spoofed via X-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 (job state + top shared hashes), POST overview/duplicates/refresh, 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_series and metric_points tables).
  • Minute data is rolled up hourly.
  • Retention is configurable: METRICS_MINUTE_RETENTION_DAYS (default 7) and METRICS_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_TOKEN to expose GET /metrics in 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.