This commit is contained in:
Tomas Dvorak
2026-01-26 08:13:18 +01:00
parent aa036b6550
commit dfc079288f
505 changed files with 95755 additions and 5712 deletions
+74 -72
View File
@@ -35,13 +35,13 @@ func (ac *AnalyticsController) resolveWebsiteID() (string, error) {
if id := strings.TrimSpace(config.AppConfig.UmamiWebsiteID); id != "" {
return id, nil
}
// Try to get the first available website
id, err := ac.umamiService.GetDefaultWebsiteID()
if err != nil {
return "", err
}
config.AppConfig.UmamiWebsiteID = id
return id, nil
}
@@ -54,13 +54,13 @@ func getClientIP(c *gin.Context) string {
ips := strings.Split(xff, ",")
return strings.TrimSpace(ips[0])
}
// Check X-Real-IP header
xri := c.GetHeader("X-Real-IP")
if xri != "" {
return xri
}
// Fall back to RemoteAddr
return c.ClientIP()
}
@@ -108,32 +108,32 @@ func (ac *AnalyticsController) GetAnalytics(c *gin.Context) {
// Get users stats
var totalUsers int64
ac.DB.Model(&models.User{}).Count(&totalUsers)
// Get new users this week
weekAgo := time.Now().AddDate(0, 0, -7)
var newUsersThisWeek int64
ac.DB.Model(&models.User{}).Where("created_at >= ?", weekAgo).Count(&newUsersThisWeek)
// Get events stats
var totalEvents int64
ac.DB.Model(&models.Event{}).Count(&totalEvents)
// Get upcoming events (events with start_time in the future)
now := time.Now()
var upcomingEvents int64
ac.DB.Model(&models.Event{}).Where("start_time > ?", now).Count(&upcomingEvents)
// Get articles stats
var totalArticles int64
ac.DB.Model(&models.Article{}).Count(&totalArticles)
var publishedArticles int64
ac.DB.Model(&models.Article{}).Where("published = ?", true).Count(&publishedArticles)
c.JSON(http.StatusOK, gin.H{
"users": gin.H{
"total": totalUsers,
"new_this_week": newUsersThisWeek,
"total": totalUsers,
"new_this_week": newUsersThisWeek,
},
"events": gin.H{
"total": totalEvents,
@@ -155,18 +155,18 @@ func (ac *AnalyticsController) GetVisitors(c *gin.Context) {
days = parsed
}
}
groupBy := c.DefaultQuery("groupBy", "day")
startDate := time.Now().AddDate(0, 0, -days)
type VisitorStat struct {
Date string `json:"date"`
PageViews int64 `json:"pageViews"`
UniqueVisitors int64 `json:"uniqueVisitors"`
}
var stats []VisitorStat
// Group by date
if groupBy == "day" {
ac.DB.Model(&models.VisitorEvent{}).
@@ -184,12 +184,11 @@ func (ac *AnalyticsController) GetVisitors(c *gin.Context) {
Order("date ASC").
Scan(&stats)
}
// Transform data for chart format
labels := make([]string, 0, len(stats))
pageViewsData := make([]int64, 0, len(stats))
uniqueVisitorsData := make([]int64, 0, len(stats))
var totalVisitors int64
for _, stat := range stats {
// Format date as "d. M." (e.g., "5. 10.")
@@ -199,11 +198,10 @@ func (ac *AnalyticsController) GetVisitors(c *gin.Context) {
} else {
labels = append(labels, stat.Date)
}
pageViewsData = append(pageViewsData, stat.PageViews)
uniqueVisitorsData = append(uniqueVisitorsData, stat.UniqueVisitors)
totalVisitors += stat.UniqueVisitors
}
// Calculate change percentage (compare last 7 days with previous 7 days)
var changePercentage float64
if len(stats) >= 14 {
@@ -218,39 +216,39 @@ func (ac *AnalyticsController) GetVisitors(c *gin.Context) {
changePercentage = float64(recentSum-previousSum) / float64(previousSum) * 100
}
}
response := gin.H{
"totalVisitors": totalVisitors,
"totalVisitors": totalVisitors,
"changePercentage": changePercentage,
"chartData": gin.H{
"labels": labels,
"datasets": []gin.H{
{
"label": "Návštěvníci",
"data": uniqueVisitorsData,
"borderColor": "rgba(66, 153, 225, 1)",
"label": "Návštěvníci",
"data": uniqueVisitorsData,
"borderColor": "rgba(66, 153, 225, 1)",
"backgroundColor": "rgba(66, 153, 225, 0.5)",
"tension": 0.3,
"fill": true,
"tension": 0.3,
"fill": true,
},
},
},
}
c.JSON(http.StatusOK, response)
}
// GetAnalyticsOverview returns overview statistics for admin dashboard
func (ac *AnalyticsController) GetAnalyticsOverview(c *gin.Context) {
var totalPageViews, uniqueVisitors, pageViewsToday, pageViewsWeek, uniqueVisitorsWeek int64
// Try to fetch from Umami first
websiteID, err := ac.resolveWebsiteID()
if err == nil && websiteID != "" {
// Fetch overall stats (last 365 days for total)
endAt := time.Now().Unix() * 1000
startAt := time.Now().AddDate(-1, 0, 0).Unix() * 1000
stats, err := ac.umamiService.GetWebsiteStats(websiteID, startAt, endAt)
if err == nil {
if pv, ok := stats["pageviews"].(map[string]interface{}); ok {
@@ -264,9 +262,9 @@ func (ac *AnalyticsController) GetAnalyticsOverview(c *gin.Context) {
}
}
}
// Fetch today's stats
todayStart := time.Now().Truncate(24 * time.Hour).Unix() * 1000
todayStart := time.Now().Truncate(24*time.Hour).Unix() * 1000
todayEnd := time.Now().Unix() * 1000
todayStats, err := ac.umamiService.GetWebsiteStats(websiteID, todayStart, todayEnd)
if err == nil {
@@ -276,7 +274,7 @@ func (ac *AnalyticsController) GetAnalyticsOverview(c *gin.Context) {
}
}
}
// Fetch this week's stats
weekStart := time.Now().AddDate(0, 0, -7).Unix() * 1000
weekEnd := time.Now().Unix() * 1000
@@ -297,10 +295,10 @@ func (ac *AnalyticsController) GetAnalyticsOverview(c *gin.Context) {
// Fallback to internal analytics if Umami is not available
ac.DB.Model(&models.VisitorEvent{}).Where("event_type = ?", "page_view").Count(&totalPageViews)
ac.DB.Model(&models.VisitorEvent{}).Where("event_type = ?", "page_view").Distinct("ip_address").Count(&uniqueVisitors)
today := time.Now().Format("2006-01-02")
ac.DB.Model(&models.VisitorEvent{}).Where("event_type = ? AND DATE(created_at) = ?", "page_view", today).Count(&pageViewsToday)
weekAgo := time.Now().AddDate(0, 0, -7)
ac.DB.Model(&models.VisitorEvent{}).Where("event_type = ? AND created_at >= ?", "page_view", weekAgo).Count(&pageViewsWeek)
ac.DB.Model(&models.VisitorEvent{}).Where("event_type = ? AND created_at >= ?", "page_view", weekAgo).Distinct("ip_address").Count(&uniqueVisitorsWeek)
@@ -312,13 +310,13 @@ func (ac *AnalyticsController) GetAnalyticsOverview(c *gin.Context) {
ac.DB.Model(&models.Article{}).Where("published = ?", true).Count(&publishedArticles)
c.JSON(http.StatusOK, gin.H{
"total_page_views": totalPageViews,
"unique_visitors": uniqueVisitors,
"total_articles": totalArticles,
"published_articles": publishedArticles,
"page_views_today": pageViewsToday,
"page_views_week": pageViewsWeek,
"unique_visitors_week": uniqueVisitorsWeek,
"total_page_views": totalPageViews,
"unique_visitors": uniqueVisitors,
"total_articles": totalArticles,
"published_articles": publishedArticles,
"page_views_today": pageViewsToday,
"page_views_week": pageViewsWeek,
"unique_visitors_week": uniqueVisitorsWeek,
})
}
@@ -337,14 +335,14 @@ func (ac *AnalyticsController) GetTopPages(c *gin.Context) {
}
var pages []PageStats
// Try to fetch from Umami first
websiteID, err := ac.resolveWebsiteID()
if err == nil && websiteID != "" {
// Fetch URL metrics from Umami (last 30 days)
endAt := time.Now().Unix() * 1000
startAt := time.Now().AddDate(0, 0, -30).Unix() * 1000
metrics, err := ac.umamiService.GetWebsiteMetrics(websiteID, "url", startAt, endAt)
if err == nil && metrics != nil {
// Convert Umami metrics to PageStats format
@@ -354,14 +352,14 @@ func (ac *AnalyticsController) GetTopPages(c *gin.Context) {
}
pagePath := ""
viewCount := int64(0)
if x, ok := metricMap["x"].(string); ok {
pagePath = x
}
if y, ok := metricMap["y"].(float64); ok {
viewCount = int64(y)
}
pages = append(pages, PageStats{
PagePath: pagePath,
PageName: pagePath,
@@ -373,7 +371,7 @@ func (ac *AnalyticsController) GetTopPages(c *gin.Context) {
return
}
}
// Fallback to internal analytics
ac.DB.Model(&models.VisitorEvent{}).
Where("event_type = ?", "page_view").
@@ -403,33 +401,37 @@ func (ac *AnalyticsController) GetTopArticles(c *gin.Context) {
}
type TopInteraction struct {
Page string `json:"page"`
Element string `json:"element"`
Count int64 `json:"count"`
Page string `json:"page"`
Element string `json:"element"`
Count int64 `json:"count"`
}
func (ctrl *AnalyticsController) GetTopInteractions(c *gin.Context) {
daysParam := c.DefaultQuery("days", "30")
limitParam := c.DefaultQuery("limit", "10")
days, _ := strconv.Atoi(daysParam)
if days <= 0 || days > 365 { days = 30 }
limit, _ := strconv.Atoi(limitParam)
if limit <= 0 || limit > 100 { limit = 10 }
daysParam := c.DefaultQuery("days", "30")
limitParam := c.DefaultQuery("limit", "10")
days, _ := strconv.Atoi(daysParam)
if days <= 0 || days > 365 {
days = 30
}
limit, _ := strconv.Atoi(limitParam)
if limit <= 0 || limit > 100 {
limit = 10
}
start := time.Now().AddDate(0, 0, -days)
start := time.Now().AddDate(0, 0, -days)
var rows []TopInteraction
err := ctrl.DB.
Model(&models.VisitorEvent{}).
Select("page, element, COUNT(*) as count").
Where("event_type IN ? AND created_at >= ?", []string{"click", "interaction"}, start).
Group("page, element").
Order("count DESC").
Limit(limit).
Scan(&rows).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load interactions"})
return
}
c.JSON(http.StatusOK, gin.H{"items": rows})
var rows []TopInteraction
err := ctrl.DB.
Model(&models.VisitorEvent{}).
Select("page, element, COUNT(*) as count").
Where("event_type IN ? AND created_at >= ?", []string{"click", "interaction"}, start).
Group("page, element").
Order("count DESC").
Limit(limit).
Scan(&rows).Error
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load interactions"})
return
}
c.JSON(http.StatusOK, gin.H{"items": rows})
}