Add Authentik SSO, budget visibility, Resend email, 12h item default
Opens Veola up to more users (Parodia's Authentik) and makes Apify spend visible to everyone. - Auth: Traefik forward-auth (trust X-Authentik-* headers only from trusted_proxies CIDRs, keyed by email, role synced from admin_group), keeping local password login as break-glass. New [auth] config, CaptureDirectIP + ForwardAuth middleware, deploy/authentik-forward-auth.md. - Budget: count every Apify run (apify_api_usage table) and show calls + estimated cost to all users on the dashboard, with an optional monthly-budget bar. New [budget] config + settings. - Email: Resend client for opt-in deal alerts and a weekly digest (Mon 09:00). Per-user email + toggles on Settings. New [resend] config. - Defaults: new items default to a 12-hour poll interval to cut spend. users table gains email/auth_source/email-pref columns (migrated in place). go build/vet/test green; boots and migrates cleanly.
This commit is contained in:
@@ -13,6 +13,39 @@ type DashboardData struct {
|
||||
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 {
|
||||
@@ -63,6 +96,50 @@ templ DashboardBody(d DashboardData) {
|
||||
<div class="v-muted text-sm mt-1">across { fmt.Sprintf("%d", d.Stats.SavedItemCount) } 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">{ fmt.Sprintf("%d", d.ApifyMonth) }</div>
|
||||
if d.ApifyCostPerCall > 0 {
|
||||
<div class="v-muted text-xs mt-1">{ fmt.Sprintf("~$%.2f est.", d.ApifyMonthCost()) }</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
<div class="v-muted text-xs uppercase tracking-wide">Apify (today)</div>
|
||||
<div class="font-mono text-2xl mt-1">{ fmt.Sprintf("%d", d.ApifyToday) }</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="v-muted text-xs uppercase tracking-wide">eBay (today)</div>
|
||||
if d.EbayLimit > 0 {
|
||||
<div class="font-mono text-2xl mt-1">{ fmt.Sprintf("%d / %d", d.EbayToday, d.EbayLimit) }</div>
|
||||
} else {
|
||||
<div class="font-mono text-2xl mt-1">{ fmt.Sprintf("%d", d.EbayToday) }</div>
|
||||
}
|
||||
</div>
|
||||
if d.MonthlyBudget > 0 {
|
||||
<div>
|
||||
<div class="v-muted text-xs uppercase tracking-wide">Monthly Budget</div>
|
||||
<div class="font-mono text-2xl mt-1">{ fmt.Sprintf("$%.0f", d.MonthlyBudget) }</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
if d.MonthlyBudget > 0 {
|
||||
<div class="mt-4">
|
||||
<progress class="v-progress w-full" value={ fmt.Sprintf("%d", d.BudgetPercent()) } max="100"></progress>
|
||||
<div class="flex justify-between text-xs mt-1">
|
||||
<span class="v-muted">{ fmt.Sprintf("%d%% of budget used", d.BudgetPercent()) }</span>
|
||||
if d.BudgetOver() {
|
||||
<span class="v-price-target">Over budget</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="v-card p-5">
|
||||
<h2 class="font-semibold mb-3">Recent Results</h2>
|
||||
|
||||
Reference in New Issue
Block a user