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) }