diff --git a/README.md b/README.md index e2f5ac3..f5d74e6 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ The server is configured using environment variables, the following options are | `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` | +| `BADGE_ENABLED` | boolean | Whether to enable the badge endpoint. | `true` | ## Contributing diff --git a/badge_handler.go b/badge_handler.go new file mode 100644 index 0000000..aabc97c --- /dev/null +++ b/badge_handler.go @@ -0,0 +1,56 @@ +package main + +import ( + "fmt" + "net/http" + + _ "embed" + + "github.com/go-chi/render" + "github.com/tinyauthapp/analytics/queries" +) + +type BadgeHandler struct { + queries *queries.Queries +} + +func NewBadgeHandler(queries *queries.Queries) *BadgeHandler { + return &BadgeHandler{ + queries: queries, + } +} + +type shieldsioData struct { + SchemaVersion int `json:"schemaVersion"` + Label string `json:"label"` + Message string `json:"message"` + Color string `json:"color,omitempty"` + LabelColor string `json:"labelColor,omitempty"` + IsError bool `json:"isError,omitempty"` + NamedLogo string `json:"namedLogo,omitempty"` + LogoSvg string `json:"logoSvg,omitempty"` + LogoColor string `json:"logoColor,omitempty"` + LogoSize string `json:"logoSize,omitempty"` + Style string `json:"style,omitempty"` +} + +func (h *BadgeHandler) Badge(w http.ResponseWriter, r *http.Request) { + instanceCount, err := h.queries.GetInstanceCount(r.Context()) + + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + return + } + + badgeData := shieldsioData{ + SchemaVersion: 1, + Label: "Active Instances", + Message: fmt.Sprintf("%d", instanceCount), + Color: "brightgreen", + LabelColor: "grey", + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + render.JSON(w, r, badgeData) +} diff --git a/dashboard_handler.go b/dashboard_handler.go index eae7a32..6eff73d 100644 --- a/dashboard_handler.go +++ b/dashboard_handler.go @@ -12,9 +12,6 @@ import ( //go:embed dashboard.html var dashboardTemplate string -//go:embed favicon.ico -var faviconData []byte - type DashboardHandler struct { queries *queries.Queries } @@ -88,15 +85,3 @@ func (h *DashboardHandler) Dashboard(w http.ResponseWriter, r *http.Request) { 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/main.go b/main.go index 29324e7..edee0d4 100644 --- a/main.go +++ b/main.go @@ -16,8 +16,13 @@ import ( "github.com/go-chi/cors" "github.com/spf13/viper" _ "modernc.org/sqlite" + + _ "embed" ) +//go:embed favicon.ico +var faviconData []byte + var version = "development" type Config struct { @@ -28,6 +33,7 @@ type Config struct { TrustedProxies []string `mapstructure:"trusted_proxies"` CORSAllowedOrigins []string `mapstructure:"cors_allowed_origins"` DashboardEnabled bool `mapstructure:"dashboard_enabled"` + BadgeEnabled bool `mapstructure:"badge_enabled"` } func main() { @@ -40,6 +46,7 @@ func main() { v.SetDefault("trusted_proxies", []string{""}) v.SetDefault("cors_allowed_origins", []string{"*"}) v.SetDefault("dashboard_enabled", true) + v.SetDefault("badge_enabled", true) v.AutomaticEnv() @@ -84,7 +91,6 @@ func main() { instancesHandler := NewInstancesHandler(queries) healthHandler := NewHealthHandler() - dashboardHandler := NewDashboardHandler(queries) router.Get("/v1/healthz", healthHandler.Health) @@ -100,12 +106,27 @@ func main() { r.Post("/v1/instances/heartbeat", instancesHandler.Heartbeat) }) + router.Get("/robots.txt", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.WriteHeader(http.StatusOK) + w.Write([]byte("User-agent: *\nDisallow: /")) + }) + if config.DashboardEnabled { + dashboardHandler := NewDashboardHandler(queries) router.Get("/dashboard", dashboardHandler.Dashboard) + router.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/x-icon") + w.WriteHeader(http.StatusOK) + w.Write(faviconData) + }) + } - router.Get("/favicon.ico", dashboardHandler.Favicon) - router.Get("/robots.txt", dashboardHandler.Robots) + if config.BadgeEnabled { + badgeHandler := NewBadgeHandler(queries) + router.Get("/v1/badge", badgeHandler.Badge) + } srv := &http.Server{ Addr: fmt.Sprintf("%s:%d", config.Address, config.Port), diff --git a/queries.sql b/queries.sql index 5d51645..5a5b99c 100644 --- a/queries.sql +++ b/queries.sql @@ -25,3 +25,6 @@ WHERE uuid = ?; DELETE FROM instances WHERE last_seen < ? OR version = '' OR uuid = '' RETURNING *; + +-- name: GetInstanceCount :one +SELECT COUNT(*) AS count FROM instances; diff --git a/queries/queries.sql.go b/queries/queries.sql.go index 814c155..f699d5b 100644 --- a/queries/queries.sql.go +++ b/queries/queries.sql.go @@ -106,6 +106,17 @@ func (q *Queries) GetInstance(ctx context.Context, uuid string) (Instance, error return i, err } +const getInstanceCount = `-- name: GetInstanceCount :one +SELECT COUNT(*) AS count FROM instances +` + +func (q *Queries) GetInstanceCount(ctx context.Context) (int64, error) { + row := q.db.QueryRowContext(ctx, getInstanceCount) + var count int64 + err := row.Scan(&count) + return count, err +} + const updateInstance = `-- name: UpdateInstance :exec UPDATE instances set last_seen = ?