package handlers import ( "database/sql" "net/http" "time" "veola/internal/db" "veola/templates" ) func (a *App) GetDashboard(w http.ResponseWriter, r *http.Request) { d, err := a.dashboardData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } render(w, r, templates.Dashboard(d)) } func (a *App) GetDashboardRefresh(w http.ResponseWriter, r *http.Request) { d, err := a.dashboardData(r) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } // Render ONLY the inner body. The hx-swap="outerHTML" on DashboardBody's // root div replaces it with this fresh copy. Rendering templates.Dashboard // here would return the whole Layout — sidebar included — nested inside // the div, producing a duplicate nav bar on every refresh. render(w, r, templates.DashboardBody(d)) } func (a *App) dashboardData(r *http.Request) (templates.DashboardData, error) { stats, err := a.Store.GetDashboardStats(r.Context()) if err != nil { return templates.DashboardData{}, err } results, err := a.Store.ListResults(r.Context(), db.ResultsQuery{Limit: 20}) if err != nil { return templates.DashboardData{}, err } itemNames := map[int64]string{} all, _ := a.Store.ListItems(r.Context()) for _, it := range all { itemNames[it.ID] = it.Name } rrs := make([]templates.ResultRow, 0, len(results)) for _, r := range results { rrs = append(rrs, templates.ResultRow{ ItemID: r.ItemID, ItemName: itemNames[r.ItemID], Title: r.Title, Price: r.Price, Currency: r.Currency, Source: r.Source, URL: r.URL, FoundAt: r.FoundAt, Alerted: r.Alerted, }) } alerts, err := alertsRecent(a, r, itemNames) if err != nil { return templates.DashboardData{}, err } return templates.DashboardData{ Page: a.page(r, "Dashboard", "dashboard"), Stats: stats, RecentResults: rrs, RecentAlerts: alerts, }, nil } func alertsRecent(a *App, r *http.Request, itemNames map[int64]string) ([]templates.AlertRow, error) { rows, err := a.Store.DB.QueryContext(r.Context(), ` SELECT item_id, price, currency, found_at FROM results WHERE alerted = 1 ORDER BY found_at DESC LIMIT 5 `) if err != nil { return nil, err } defer rows.Close() var out []templates.AlertRow for rows.Next() { var ( itemID int64 price sql.NullFloat64 currency string foundAt time.Time ) if err := rows.Scan(&itemID, &price, ¤cy, &foundAt); err != nil { return nil, err } var p *float64 if price.Valid { v := price.Float64 p = &v } out = append(out, templates.AlertRow{ ItemName: itemNames[itemID], Price: p, Currency: currency, FoundAt: foundAt, }) } return out, rows.Err() }