admin dashboard, metrics and telemetry
Cross Compile Go / build (push) Canceled after 0s

- 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
This commit is contained in:
ilbinek committed 2026-10-03 18:10:57 +02:00
1 parent 0ef27b0072
commit 56d9e10621
79 files changed
+13146 -118

No files matched your search

+1
View File
@@ -27,6 +27,7 @@ type Addon struct {
PriorityIndexing bool `json:"priorityIndexing" gorm:"default:false"`
AddonFiles []AddonFile `json:"addonFiles"`
Recheck bool `json:"recheck" gorm:"default:false"`
LastIndexedAt *time.Time `json:"lastIndexedAt"`
Preview string `json:"preview"`
Author string `json:"author"`
+74
View File
@@ -0,0 +1,74 @@
package models
import "time"
// All timestamps in the metrics tables are int64 unix seconds (events: unix
// milliseconds) so range queries behave the same on SQLite and Postgres.
// MetricSeries identifies one time series: a metric name plus a fixed label set
// reported by one service instance.
type MetricSeries struct {
ID uint `gorm:"primaryKey" json:"id"`
Service string `gorm:"size:64;not null;uniqueIndex:ux_metric_series,priority:1" json:"service"`
Instance string `gorm:"size:128;not null;uniqueIndex:ux_metric_series,priority:2" json:"instance"`
Name string `gorm:"size:96;not null;uniqueIndex:ux_metric_series,priority:3;index" json:"name"`
Labels string `gorm:"size:512;not null;uniqueIndex:ux_metric_series,priority:4" json:"labels"` // canonical JSON object
Kind string `gorm:"size:16;not null" json:"kind"` // counter|gauge|summary|histogram
Bounds string `gorm:"type:text" json:"bounds"` // JSON []float64, histograms only
Help string `gorm:"size:256" json:"help"`
CreatedAt time.Time
LastSeen int64 `gorm:"index" json:"lastSeen"`
}
// MetricPoint is one aggregated bucket of a series. Resolution is the bucket
// width in seconds (60 = raw minute data, 3600 = hourly rollup).
type MetricPoint struct {
SeriesID uint `gorm:"primaryKey;autoIncrement:false"`
Resolution int `gorm:"primaryKey;autoIncrement:false;index:idx_metric_points_res_bucket,priority:1"`
Bucket int64 `gorm:"primaryKey;autoIncrement:false;index:idx_metric_points_res_bucket,priority:2"`
Count int64 `gorm:"not null;default:0"`
Sum float64 `gorm:"not null;default:0"`
Min float64 `gorm:"not null;default:0"`
Max float64 `gorm:"not null;default:0"`
Last float64 `gorm:"not null;default:0"`
Hist string `gorm:"type:text"` // JSON []uint64, histograms only
}
// ServiceInstance is one running process (indexer, scraper, the API itself)
// that reports telemetry. Every telemetry push counts as a heartbeat.
type ServiceInstance struct {
ID uint `gorm:"primaryKey" json:"id"`
Service string `gorm:"size:64;not null;uniqueIndex:ux_service_instance,priority:1" json:"service"`
Instance string `gorm:"size:128;not null;uniqueIndex:ux_service_instance,priority:2" json:"instance"`
Version string `gorm:"size:64" json:"version"`
Status string `gorm:"size:16" json:"status"` // self-reported: ok|degraded|error
StatusMessage string `gorm:"size:512" json:"statusMessage"`
StartedAt int64 `json:"startedAt"`
LastSeen int64 `gorm:"index" json:"lastSeen"`
HeartbeatIntervalSec int `json:"heartbeatIntervalSec"`
RemoteAddr string `gorm:"size:64" json:"remoteAddr"`
Meta string `gorm:"type:text" json:"meta"` // JSON object
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
// ServiceEvent is a log line reported by a service. Admin actions are recorded
// here too (service "main", fields.kind "audit").
type ServiceEvent struct {
ID uint `gorm:"primaryKey" json:"id"`
Service string `gorm:"size:64;index:idx_service_events_svc_ts,priority:1" json:"service"`
Instance string `gorm:"size:128" json:"instance"`
Level string `gorm:"size:8;index" json:"level"` // debug|info|warn|error
Message string `gorm:"size:1024" json:"message"`
Fields string `gorm:"type:text" json:"fields"` // JSON object
TS int64 `gorm:"index:idx_service_events_svc_ts,priority:2;index" json:"ts"` // unix ms
}
// DailyTopItem counts how often something was requested on one UTC day
// (Kind "addon": addon lookups, Kind "search": search queries).
type DailyTopItem struct {
Day string `gorm:"primaryKey;size:10"` // YYYY-MM-DD
Kind string `gorm:"primaryKey;size:16"`
Item string `gorm:"primaryKey;size:128"`
Count int64
}
+39
View File
@@ -0,0 +1,39 @@
package models
// TelemetryBatch is the payload of POST /back/telemetry. The client lives in
// the telemetry/ module; keep both definitions in sync.
type TelemetryBatch struct {
Service string `json:"service"`
Instance string `json:"instance"`
Version string `json:"version"`
StartedAt int64 `json:"startedAt"`
HeartbeatIntervalSec int `json:"heartbeatIntervalSec"`
Status string `json:"status"`
StatusMessage string `json:"statusMessage"`
Meta map[string]any `json:"meta"`
Metrics []TelemetryMetric `json:"metrics"`
Events []TelemetryEvent `json:"events"`
}
// TelemetryMetric is one sample. Type "counter" carries a delta since the last
// push, "gauge" the current value, "summary" client-side pre-aggregated
// Count/Sum/Min/Max. Ts is unix seconds or milliseconds, optional.
type TelemetryMetric struct {
Name string `json:"name"`
Type string `json:"type"`
Help string `json:"help"`
Value float64 `json:"value"`
Count int64 `json:"count"`
Sum float64 `json:"sum"`
Min float64 `json:"min"`
Max float64 `json:"max"`
Labels map[string]string `json:"labels"`
Ts int64 `json:"ts"`
}
type TelemetryEvent struct {
Level string `json:"level"`
Message string `json:"message"`
Ts int64 `json:"ts"`
Fields map[string]any `json:"fields"`
}
+2 -1
View File
@@ -4,5 +4,6 @@ import "gorm.io/gorm"
type WhitelistedHash struct {
gorm.Model
Hash string `json:"hash"`
Hash string `json:"hash"`
Note string `json:"note" gorm:"size:256"`
}