mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
365 lines
11 KiB
Go
365 lines
11 KiB
Go
package controllers
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"fotbal-club/internal/config"
|
|
"fotbal-club/internal/services"
|
|
"fotbal-club/pkg/logger"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type UmamiController struct {
|
|
umamiService *services.UmamiService
|
|
}
|
|
|
|
func NewUmamiController() *UmamiController {
|
|
return &UmamiController{
|
|
umamiService: services.NewUmamiService(),
|
|
}
|
|
}
|
|
|
|
// resolveWebsiteID attempts to obtain a usable Umami website ID for requests.
|
|
// Preference order:
|
|
// 1. Cached value in configuration.
|
|
// 2. Match by configured frontend domain.
|
|
// 3. Fall back to the first website available in Umami.
|
|
func (uc *UmamiController) resolveWebsiteID() (string, error) {
|
|
if id := strings.TrimSpace(config.AppConfig.UmamiWebsiteID); id != "" {
|
|
logger.Info("Using cached Umami website ID: %s", id)
|
|
return id, nil
|
|
}
|
|
|
|
logger.Info("UMAMI_WEBSITE_ID is empty, attempting to auto-detect...")
|
|
|
|
if frontendURL := strings.TrimSpace(config.AppConfig.FrontendBaseURL); frontendURL != "" {
|
|
parsed, err := url.Parse(frontendURL)
|
|
if err != nil {
|
|
logger.Warn("Failed to parse FRONTEND_BASE_URL '%s': %v", frontendURL, err)
|
|
} else if host := parsed.Hostname(); host != "" {
|
|
logger.Info("Attempting to find Umami website by domain: %s", host)
|
|
id, err := uc.umamiService.FindWebsiteIDByDomain(host)
|
|
if err != nil {
|
|
logger.Warn("Failed to find website by domain '%s': %v", host, err)
|
|
} else if id != "" {
|
|
logger.Info("Found Umami website by domain match: %s", id)
|
|
config.AppConfig.UmamiWebsiteID = id
|
|
return id, nil
|
|
} else {
|
|
logger.Info("No Umami website found with domain '%s'", host)
|
|
}
|
|
}
|
|
}
|
|
|
|
logger.Info("Falling back to first available Umami website...")
|
|
id, err := uc.umamiService.GetDefaultWebsiteID()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
config.AppConfig.UmamiWebsiteID = id
|
|
logger.Info("Auto-detected and cached Umami website ID: %s", id)
|
|
return id, nil
|
|
}
|
|
|
|
// GetUmamiConfig returns the Umami configuration for the frontend
|
|
func (uc *UmamiController) GetUmamiConfig(c *gin.Context) {
|
|
if config.AppConfig.UmamiURL == "" || config.AppConfig.UmamiUsername == "" || config.AppConfig.UmamiPassword == "" {
|
|
reason := "No website ID configured - run initial setup"
|
|
switch {
|
|
case config.AppConfig.UmamiURL == "":
|
|
reason = "UMAMI_URL not set in .env"
|
|
case config.AppConfig.UmamiUsername == "":
|
|
reason = "UMAMI_USERNAME not set in .env"
|
|
case config.AppConfig.UmamiPassword == "":
|
|
reason = "UMAMI_PASSWORD not set in .env"
|
|
}
|
|
logger.Warn("Umami not configured: %s", reason)
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"enabled": false,
|
|
"website_id": "",
|
|
"script_url": "",
|
|
"reason": reason,
|
|
})
|
|
return
|
|
}
|
|
|
|
websiteID, err := uc.resolveWebsiteID()
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrUmamiNoWebsites) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"enabled": false,
|
|
"website_id": "",
|
|
"script_url": "",
|
|
"reason": "No websites found in Umami",
|
|
})
|
|
} else {
|
|
logger.Error("Failed to resolve Umami website ID: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to resolve Umami website ID"})
|
|
}
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"enabled": true,
|
|
"website_id": websiteID,
|
|
"script_url": config.AppConfig.UmamiURL + "/script.js",
|
|
})
|
|
}
|
|
|
|
// InitializeUmamiSetup is called during initial setup (no auth required).
|
|
// It auto-detects the domain from the Host header (works with Cloudflare Tunnel).
|
|
func (uc *UmamiController) InitializeUmamiSetup(c *gin.Context) {
|
|
if config.AppConfig.UmamiWebsiteID != "" {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Umami already configured",
|
|
"website_id": config.AppConfig.UmamiWebsiteID,
|
|
})
|
|
return
|
|
}
|
|
|
|
var payload struct {
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
|
|
return
|
|
}
|
|
|
|
if payload.Name == "" {
|
|
payload.Name = "Fotbal Club"
|
|
}
|
|
|
|
domain := c.Request.Host
|
|
if i := strings.Index(domain, ":"); i >= 0 {
|
|
domain = domain[:i]
|
|
}
|
|
|
|
logger.Info("Initializing Umami website: name='%s', domain='%s'", payload.Name, domain)
|
|
|
|
websiteID, err := uc.umamiService.EnsureWebsite(payload.Name, domain)
|
|
if err != nil {
|
|
logger.Error("Failed to create Umami website: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Umami website: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
config.AppConfig.UmamiWebsiteID = websiteID
|
|
logger.Info("Umami website created successfully: ID=%s", websiteID)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Umami initialized successfully",
|
|
"website_id": websiteID,
|
|
"script_url": config.AppConfig.UmamiURL + "/script.js",
|
|
"domain": domain,
|
|
})
|
|
}
|
|
|
|
// InitializeUmami sets up Umami tracking (admin endpoint with manual domain).
|
|
func (uc *UmamiController) InitializeUmami(c *gin.Context) {
|
|
if config.AppConfig.UmamiWebsiteID != "" {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Umami already configured",
|
|
"website_id": config.AppConfig.UmamiWebsiteID,
|
|
})
|
|
return
|
|
}
|
|
|
|
var payload struct {
|
|
Name string `json:"name"`
|
|
Domain string `json:"domain"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&payload); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request payload"})
|
|
return
|
|
}
|
|
|
|
if payload.Name == "" || payload.Domain == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Name and domain are required"})
|
|
return
|
|
}
|
|
|
|
websiteID, err := uc.umamiService.EnsureWebsite(payload.Name, payload.Domain)
|
|
if err != nil {
|
|
logger.Error("Failed to create Umami website: %v", err)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create Umami website: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
config.AppConfig.UmamiWebsiteID = websiteID
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Umami initialized successfully",
|
|
"website_id": websiteID,
|
|
"script_url": config.AppConfig.UmamiURL + "/script.js",
|
|
})
|
|
}
|
|
|
|
// GetStats returns analytics stats from Umami
|
|
func (uc *UmamiController) GetStats(c *gin.Context) {
|
|
// Check if Umami is configured
|
|
if config.AppConfig.UmamiURL == "" || config.AppConfig.UmamiUsername == "" || config.AppConfig.UmamiPassword == "" {
|
|
logger.Warn("Umami not configured - UMAMI_URL='%s', username empty=%v, password empty=%v",
|
|
config.AppConfig.UmamiURL,
|
|
config.AppConfig.UmamiUsername == "",
|
|
config.AppConfig.UmamiPassword == "")
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
return
|
|
}
|
|
|
|
websiteID, err := uc.resolveWebsiteID()
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrUmamiNoWebsites) {
|
|
logger.Warn("No Umami websites found in instance at %s - returning empty stats", config.AppConfig.UmamiURL)
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
} else {
|
|
logger.Error("Failed to resolve Umami website ID: %v", err)
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
}
|
|
return
|
|
}
|
|
logger.Info("Using Umami website ID: %s", websiteID)
|
|
|
|
// Get time range from query params (default to last 30 days)
|
|
days := 30
|
|
if d := c.Query("days"); d != "" {
|
|
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 {
|
|
days = parsed
|
|
}
|
|
}
|
|
endAt := time.Now().Unix() * 1000 // milliseconds
|
|
startAt := time.Now().AddDate(0, 0, -days).Unix() * 1000
|
|
|
|
stats, err := uc.umamiService.GetWebsiteStats(websiteID, startAt, endAt)
|
|
if err != nil {
|
|
logger.Error("Failed to get Umami stats for websiteID=%s (days=%d): %v", websiteID, days, err)
|
|
// Return empty stats instead of error
|
|
c.JSON(http.StatusOK, gin.H{})
|
|
return
|
|
}
|
|
|
|
logger.Info("Successfully fetched Umami stats for websiteID=%s (days=%d)", websiteID, days)
|
|
c.JSON(http.StatusOK, stats)
|
|
}
|
|
|
|
// GetMetrics returns specific metrics from Umami (pageviews, referrers, browsers, os, devices, countries, events)
|
|
func (uc *UmamiController) GetMetrics(c *gin.Context) {
|
|
// Check if Umami is configured
|
|
if config.AppConfig.UmamiURL == "" || config.AppConfig.UmamiUsername == "" || config.AppConfig.UmamiPassword == "" {
|
|
logger.Warn("Umami not configured - returning empty metrics")
|
|
c.JSON(http.StatusOK, []map[string]interface{}{})
|
|
return
|
|
}
|
|
|
|
websiteID, err := uc.resolveWebsiteID()
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrUmamiNoWebsites) {
|
|
logger.Warn("No Umami websites found - returning empty metrics")
|
|
c.JSON(http.StatusOK, []map[string]interface{}{})
|
|
} else {
|
|
logger.Error("Failed to resolve Umami website ID: %v", err)
|
|
c.JSON(http.StatusOK, []map[string]interface{}{})
|
|
}
|
|
return
|
|
}
|
|
|
|
metricType := c.Param("type")
|
|
if metricType == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "Metric type is required"})
|
|
return
|
|
}
|
|
|
|
days := 30
|
|
if d := c.Query("days"); d != "" {
|
|
if parsed, err := strconv.Atoi(d); err == nil && parsed > 0 {
|
|
days = parsed
|
|
}
|
|
}
|
|
|
|
endAt := time.Now().Unix() * 1000
|
|
startAt := time.Now().AddDate(0, 0, -days).Unix() * 1000
|
|
|
|
metrics, err := uc.umamiService.GetWebsiteMetrics(websiteID, metricType, startAt, endAt)
|
|
if err != nil {
|
|
logger.Error("Failed to get Umami metrics (websiteID=%s, type=%s, days=%d): %v", websiteID, metricType, days, err)
|
|
// Return empty metrics instead of error
|
|
c.JSON(http.StatusOK, []map[string]interface{}{})
|
|
return
|
|
}
|
|
|
|
logger.Info("Successfully fetched %d Umami metrics (websiteID=%s, type=%s, days=%d)", len(metrics), websiteID, metricType, days)
|
|
c.JSON(http.StatusOK, metrics)
|
|
}
|
|
|
|
// GetPageviews returns pageviews data over time from Umami
|
|
func (uc *UmamiController) GetPageviews(c *gin.Context) {
|
|
// Check if Umami is configured
|
|
if config.AppConfig.UmamiURL == "" || config.AppConfig.UmamiUsername == "" || config.AppConfig.UmamiPassword == "" {
|
|
logger.Warn("Umami not configured - returning empty pageviews")
|
|
c.JSON(http.StatusOK, []map[string]interface{}{})
|
|
return
|
|
}
|
|
|
|
websiteID, err := uc.resolveWebsiteID()
|
|
if err != nil {
|
|
if errors.Is(err, services.ErrUmamiNoWebsites) {
|
|
logger.Warn("No Umami websites found - returning empty pageviews")
|
|
c.JSON(http.StatusOK, []map[string]interface{}{})
|
|
} else {
|
|
logger.Error("Failed to resolve Umami website ID: %v", err)
|
|
c.JSON(http.StatusOK, []map[string]interface{}{})
|
|
}
|
|
return
|
|
}
|
|
|
|
days := 30
|
|
if d := c.Query("days"); d != "" {
|
|
if parsed, err := strconv.Atoi(d); err == nil && parsed >= 0 {
|
|
days = parsed
|
|
}
|
|
}
|
|
|
|
// Determine time unit based on range
|
|
unit := "day"
|
|
if days == 0 || days == 1 {
|
|
unit = "hour"
|
|
} else if days <= 90 {
|
|
unit = "day"
|
|
} else {
|
|
unit = "month"
|
|
}
|
|
|
|
// Calculate time range
|
|
var startAt, endAt int64
|
|
endDate := time.Now()
|
|
if days == 0 {
|
|
// Today: from midnight to now
|
|
startDate := time.Date(endDate.Year(), endDate.Month(), endDate.Day(), 0, 0, 0, 0, endDate.Location())
|
|
startAt = startDate.Unix() * 1000
|
|
endAt = endDate.Unix() * 1000
|
|
} else {
|
|
// Other ranges: from X days ago to now
|
|
startDate := endDate.AddDate(0, 0, -days)
|
|
startAt = startDate.Unix() * 1000
|
|
endAt = endDate.Unix() * 1000
|
|
}
|
|
|
|
logger.Info("Fetching pageviews: websiteID=%s, days=%d, unit=%s, startAt=%d, endAt=%d", websiteID, days, unit, startAt, endAt)
|
|
|
|
pageviews, err := uc.umamiService.GetWebsitePageviews(websiteID, startAt, endAt, unit)
|
|
if err != nil {
|
|
logger.Error("Failed to get Umami pageviews (websiteID=%s, days=%d, unit=%s): %v", websiteID, days, unit, err)
|
|
// Return empty array instead of error to prevent frontend crash
|
|
c.JSON(http.StatusOK, []map[string]interface{}{})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, pageviews)
|
|
}
|