This commit is contained in:
lfirmin
2026-05-10 18:07:51 +02:00
parent 50af2452b1
commit a5f888b3c1
59 changed files with 6682 additions and 108 deletions
+161
View File
@@ -0,0 +1,161 @@
package handlers
import (
"context"
"fmt"
"net/http"
"time"
"github.com/leolionad58/ticketbot/internal/db"
panelauth "github.com/leolionad58/ticketbot/internal/panel/auth"
)
// BotStatus holds live bot state, populated by the BotService.
type BotStatus struct {
Online bool
StartedAt time.Time
LatencyMs int64
GuildName string
}
func (s BotStatus) Uptime() string {
if !s.Online {
return "—"
}
d := time.Since(s.StartedAt).Truncate(time.Second)
h := int(d.Hours())
m := int(d.Minutes()) % 60
sec := int(d.Seconds()) % 60
if h > 0 {
return fmt.Sprintf("%dh %dm", h, m)
}
return fmt.Sprintf("%dm %ds", m, sec)
}
// DashboardStats holds pre-computed stats for the dashboard.
type DashboardStats struct {
OpenTickets int
ClosedLast30 int
AvgClaimMinutes int
AvgResolutionMinutes int
}
// ChartBar is one bar in the daily chart.
type ChartBar struct {
Day string
Count int
}
// DashboardHandler serves the main dashboard page.
type DashboardHandler struct {
TicketRepo *db.TicketRepo
PanelRepo *db.PanelConfigRepo
GetBotStatus func() BotStatus
Renderer *Renderer
}
func (h *DashboardHandler) Handle(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
base := h.Renderer.base(r, "dashboard")
sess := panelauth.SessionFromContext(ctx)
if sess != nil {
base.CSRFToken = sess.CSRFToken
}
stats := h.loadStats(ctx)
chart := h.loadChart(ctx)
staff := h.loadStaff(ctx)
panels, _ := h.PanelRepo.List(ctx)
status := BotStatus{}
if h.GetBotStatus != nil {
status = h.GetBotStatus()
}
h.Renderer.Page(w, "dashboard", struct {
baseData
Stats DashboardStats
Chart []ChartBar
ChartMax int
Staff []db.StaffStat
Panels []*db.PanelConfig
BotOnline bool
Uptime string
LatencyMs int64
GuildName string
}{
baseData: base,
Stats: stats,
Chart: chart,
ChartMax: chartMax(chart),
Staff: staff,
Panels: panels,
BotOnline: status.Online,
Uptime: status.Uptime(),
LatencyMs: status.LatencyMs,
GuildName: status.GuildName,
})
}
func (h *DashboardHandler) loadStats(ctx context.Context) DashboardStats {
var stats DashboardStats
if h.TicketRepo == nil {
return stats
}
open, _ := h.TicketRepo.CountByStatus(ctx, "open")
claimed, _ := h.TicketRepo.CountByStatus(ctx, "claimed")
stats.OpenTickets = open + claimed
since := time.Now().AddDate(0, 0, -30)
stats.ClosedLast30, _ = h.TicketRepo.CountClosedSince(ctx, since)
stats.AvgClaimMinutes, _ = h.TicketRepo.AvgClaimMinutes(ctx, since)
stats.AvgResolutionMinutes, _ = h.TicketRepo.AvgResolutionMinutes(ctx, since)
return stats
}
func (h *DashboardHandler) loadChart(ctx context.Context) []ChartBar {
if h.TicketRepo == nil {
return nil
}
daily, _ := h.TicketRepo.DailyTicketCounts(ctx, 30)
dayMap := make(map[string]int, len(daily))
for _, d := range daily {
dayMap[d.Day] = d.Count
}
bars := make([]ChartBar, 30)
for i := range bars {
t := time.Now().AddDate(0, 0, -29+i)
day := t.Format("2006-01-02")
bars[i] = ChartBar{Day: t.Format("02/01"), Count: dayMap[day]}
}
return bars
}
func (h *DashboardHandler) loadStaff(ctx context.Context) []db.StaffStat {
if h.TicketRepo == nil {
return nil
}
stats, _ := h.TicketRepo.StaffStats(ctx, 30)
return stats
}
func chartMax(bars []ChartBar) int {
m := 1
for _, b := range bars {
if b.Count > m {
m = b.Count
}
}
return m
}
func fmtMinutes(m int) string {
if m <= 0 {
return "—"
}
if m < 60 {
return fmt.Sprintf("%dm", m)
}
return fmt.Sprintf("%dh%dm", m/60, m%60)
}