From e5997c036d2dba3ca427debd74dfda94cc916f4d Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 18 Apr 2026 00:00:35 +0300 Subject: [PATCH 1/5] refactor: move dashboard out of separate package --- .gitignore | 9 +- .vscode/launch.json | 19 -- dashboard.go | 105 +++++++ dashboard/dashboard.html => dashboard.html | 180 +++++++----- dashboard/.gitignore | 2 - dashboard/Dockerfile | 33 --- dashboard/README.md | 34 --- dashboard/go.mod | 3 - dashboard/main.go | 269 ------------------ dashboard/favicon.ico => favicon.ico | Bin health_handler.go | 2 +- instances_handler.go | 2 +- main.go | 9 +- query.sql => queries.sql | 0 {database/queries => queries}/db.go | 0 {database/queries => queries}/models.go | 0 .../query.sql.go => queries/queries.sql.go | 2 +- seed.ts | 61 ++++ sqlc.yaml | 4 +- 19 files changed, 283 insertions(+), 451 deletions(-) delete mode 100644 .vscode/launch.json create mode 100644 dashboard.go rename dashboard/dashboard.html => dashboard.html (53%) delete mode 100644 dashboard/.gitignore delete mode 100644 dashboard/Dockerfile delete mode 100644 dashboard/README.md delete mode 100644 dashboard/go.mod delete mode 100644 dashboard/main.go rename dashboard/favicon.ico => favicon.ico (100%) rename query.sql => queries.sql (100%) rename {database/queries => queries}/db.go (100%) rename {database/queries => queries}/models.go (100%) rename database/queries/query.sql.go => queries/queries.sql.go (99%) create mode 100644 seed.ts diff --git a/.gitignore b/.gitignore index 5620294..b4fff02 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,8 @@ -# data -data - # debug __debug_* # build out -tinyauth-analytics +/analytics -# benchmarks -bench \ No newline at end of file +# data +/analytics.db* diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index d9d8835..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Package", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${fileDirname}", - "env": { - "DATABASE_PATH": "./data/analytics.db", - "PORT": "8080", - "ADDRESS": "0.0.0.0", - "TRUSTED_PROXIES": "0.0.0.0", - "RATE_LIMIT_COUNT": "3" - } - } - ] -} diff --git a/dashboard.go b/dashboard.go new file mode 100644 index 0000000..2428a9d --- /dev/null +++ b/dashboard.go @@ -0,0 +1,105 @@ +package main + +import ( + _ "embed" + "fmt" + "html/template" + "log" + "net/http" + + "github.com/tinyauthapp/analytics/queries" +) + +//go:embed dashboard.html +var dashboardTemplate string + +//go:embed favicon.ico +var faviconData []byte + +type DashboardHandler struct { + queries *queries.Queries +} + +type versionStats struct { + Total int + MostUsed string + VersionLabels []string + VersionValues []int +} + +func NewDashboardHandler(queries *queries.Queries) *DashboardHandler { + return &DashboardHandler{ + queries: queries, + } +} + +func (h *DashboardHandler) compileVersionStats(instances []queries.Instance) versionStats { + stats := make(map[string]int) + total := 0 + + for _, instance := range instances { + stats[instance.Version]++ + total++ + } + + mostUsed := "" + maxCount := 0 + + versionLabels := make([]string, 0, len(stats)) + versionValues := make([]int, 0, len(stats)) + + for version, count := range stats { + if count > maxCount { + maxCount = count + mostUsed = version + } + versionLabels = append(versionLabels, version) + versionValues = append(versionValues, count) + } + + return versionStats{ + Total: total, + MostUsed: mostUsed, + VersionLabels: versionLabels, + VersionValues: versionValues, + } +} + +func (h *DashboardHandler) Dashboard(w http.ResponseWriter, r *http.Request) { + instances, err := h.queries.GetAllInstances(r.Context()) + + if err != nil { + log.Printf("failed to get instances: %v", err) + http.Error(w, "Failed to retrieve instances", http.StatusInternalServerError) + return + } + + versionStats := h.compileVersionStats(instances) + + fmt.Println(versionStats) + + tmpl, err := template.New("dashboard").Parse(dashboardTemplate) + if err != nil { + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + err = tmpl.Execute(w, versionStats) + + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } +} + +func (h *DashboardHandler) Favicon(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/x-icon") + w.WriteHeader(http.StatusOK) + w.Write(faviconData) +} + +func (h *DashboardHandler) Robots(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte("User-agent: *\nDisallow: /")) +} diff --git a/dashboard/dashboard.html b/dashboard.html similarity index 53% rename from dashboard/dashboard.html rename to dashboard.html index 6336b7d..638221a 100644 --- a/dashboard/dashboard.html +++ b/dashboard.html @@ -7,6 +7,7 @@ Dashboard + -
+
-
-
+
+
Total instances
- {{.TotalInstances}} + {{.Total}}
Most used version
- {{.MostUsedVersion}} + {{.MostUsed}}
-
-
- - - - - - - - - - {{range .Instances}} - - - - - - {{end}} - -
- UUID - - Version - - Last Seen -
- {{.UUID}} - - {{.Version}} - - {{.LastSeen}} -
-
- {{if lt .NextPage .MaxPages }} - Load more... - {{end}} +
+
-

- Copyright © 2025 Tinyauth +

+ Copyright © 2026 Tinyauth

diff --git a/dashboard/.gitignore b/dashboard/.gitignore deleted file mode 100644 index ce16279..0000000 --- a/dashboard/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# build out -dashboard \ No newline at end of file diff --git a/dashboard/Dockerfile b/dashboard/Dockerfile deleted file mode 100644 index 699fd62..0000000 --- a/dashboard/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -# Builder -FROM golang:1.25-alpine3.21 AS builder - -ARG VERSION - -WORKDIR /dashboard - -COPY go.mod ./ - -RUN go mod download - -COPY ./main.go ./ -COPY ./dashboard.html ./ -COPY ./favicon.ico ./ - -RUN CGO_ENABLED=0 go build -o dashboard -ldflags "-s -w -X main.version=${VERSION}" - -# Runner -FROM alpine:3.22 AS runner - -WORKDIR /dashboard - -COPY --from=builder /dashboard/dashboard ./ - -RUN adduser -u 1000 -H -D dashboard - -EXPOSE 8080 - -USER dashboard - -ENV PATH=$PATH:/dashboard - -ENTRYPOINT ["dashboard"] diff --git a/dashboard/README.md b/dashboard/README.md deleted file mode 100644 index 45f66f6..0000000 --- a/dashboard/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Dashboard - -A simple server that periodically fetches data from the analytics server API and displays them in a simple dashboard. - -## Usage - -Build the binary with: - -```sh -go build . -``` - -And run with: - -``` -./dashboard -``` - -Then visit to see the analytics. - -> [!NOTE] -> A docker image is also available, check out the example [docker compose](../docker-compose.yml) file. - -## Configuration - -You can configure the server using environment variables, the following options are supported: - -| Name | Type | Description | Default | -| ------------------ | ------ | ------------------------------------------------ | -------------------------- | -| `PORT` | number | The port to run the server on. | `8080` | -| `ADDRESS` | string | The address to bind the server to. | `0.0.0.0` | -| `API_SERVER` | string | The analytics API server URL to fetch data from. | `https://api.tinyauth.app` | -| `PAGE_SIZE` | number | Number of instances to display per page. | `10` | -| `REFRESH_INTERVAL` | number | How often to refresh data from API (in minutes). | `30` | diff --git a/dashboard/go.mod b/dashboard/go.mod deleted file mode 100644 index bb28142..0000000 --- a/dashboard/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/tinyauthapp/analytics/dashboard - -go 1.24.3 diff --git a/dashboard/main.go b/dashboard/main.go deleted file mode 100644 index d3a65a5..0000000 --- a/dashboard/main.go +++ /dev/null @@ -1,269 +0,0 @@ -package main - -import ( - _ "embed" - "encoding/json" - "fmt" - "html/template" - "log" - "net/http" - "os" - "strconv" - "strings" - "time" -) - -// Variables -var version = "development" - -//go:embed dashboard.html -var dashboardTemplate string - -//go:embed favicon.ico -var faviconData []byte - -type instance struct { - UUID string `json:"uuid"` - Version string `json:"version"` - LastSeen int `json:"last_seen"` -} - -type instancesResponse struct { - Instances []instance `json:"instances"` - Total int `json:"total"` -} - -type dashboardData struct { - TotalInstances int - MostUsedVersion string - Instances []instance - MaxPages int - NextPage int -} - -type dashboardHandler struct { - apiServer string - pageSize int - refreshInterval int - totalInstances int - mostUsedVersion string - maxPages int - pages [][]instance -} - -func getInstances(api string) (instancesResponse, error) { - resp, err := http.Get(api + "/v1/instances/all") - - if err != nil { - return instancesResponse{}, err - } - - defer resp.Body.Close() - - var instancesResp instancesResponse - - err = json.NewDecoder(resp.Body).Decode(&instancesResp) - - if err != nil { - return instancesResponse{}, err - } - - return instancesResp, nil -} - -func parseInstancesToPages(instances []instance, pageSize int) [][]instance { - var pages [][]instance - - for pageSize < len(instances) { - instances, pages = instances[pageSize:], append(pages, instances[0:pageSize]) - } - - pages = append(pages, instances) - return pages -} - -func getMostUsedVersion(instances []instance) string { - versionCount := make(map[string]int) - - for _, instance := range instances { - versionCount[instance.Version]++ - } - - mostUsedVersion := "" - maxCount := 0 - - for version, count := range versionCount { - if count > maxCount { - maxCount = count - mostUsedVersion = version - } - } - - return mostUsedVersion -} - -func bundleInstances(instances [][]instance, pages int) []instance { - var bundled []instance - - for i := 0; i < pages && i < len(instances); i++ { - bundled = append(bundled, instances[i]...) - } - - return bundled -} - -func NewDashboardHandler(apiServer string, pageSize int, refreshInterval int) *dashboardHandler { - return &dashboardHandler{ - apiServer: apiServer, - pageSize: pageSize, - refreshInterval: refreshInterval, - } -} - -func (h *dashboardHandler) Init() error { - go func() { - ticker := time.NewTicker(time.Duration(h.refreshInterval) * time.Minute) - defer ticker.Stop() - - for ; true; <-ticker.C { - log.Print("refreshing dashboard data") - err := h.loadData() - if err != nil { - log.Printf("failed to refresh dashboard data: %v", err) - } - } - }() - - return nil -} - -func (h *dashboardHandler) loadData() error { - res, err := getInstances(h.apiServer) - - if err != nil { - return err - } - - h.totalInstances = res.Total - h.maxPages = res.Total / h.pageSize - h.pages = parseInstancesToPages(res.Instances, h.pageSize) - h.mostUsedVersion = getMostUsedVersion(res.Instances) - - if strings.TrimSpace(h.mostUsedVersion) == "" { - h.mostUsedVersion = "N/A" - } - - log.Printf("loaded %d instances, most used version: %s", h.totalInstances, h.mostUsedVersion) - - return nil -} - -func (h *dashboardHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - log.Println("received request for", r.URL.Path) - switch r.URL.Path { - case "/": - page := 0 - query := r.URL.Query() - - if val, ok := query["page"]; ok && len(val) > 0 { - var err error - page, err = strconv.Atoi(val[0]) - if err != nil || page < 0 || page >= len(h.pages) { - http.Error(w, "invalid page number", http.StatusBadRequest) - return - } - } - - tmpl, err := template.New("dashboard").Parse(dashboardTemplate) - if err != nil { - http.Error(w, "internal server error", http.StatusInternalServerError) - return - } - - err = tmpl.Execute(w, dashboardData{ - TotalInstances: h.totalInstances, - MostUsedVersion: h.mostUsedVersion, - Instances: bundleInstances(h.pages, page+1), - MaxPages: h.maxPages, - NextPage: page + 1, - }) - - if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - // No indexing for the analytics dashboard - case "/robots.txt": - w.Write([]byte("User-agent: *\nDisallow: /")) - case "/favicon.ico": - w.Header().Set("Content-Type", "image/x-icon") - w.Write(faviconData) - default: - http.NotFound(w, r) - } -} - -func main() { - log.Printf("tinyauth analytics dashboard version %s", version) - - port := os.Getenv("PORT") - - if port == "" { - port = "8080" - } - - address := os.Getenv("ADDRESS") - - if address == "" { - address = "0.0.0.0" - } - - apiServer := os.Getenv("API_SERVER") - - if apiServer == "" { - apiServer = "https://api.tinyauth.app" - } - - pageSize := 10 - pageSizeEnv := os.Getenv("PAGE_SIZE") - - if pageSizeEnv != "" { - ps, err := strconv.Atoi(pageSizeEnv) - if err != nil { - log.Printf("invalid PAGE_SIZE value, using default %d: %v", pageSize, err) - } else { - pageSize = ps - } - } - - refreshInterval := 30 - refreshIntervalEnv := os.Getenv("REFRESH_INTERVAL") - - if refreshIntervalEnv != "" { - ps, err := strconv.Atoi(refreshIntervalEnv) - if err != nil { - log.Printf("invalid REFRESH_INTERVAL value, using default %d: %v", refreshInterval, err) - } else { - refreshInterval = ps - } - } - - mux := http.NewServeMux() - - dashboardHandler := NewDashboardHandler(apiServer, pageSize, refreshInterval) - err := dashboardHandler.Init() - if err != nil { - log.Printf("failed to initialize dashboard handler: %v", err) - return - } - - mux.Handle("/", dashboardHandler) - - bind := fmt.Sprintf("%s:%s", address, port) - - log.Printf("starting server on %s", bind) - err = http.ListenAndServe(bind, mux) - if err != nil { - log.Printf("server error: %v", err) - } -} diff --git a/dashboard/favicon.ico b/favicon.ico similarity index 100% rename from dashboard/favicon.ico rename to favicon.ico diff --git a/health_handler.go b/health_handler.go index 2a96028..4fa9f55 100644 --- a/health_handler.go +++ b/health_handler.go @@ -12,7 +12,7 @@ func NewHealthHandler() *HealthHandler { return &HealthHandler{} } -func (h *HealthHandler) health(w http.ResponseWriter, r *http.Request) { +func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) render.JSON(w, r, map[string]string{ "status": "200", diff --git a/instances_handler.go b/instances_handler.go index eeb8752..cec3167 100644 --- a/instances_handler.go +++ b/instances_handler.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/tinyauthapp/analytics/database/queries" + "github.com/tinyauthapp/analytics/queries" "github.com/go-chi/render" ) diff --git a/main.go b/main.go index 3d14723..3871fa1 100644 --- a/main.go +++ b/main.go @@ -9,7 +9,7 @@ import ( "os" "time" - "github.com/tinyauthapp/analytics/database/queries" + "github.com/tinyauthapp/analytics/queries" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -80,8 +80,9 @@ func main() { instancesHandler := NewInstancesHandler(queries) healthHandler := NewHealthHandler() + dashboardHandler := NewDashboardHandler(queries) - router.Get("/v1/healthz", healthHandler.health) + router.Get("/v1/healthz", healthHandler.Health) router.Group(func(r chi.Router) { r.Use(cors.Handler(cors.Options{ @@ -95,6 +96,10 @@ func main() { r.Post("/v1/instances/heartbeat", instancesHandler.Heartbeat) }) + router.Get("/dashboard", dashboardHandler.Dashboard) + router.Get("/favicon.txt", dashboardHandler.Favicon) + router.Get("/robots.txt", dashboardHandler.Robots) + srv := &http.Server{ Addr: fmt.Sprintf("%s:%d", config.Address, config.Port), Handler: router, diff --git a/query.sql b/queries.sql similarity index 100% rename from query.sql rename to queries.sql diff --git a/database/queries/db.go b/queries/db.go similarity index 100% rename from database/queries/db.go rename to queries/db.go diff --git a/database/queries/models.go b/queries/models.go similarity index 100% rename from database/queries/models.go rename to queries/models.go diff --git a/database/queries/query.sql.go b/queries/queries.sql.go similarity index 99% rename from database/queries/query.sql.go rename to queries/queries.sql.go index 6182917..814c155 100644 --- a/database/queries/query.sql.go +++ b/queries/queries.sql.go @@ -1,7 +1,7 @@ // Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.30.0 -// source: query.sql +// source: queries.sql package queries diff --git a/seed.ts b/seed.ts new file mode 100644 index 0000000..2398df8 --- /dev/null +++ b/seed.ts @@ -0,0 +1,61 @@ +import { Database } from "bun:sqlite"; +import { randomUUID } from "crypto"; + +const db = new Database("analytics.db"); + +// Create table if it doesn't exist +db.exec(` + CREATE TABLE IF NOT EXISTS instances ( + uuid TEXT PRIMARY KEY, + version TEXT NOT NULL, + last_seen INTEGER NOT NULL + ) +`); + +const versions = [ + { version: "v4.1.0", count: 1988 }, + { version: "v4.0.1", count: 267 }, + { version: "v4.0.0", count: 21 }, + { version: "v4.1.0-beta.1", count: 4 }, + { version: "development", count: 8 }, + { version: "v4.0.1-beta.1", count: 1 }, + { version: "4.1.0", count: 1 }, + { version: "v4.1.0-custom-2", count: 3 }, + { version: "nightly", count: 2 }, + { version: "v5.0.0-alpha.1", count: 1 }, + { version: "v5.0.0-beta.3", count: 1 }, + { version: "v5.0.0", count: 43 }, + { version: "v5.0.1", count: 88 }, + { version: "main", count: 1 }, + { version: "v5.0.2", count: 77 }, + { version: "v5.0.3", count: 20 }, + { version: "v5.0.4", count: 410 }, + { version: "v5.0.5", count: 13 }, + { version: "v5.0.6-beta.1", count: 1 }, + { version: "v5.0.6", count: 284 }, +]; + +// Clear existing data +db.exec("DELETE FROM instances"); + +// Insert data +const insert = db.prepare( + `INSERT INTO instances (uuid, version, last_seen) VALUES (?, ?, ?)`, +); + +let totalInstances = 0; +const now = Math.floor(Date.now() / 1000); + +versions.forEach(({ version, count }) => { + for (let i = 0; i < count; i++) { + const uuid = randomUUID(); + const lastSeen = now - Math.floor(Math.random() * 86400 * 30); // Random time in last 30 days + insert.run(uuid, version, lastSeen); + totalInstances++; + } +}); + +console.log(`✓ Seeded ${totalInstances} instances`); +console.log(`✓ Across ${versions.length} versions`); + +db.close(); diff --git a/sqlc.yaml b/sqlc.yaml index d931792..4862e00 100644 --- a/sqlc.yaml +++ b/sqlc.yaml @@ -1,12 +1,12 @@ version: "2" sql: - engine: "sqlite" - queries: "query.sql" + queries: "queries.sql" schema: "schema.sql" gen: go: package: "queries" - out: "database/queries" + out: "queries" overrides: - column: "instances.uuid" go_struct_tag: json:"uuid" From e9ab0da893f73f72b467c48f12de3512d888e01c Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 18 Apr 2026 16:14:34 +0300 Subject: [PATCH 2/5] feat: add option to disable dashboard --- dashboard.html | 8 +++---- main.go | 7 +++++- seed.ts | 61 -------------------------------------------------- 3 files changed, 9 insertions(+), 67 deletions(-) delete mode 100644 seed.ts diff --git a/dashboard.html b/dashboard.html index 638221a..f821257 100644 --- a/dashboard.html +++ b/dashboard.html @@ -43,14 +43,10 @@ .stat-card:hover { border-color: var(--color-fd-blue); transform: translateY(-2px); - background-color: #1a1a1a; } .stat-value { transition: color 0.2s ease; } - .stat-card:hover .stat-value { - color: var(--color-fd-blue); - } .chart-wrapper { position: relative; width: 100%; @@ -129,7 +125,9 @@
-
+
diff --git a/main.go b/main.go index 3871fa1..c99c3ba 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ type Config struct { DatabasePath string `mapstructure:"database_path"` TrustedProxies []string `mapstructure:"trusted_proxies"` CORSAllowedOrigins []string `mapstructure:"cors_allowed_origins"` + DashboardEnabled bool `mapstructure:"dashboard_enabled"` } func main() { @@ -36,6 +37,7 @@ func main() { v.SetDefault("database_path", "analytics.db") v.SetDefault("trusted_proxies", []string{""}) v.SetDefault("cors_allowed_origins", []string{"*"}) + v.SetDefault("dashboard_enabled", true) v.AutomaticEnv() @@ -96,7 +98,10 @@ func main() { r.Post("/v1/instances/heartbeat", instancesHandler.Heartbeat) }) - router.Get("/dashboard", dashboardHandler.Dashboard) + if config.DashboardEnabled { + router.Get("/dashboard", dashboardHandler.Dashboard) + } + router.Get("/favicon.txt", dashboardHandler.Favicon) router.Get("/robots.txt", dashboardHandler.Robots) diff --git a/seed.ts b/seed.ts deleted file mode 100644 index 2398df8..0000000 --- a/seed.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Database } from "bun:sqlite"; -import { randomUUID } from "crypto"; - -const db = new Database("analytics.db"); - -// Create table if it doesn't exist -db.exec(` - CREATE TABLE IF NOT EXISTS instances ( - uuid TEXT PRIMARY KEY, - version TEXT NOT NULL, - last_seen INTEGER NOT NULL - ) -`); - -const versions = [ - { version: "v4.1.0", count: 1988 }, - { version: "v4.0.1", count: 267 }, - { version: "v4.0.0", count: 21 }, - { version: "v4.1.0-beta.1", count: 4 }, - { version: "development", count: 8 }, - { version: "v4.0.1-beta.1", count: 1 }, - { version: "4.1.0", count: 1 }, - { version: "v4.1.0-custom-2", count: 3 }, - { version: "nightly", count: 2 }, - { version: "v5.0.0-alpha.1", count: 1 }, - { version: "v5.0.0-beta.3", count: 1 }, - { version: "v5.0.0", count: 43 }, - { version: "v5.0.1", count: 88 }, - { version: "main", count: 1 }, - { version: "v5.0.2", count: 77 }, - { version: "v5.0.3", count: 20 }, - { version: "v5.0.4", count: 410 }, - { version: "v5.0.5", count: 13 }, - { version: "v5.0.6-beta.1", count: 1 }, - { version: "v5.0.6", count: 284 }, -]; - -// Clear existing data -db.exec("DELETE FROM instances"); - -// Insert data -const insert = db.prepare( - `INSERT INTO instances (uuid, version, last_seen) VALUES (?, ?, ?)`, -); - -let totalInstances = 0; -const now = Math.floor(Date.now() / 1000); - -versions.forEach(({ version, count }) => { - for (let i = 0; i < count; i++) { - const uuid = randomUUID(); - const lastSeen = now - Math.floor(Math.random() * 86400 * 30); // Random time in last 30 days - insert.run(uuid, version, lastSeen); - totalInstances++; - } -}); - -console.log(`✓ Seeded ${totalInstances} instances`); -console.log(`✓ Across ${versions.length} versions`); - -db.close(); From 3f02ca53f7eda491822aa223c090a7e0a11545c9 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 18 Apr 2026 16:21:57 +0300 Subject: [PATCH 3/5] chore: update dockerfile and compose --- .gitignore | 1 + Dockerfile | 5 ++++- LICENSE | 2 +- dashboard.go => dashboard_handler.go | 2 +- docker-compose.dev.yml | 21 ++------------------- docker-compose.yml | 18 ++---------------- main.go | 4 +++- 7 files changed, 14 insertions(+), 39 deletions(-) rename dashboard.go => dashboard_handler.go (99%) diff --git a/.gitignore b/.gitignore index b4fff02..d7071db 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ __debug_* # data /analytics.db* +/data diff --git a/Dockerfile b/Dockerfile index 02994b1..eb43950 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,11 +11,14 @@ COPY go.sum ./ RUN go mod download COPY ./cache.go ./ +COPY ./dashboard.html ./ +COPY ./dashboard_handler.go ./ +COPY ./favicon.ico ./ COPY ./health_handler.go ./ COPY ./instances_handler.go ./ COPY ./main.go ./ COPY ./rate_limiter.go ./ -COPY ./database ./database +COPY ./queries ./queries RUN CGO_ENABLED=0 go build -o analytics -ldflags "-s -w -X main.version=${VERSION}" diff --git a/LICENSE b/LICENSE index e13034d..68ee9cf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2025 steveiliop56 +Copyright 2026 steveiliop56 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/dashboard.go b/dashboard_handler.go similarity index 99% rename from dashboard.go rename to dashboard_handler.go index 2428a9d..6e88a14 100644 --- a/dashboard.go +++ b/dashboard_handler.go @@ -42,7 +42,7 @@ func (h *DashboardHandler) compileVersionStats(instances []queries.Instance) ver total++ } - mostUsed := "" + mostUsed := "unkown" maxCount := 0 versionLabels := make([]string, 0, len(stats)) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1af6aa2..6c671d3 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,5 +1,5 @@ services: - tinyauth-analytics: + analytics: build: context: . dockerfile: Dockerfile @@ -12,26 +12,9 @@ services: - RATE_LIMIT_COUNT=15 - CORS_ALLOWED_ORIGINS=* - TRUSTED_PROXIES=0.0.0.0 + - DASHBOARD_ENABLED=true volumes: - ./data:/data ports: - 8080:8080 restart: unless-stopped - - # analytics-dashboard: - # build: - # context: ./dashboard - # dockerfile: Dockerfile - # args: - # - VERSION=development - # environment: - # - PORT=8090 - # - ADDRESS=0.0.0.0 - # - API_SERVER=http://tinyauth-analytics:8080 - # - PAGE_SIZE=10 - # - REFRESH_INTERVAL=1 - # ports: - # - 8090:8090 - # restart: unless-stopped - # depends_on: - # - tinyauth-analytics diff --git a/docker-compose.yml b/docker-compose.yml index 145aabc..7ed94dc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: - tinyauth-analytics: - image: ghcr.io/steveiliop56/tinyauth-analytics:v1 + analytics: + image: ghcr.io/tinyauthapp/analytics:v1 environment: - PORT=8080 - ADDRESS=0.0.0.0 @@ -10,17 +10,3 @@ services: ports: - 8080:8080 restart: unless-stopped - - # Uncomment for the analytics dashboard - # analytics-dashboard: - # image: ghcr.io/steveiliop56/tinyauth-analytics-dashboard:v1 - # environment: - # - PORT=8090 - # - ADDRESS=0.0.0.0 - # - API_SERVER=http://tinyauth-analytics:8080 - # - REFRESH_INTERVAL=30 - # ports: - # - 8090:8090 - # restart: unless-stopped - # depends_on: - # - tinyauth-analytics diff --git a/main.go b/main.go index c99c3ba..985e3fb 100644 --- a/main.go +++ b/main.go @@ -18,6 +18,8 @@ import ( _ "modernc.org/sqlite" ) +var version = "development" + type Config struct { Port int `mapstructure:"port"` Address string `mapstructure:"address"` @@ -50,7 +52,7 @@ func main() { os.Exit(1) } - slog.Info("starting tinyauth analytics", "config", config) + slog.Info("starting tinyauth analytics", "version", version, "config", config) sqlDb, err := sql.Open("sqlite", config.DatabasePath) From d3064da114ece868fc39e760b05aec65a8f80772 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 18 Apr 2026 16:25:37 +0300 Subject: [PATCH 4/5] docs: update readme --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 53d92c0..6294c0a 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ A simple server to transparently collect version information from Tinyauth instances. -## How does it work +## How it works -Every Tinyauth instance runs a goroutine (unless you choose to opt-out) that does a "heartbeat" every 12 hours indicating the instance is still alive. The heartbeat contains the UUID generated by Tinyauth on start up and the version information. The server stores them in the SQLite database alongside with the last seen date. When you request all the instances, the server responds with an array containing the versions, UUIDs and last seen dates. +Every Tinyauth instance runs a goroutine (unless you choose to opt-out) that does a "heartbeat" every 12 hours indicating the instance is still "alive". The heartbeat contains the UUID generated by Tinyauth on start up and the version information. The server stores them in the SQLite database alongside with the last seen date. When you request all the instances, the server responds with an array containing the versions, UUIDs and last seen dates. ## Running -The central information server is hosted at `https://api.tinyauth.app` and all instance information can be requested from the `/v1/instances/all` endpoint. But, if you like, you can run your own server for your own projects, you can do so by simply cloning the repository and running: +The central information server is hosted at `https://api.tinyauth.app` and all instance information can be requested from the `/v1/instances/all` endpoint or be seen visually on the `/dashboard` page. If you like, you can run your own server for your own projects. This can be done by cloning the repository and running: ```sh docker compose up -d @@ -26,6 +26,7 @@ The server is configured using environment variables, the following options are | `RATE_LIMIT_COUNT` | number | Maximum number of requests per minute per IP. | `3` | | `CORS_ALLOWED_ORIGINS` | string/array | Comma-separated list of allowed CORS origins. | `*` | | `TRUSTED_PROXIES` | string/array | Comma-separated list of trusted proxy IPs. | `` | +| `DASHBOARD_ENABLED` | boolean | Whether to enable the dashboard. | `true` | ## Contributing @@ -33,4 +34,4 @@ If you like you can contribute to this project by picking up an [issue](https:// ## License -Tinyauth analytics is licensed under the MIT License. TL;DR — You can use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software. Just make sure to include the original license in any substantial portions of the code. There’s no warranty — use at your own risk. See the [LICENSE](./LICENSE) file for full details. +Tinyauth analytics is licensed under the MIT License. TL;DR — You can use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the software. Just make sure to include the original license in any substantial portions of the code. There’s no warranty — use at your own risk. See the [LICENSE](LICENSE) file for full details. From ac2ad812cc7f236efab8d8b9030ad5ef7dab593e Mon Sep 17 00:00:00 2001 From: Stavros Date: Sat, 18 Apr 2026 16:28:04 +0300 Subject: [PATCH 5/5] chore: update release workflows --- .github/workflows/release.yml | 155 +++------------------------------- 1 file changed, 11 insertions(+), 144 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f8e394d..f0531f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics + images: ghcr.io/${{ github.repository_owner }}/analytics - name: Login to GitHub Container Registry uses: docker/login-action@v3 @@ -34,7 +34,7 @@ jobs: with: platforms: linux/amd64 labels: ${{ steps.meta.outputs.labels }} - tags: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics + tags: ghcr.io/${{ github.repository_owner }}/analytics outputs: type=image,push-by-digest=true,name-canonical=true,push=true build-args: | VERSION=${{ github.ref_name }} @@ -53,54 +53,6 @@ jobs: if-no-files-found: error retention-days: 1 - image-build-dashboard: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v6 - with: - images: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics-dashboard - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Build and push - uses: docker/build-push-action@v7 - id: build - with: - platforms: linux/amd64 - labels: ${{ steps.meta.outputs.labels }} - tags: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics-dashboard - outputs: type=image,push-by-digest=true,name-canonical=true,push=true - context: ./dashboard - build-args: | - VERSION=${{ github.ref_name }} - - - name: Export digest - run: | - mkdir -p ${{ runner.temp }}/digests - digest="${{ steps.build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v7 - with: - name: dashboard-digests-linux-amd64 - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 - image-build-arm: runs-on: ubuntu-24.04-arm steps: @@ -111,7 +63,7 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics + images: ghcr.io/${{ github.repository_owner }}/analytics - name: Login to GitHub Container Registry uses: docker/login-action@v3 @@ -129,7 +81,7 @@ jobs: with: platforms: linux/arm64 labels: ${{ steps.meta.outputs.labels }} - tags: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics + tags: ghcr.io/${{ github.repository_owner }}/analytics outputs: type=image,push-by-digest=true,name-canonical=true,push=true build-args: | VERSION=${{ github.ref_name }} @@ -148,54 +100,6 @@ jobs: if-no-files-found: error retention-days: 1 - image-build-arm-dashboard: - runs-on: ubuntu-24.04-arm - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v6 - with: - images: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics-dashboard - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Build and push - uses: docker/build-push-action@v7 - id: build - with: - platforms: linux/arm64 - labels: ${{ steps.meta.outputs.labels }} - tags: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics-dashboard - outputs: type=image,push-by-digest=true,name-canonical=true,push=true - context: ./dashboard - build-args: | - VERSION=${{ github.ref_name }} - - - name: Export digest - run: | - mkdir -p ${{ runner.temp }}/digests - digest="${{ steps.build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v7 - with: - name: dashboard-digests-linux-arm64 - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 - image-merge: runs-on: ubuntu-latest needs: @@ -223,53 +127,16 @@ jobs: id: meta uses: docker/metadata-action@v6 with: - images: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics - tags: | - type=semver,pattern={{version}},prefix=v - type=semver,pattern={{major}},prefix=v - type=semver,pattern={{major}}.{{minor}},prefix=v - - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/digests - run: | - docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth-analytics@sha256:%s ' *) - - image-merge-dashboard: - runs-on: ubuntu-latest - needs: - - image-build-dashboard - - image-build-arm-dashboard - steps: - - name: Download digests - uses: actions/download-artifact@v4 - with: - path: ${{ runner.temp }}/digests - pattern: dashboard-digests-* - merge-multiple: true - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v6 - with: - images: ghcr.io/${{ github.repository_owner }}/tinyauth-analytics-dashboard + images: ghcr.io/${{ github.repository_owner }}/analytics + flavor: | + prefix=v,onlatest=false tags: | - type=semver,pattern={{version}},prefix=v - type=semver,pattern={{major}},prefix=v - type=semver,pattern={{major}}.{{minor}},prefix=v + type=semver,pattern={{version}} + type=semver,pattern={{major}} + type=semver,pattern={{major}}.{{minor}} - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests run: | docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ - $(printf 'ghcr.io/${{ github.repository_owner }}/tinyauth-analytics-dashboard@sha256:%s ' *) + $(printf 'ghcr.io/${{ github.repository_owner }}/analytics@sha256:%s ' *)