package dashboard import ( "embed" "fmt" "html/template" "net/http" "time" "llm-gateway/internal/auth" ) //go:embed templates/*.html templates/partials/*.html var templateFiles embed.FS var templateFuncs = template.FuncMap{ "formatTime": func(ts int64) string { if ts == 0 { return "never" } return time.Unix(ts, 0).Format("2006-01-02") }, "addInt": func(a, b int) int { return a + b }, "formatCost": func(v float64) string { if v == 0 { return "$0.00" } if v < 0.01 { return fmt.Sprintf("$%.6f", v) } return fmt.Sprintf("$%.4f", v) }, } // PageData is the common data passed to all templates. type PageData struct { ActivePage string User *auth.User // Page-specific data Summary *SummaryResult Models []ModelStats Providers []ProviderStats TokenStats []TokenUsageStats Tokens []auth.APIToken Users []auth.User } // Dashboard serves the HTMX-based dashboard pages. type Dashboard struct { templates *template.Template authStore *auth.Store statsAPI *StatsAPI } // NewDashboard creates a new Dashboard handler. func NewDashboard(authStore *auth.Store, statsAPI *StatsAPI) *Dashboard { tmpl := template.Must( template.New("").Funcs(templateFuncs).ParseFS(templateFiles, "templates/*.html", "templates/partials/*.html", ), ) return &Dashboard{ templates: tmpl, authStore: authStore, statsAPI: statsAPI, } } // LoginPage serves the login page. func (d *Dashboard) LoginPage(w http.ResponseWriter, r *http.Request) { if !d.authStore.HasAnyUser() { http.Redirect(w, r, "/setup", http.StatusFound) return } if user := d.getSessionUser(r); user != nil { http.Redirect(w, r, "/dashboard", http.StatusFound) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") d.templates.ExecuteTemplate(w, "login", nil) } // SetupPage serves the initial setup page. func (d *Dashboard) SetupPage(w http.ResponseWriter, r *http.Request) { if d.authStore.HasAnyUser() { http.Redirect(w, r, "/login", http.StatusFound) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") d.templates.ExecuteTemplate(w, "setup", nil) } // DashboardPage serves the main dashboard view. func (d *Dashboard) DashboardPage(w http.ResponseWriter, r *http.Request) { user := auth.UserFromContext(r.Context()) tokenNames := d.statsAPI.TokenNamesForUser(user) data := PageData{ ActivePage: "dashboard", User: user, Summary: d.statsAPI.GetSummary(tokenNames), Models: d.statsAPI.GetModels(tokenNames), Providers: d.statsAPI.GetProviders(tokenNames), TokenStats: d.statsAPI.GetTokenUsage(tokenNames), } d.renderDashboardPage(w, r, "partials/dashboard.html", data) } // TokensPage serves the tokens management view. func (d *Dashboard) TokensPage(w http.ResponseWriter, r *http.Request) { user := auth.UserFromContext(r.Context()) var userID int64 if !user.IsAdmin { userID = user.ID } tokens, _ := d.authStore.ListAPITokens(userID) if tokens == nil { tokens = []auth.APIToken{} } d.renderDashboardPage(w, r, "partials/tokens.html", PageData{ ActivePage: "tokens", User: user, Tokens: tokens, }) } // UsersPage serves the user management view (admin only). func (d *Dashboard) UsersPage(w http.ResponseWriter, r *http.Request) { user := auth.UserFromContext(r.Context()) users, _ := d.authStore.ListUsers() d.renderDashboardPage(w, r, "partials/users.html", PageData{ ActivePage: "users", User: user, Users: users, }) } // SettingsPage serves the settings view. func (d *Dashboard) SettingsPage(w http.ResponseWriter, r *http.Request) { user := auth.UserFromContext(r.Context()) user, _ = d.authStore.GetUserByID(user.ID) d.renderDashboardPage(w, r, "partials/settings.html", PageData{ ActivePage: "settings", User: user, }) } // renderDashboardPage renders either the full layout or just the content partial. func (d *Dashboard) renderDashboardPage(w http.ResponseWriter, r *http.Request, partialFile string, data PageData) { w.Header().Set("Content-Type", "text/html; charset=utf-8") if r.Header.Get("HX-Request") == "true" { tmpl := template.Must( template.New("").Funcs(templateFuncs).ParseFS(templateFiles, "templates/"+partialFile), ) tmpl.ExecuteTemplate(w, "content", data) } else { tmpl := template.Must( template.New("").Funcs(templateFuncs).ParseFS(templateFiles, "templates/layout.html", "templates/"+partialFile, ), ) tmpl.ExecuteTemplate(w, "layout", data) } } func (d *Dashboard) getSessionUser(r *http.Request) *auth.User { cookie, err := r.Cookie("llmgw_session") if err != nil || cookie.Value == "" { return nil } sess, err := d.authStore.GetSession(cookie.Value) if err != nil { return nil } user, err := d.authStore.GetUserByID(sess.UserID) if err != nil { return nil } return user }