162 lines
3.5 KiB
Go
162 lines
3.5 KiB
Go
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)
|
|
}
|