Window dashboard Money Saved to 30-day average

This commit is contained in:
prosolis
2026-06-20 11:57:54 -07:00
parent 2fd4019103
commit ed03494edf
5 changed files with 55 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"veola/internal/crypto" "veola/internal/crypto"
"veola/internal/models" "veola/internal/models"
@@ -74,6 +75,51 @@ func TestDedupByItemAndURL(t *testing.T) {
} }
} }
func TestDashboardMoneySavedWindow(t *testing.T) {
if os.Getenv("CI_SKIP_SQLITE") != "" {
t.Skip()
}
s := newTestStore(t)
ctx := context.Background()
bp := 100.0
id, err := s.CreateItem(ctx, &models.Item{
Name: "Windowed", NtfyTopic: "veola", Active: true,
PollIntervalMinutes: 60, NtfyPriority: "default",
BestPrice: &bp,
})
if err != nil {
t.Fatal(err)
}
now := time.Now()
// A stale point well outside the 30-day window at a much higher price. If
// the average were computed over all history this would inflate "saved".
if err := s.InsertPricePoint(ctx, &models.PricePoint{
ItemID: id, Price: 500, PolledAt: now.AddDate(0, 0, -60),
}); err != nil {
t.Fatal(err)
}
// A recent point inside the window. Best price (100) is below it, so the
// only legitimate saving is 120 - 100 = 20.
if err := s.InsertPricePoint(ctx, &models.PricePoint{
ItemID: id, Price: 120, PolledAt: now.AddDate(0, 0, -5),
}); err != nil {
t.Fatal(err)
}
stats, err := s.GetDashboardStats(ctx)
if err != nil {
t.Fatal(err)
}
if stats.SavedItemCount != 1 {
t.Fatalf("expected 1 saved item, got %d", stats.SavedItemCount)
}
if got := stats.MoneySaved; got < 19.99 || got > 20.01 {
t.Errorf("expected money saved ~20 (recent avg only), got %.2f", got)
}
}
func TestCJKRoundTrip(t *testing.T) { func TestCJKRoundTrip(t *testing.T) {
s := newTestStore(t) s := newTestStore(t)
ctx := context.Background() ctx := context.Background()

View File

@@ -1040,11 +1040,16 @@ func (s *Store) GetDashboardStats(ctx context.Context) (*DashboardStats, error)
`).Scan(&d.UnpricedCount); err != nil { `).Scan(&d.UnpricedCount); err != nil {
return nil, err return nil, err
} }
// "Money saved" compares each item's current best price against its recent
// market average. The average is windowed to the last 30 days so stale
// historical prices don't anchor the baseline; items whose only price
// history predates the window simply don't contribute an estimate.
rows, err := s.DB.QueryContext(ctx, ` rows, err := s.DB.QueryContext(ctx, `
SELECT i.best_price, AVG(p.price) AS avg_price SELECT i.best_price, AVG(p.price) AS avg_price
FROM items i FROM items i
JOIN price_history p ON p.item_id = i.id JOIN price_history p ON p.item_id = i.id
WHERE i.active = 1 AND i.best_price IS NOT NULL WHERE i.active = 1 AND i.best_price IS NOT NULL
AND p.polled_at >= datetime('now', '-30 day')
GROUP BY i.id GROUP BY i.id
`) `)
if err != nil { if err != nil {

View File

@@ -137,7 +137,7 @@ func digestHTML(items []models.Item, stats *db.DashboardStats) string {
b.WriteString(`<div style="font-family:sans-serif">`) b.WriteString(`<div style="font-family:sans-serif">`)
b.WriteString(`<h2>Veola weekly digest</h2>`) b.WriteString(`<h2>Veola weekly digest</h2>`)
if stats != nil { if stats != nil {
fmt.Fprintf(&b, `<p>%d active items. Estimated money saved so far: $%.2f.</p>`, fmt.Fprintf(&b, `<p>%d active items. Estimated money saved vs 30-day average: $%.2f.</p>`,
stats.ActiveItems, stats.MoneySaved) stats.ActiveItems, stats.MoneySaved)
} }
b.WriteString(`<table style="border-collapse:collapse" cellpadding="6">`) b.WriteString(`<table style="border-collapse:collapse" cellpadding="6">`)
@@ -158,7 +158,7 @@ func digestText(items []models.Item, stats *db.DashboardStats) string {
var b strings.Builder var b strings.Builder
b.WriteString("Veola weekly digest\n\n") b.WriteString("Veola weekly digest\n\n")
if stats != nil { if stats != nil {
fmt.Fprintf(&b, "%d active items. Estimated money saved so far: $%.2f.\n\n", stats.ActiveItems, stats.MoneySaved) fmt.Fprintf(&b, "%d active items. Estimated money saved vs 30-day average: $%.2f.\n\n", stats.ActiveItems, stats.MoneySaved)
} }
for _, it := range items { for _, it := range items {
best := "no price yet" best := "no price yet"

View File

@@ -93,7 +93,7 @@ templ DashboardBody(d DashboardData) {
<div class="v-card p-5"> <div class="v-card p-5">
<div class="v-muted text-sm uppercase tracking-wide">Money Saved</div> <div class="v-muted text-sm uppercase tracking-wide">Money Saved</div>
<div class="font-mono text-4xl mt-2 v-price-deal" data-countup>{ fmt.Sprintf("$%.2f", d.Stats.MoneySaved) }</div> <div class="font-mono text-4xl mt-2 v-price-deal" data-countup>{ fmt.Sprintf("$%.2f", d.Stats.MoneySaved) }</div>
<div class="v-muted text-sm mt-1">across { fmt.Sprintf("%d", d.Stats.SavedItemCount) } items</div> <div class="v-muted text-sm mt-1">across { fmt.Sprintf("%d", d.Stats.SavedItemCount) } items, vs 30-day average</div>
</div> </div>
</div> </div>
<div class="v-card p-5 mb-6"> <div class="v-card p-5 mb-6">

View File

@@ -196,7 +196,7 @@ func DashboardBody(d DashboardData) templ.Component {
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " items</div></div></div><div class=\"v-card p-5 mb-6\"><div class=\"flex items-center justify-between mb-3\"><h2 class=\"font-semibold\">API Usage and Budget</h2><span class=\"v-muted text-xs\">Apify runs cost credits. Visible to everyone.</span></div><div class=\"grid grid-cols-2 md:grid-cols-4 gap-4\"><div><div class=\"v-muted text-xs uppercase tracking-wide\">Apify (month)</div><div class=\"font-mono text-2xl mt-1\">") templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, " items, vs 30-day average</div></div></div><div class=\"v-card p-5 mb-6\"><div class=\"flex items-center justify-between mb-3\"><h2 class=\"font-semibold\">API Usage and Budget</h2><span class=\"v-muted text-xs\">Apify runs cost credits. Visible to everyone.</span></div><div class=\"grid grid-cols-2 md:grid-cols-4 gap-4\"><div><div class=\"v-muted text-xs uppercase tracking-wide\">Apify (month)</div><div class=\"font-mono text-2xl mt-1\">")
if templ_7745c5c3_Err != nil { if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err return templ_7745c5c3_Err
} }