package templates import ( "fmt" "time" "veola/internal/db" "veola/internal/models" ) type DashboardData struct { Page Stats *db.DashboardStats RecentResults []ResultRow RecentAlerts []AlertRow // Budget surface, shown to every signed-in user (Apify calls cost money). ApifyToday int ApifyMonth int ApifyCostPerCall float64 MonthlyBudget float64 EbayToday int EbayLimit int } // ApifyMonthCost is the estimated month-to-date Apify spend in USD. func (d DashboardData) ApifyMonthCost() float64 { return float64(d.ApifyMonth) * d.ApifyCostPerCall } // BudgetPercent is month-to-date spend as a percent of the monthly budget, // clamped to [0,100]. Zero when no budget is set. func (d DashboardData) BudgetPercent() int { if d.MonthlyBudget <= 0 { return 0 } p := int(d.ApifyMonthCost() / d.MonthlyBudget * 100) if p < 0 { return 0 } if p > 100 { return 100 } return p } // BudgetOver reports whether estimated spend has exceeded the monthly budget. func (d DashboardData) BudgetOver() bool { return d.MonthlyBudget > 0 && d.ApifyMonthCost() > d.MonthlyBudget } type ResultRow struct { ItemID int64 ItemName string Title string Price *float64 Currency string Source string URL string ImageURL string FoundAt time.Time Alerted bool } type AlertRow struct { ItemName string Title string Price *float64 Currency string ImageURL string FoundAt time.Time } // DashboardBody is the self-refreshing inner block. It is both the initial // render target (inside Layout) and the response to /dashboard/refresh, so the // hx-swap="outerHTML" on its root div replaces it with a fresh copy of itself. // GetDashboardRefresh must render THIS, not Dashboard — rendering the full // Layout would inject a nested page (and a duplicate sidebar) into the div. templ DashboardBody(d DashboardData) {

Dashboard

{ fmt.Sprintf("Watching %d items · %d active", d.Stats.TotalItems, d.Stats.ActiveItems) }

Potential spend
{ fmt.Sprintf("$%.2f", d.Stats.PotentialSpend) }
{ fmt.Sprintf("across %d priced items", d.Stats.PricedItemCount) } if d.Stats.UnpricedCount > 0 { { fmt.Sprintf(" · %d not yet priced", d.Stats.UnpricedCount) } }
Money saved
{ fmt.Sprintf("$%.2f", d.Stats.MoneySaved) }
{ fmt.Sprintf("across %d items, vs 30-day average", d.Stats.SavedItemCount) }

@statBlock("Total items", fmt.Sprintf("%d", d.Stats.TotalItems)) @statBlock("Active", fmt.Sprintf("%d", d.Stats.ActiveItems)) @statBlock("Results today", fmt.Sprintf("%d", d.Stats.ResultsToday)) @statBlock("Alerts today", fmt.Sprintf("%d", d.Stats.AlertsToday))

API usage and budget

Apify runs cost credits. Visible to everyone.
Apify (month)
{ fmt.Sprintf("%d", d.ApifyMonth) }
if d.ApifyCostPerCall > 0 {
{ fmt.Sprintf("~$%.2f estimated", d.ApifyMonthCost()) }
}
Apify (today)
{ fmt.Sprintf("%d", d.ApifyToday) }
eBay (today)
if d.EbayLimit > 0 {
{ fmt.Sprintf("%d / %d", d.EbayToday, d.EbayLimit) }
} else {
{ fmt.Sprintf("%d", d.EbayToday) }
}
if d.MonthlyBudget > 0 {
Monthly budget
{ fmt.Sprintf("$%.0f", d.MonthlyBudget) }
}
if d.MonthlyBudget > 0 {
{ fmt.Sprintf("%d%% of budget used", d.BudgetPercent()) } if d.BudgetOver() { Over budget }
}

Recent results

if len(d.RecentResults) == 0 {
Nothing found yet. Veola is watching.
} else { }

Recent alerts

if len(d.RecentAlerts) == 0 {
No alerts sent. Nothing has hit target.
} else {
    for _, a := range d.RecentAlerts {
  • @thumb(a.ImageURL, "")
    { a.ItemName }
    if a.Title != "" {
    { a.Title }
    }
    { fmtPrice(a.Price, a.Currency) }
  • }
}
} // thumb renders a product thumbnail, falling back to a quiet placeholder // (the source initial) when a listing has no image. Marketplace images come // from an open set of CDNs, which the CSP's img-src allows. templ thumb(url, source string) { if url != "" { } else {
{ sourceInitial(source) }
} } // thumbSm is the compact table-row variant of thumb (no source placeholder // glyph — a plain quiet square when an item has no best-price image yet). templ thumbSm(url string) { if url != "" { } else {
} } func resultMeta(r ResultRow) string { if r.Title != "" { return r.Title } return r.Source + " · " + humanTime(r.FoundAt) } func sourceInitial(source string) string { if source == "" { return "" } return string([]rune(source)[:1]) } templ statBlock(label, value string) {
{ label }
{ value }
} templ Dashboard(d DashboardData) { @Layout(d.Page, DashboardBody(d)) } // Helpers used by multiple templates. func fmtPrice(p *float64, currency string) string { if p == nil { return "—" } sym := currencySymbol(currency) return fmt.Sprintf("%s%.2f", sym, *p) } func currencySymbol(c string) string { switch c { case "USD", "": return "$" case "GBP": return "£" case "EUR": return "€" case "JPY": return "¥" default: return c + " " } } func humanTime(t time.Time) string { if t.IsZero() { return "—" } d := time.Since(t) switch { case d < time.Minute: return "just now" case d < time.Hour: m := int(d.Minutes()) return fmt.Sprintf("%d minutes ago", m) case d < 24*time.Hour: h := int(d.Hours()) return fmt.Sprintf("%d hours ago", h) case d < 30*24*time.Hour: d2 := int(d.Hours() / 24) return fmt.Sprintf("%d days ago", d2) default: return t.Format("2006-01-02") } } // Used by item rendering. var _ = models.Item{}