Auction handling: - Capture itemEndDate from eBay Browse API and ending_date from ZenMarket (Yahoo JP); plumb through results.ends_at column. Permissive ZenMarket parser (multiple layouts, JST when offset missing). - Per-row "Ends" countdown column + "Ending soon" banner on results pages, live-ticked by flair.js with urgent/critical tinting under 1h/5m. - Backfill ends_at for known auctions when their URL reappears in a poll (dedup hit no longer drops the new end time). - Hide ended auctions from result listings by default via ResultsQuery.ExcludeEnded; rows stay in the DB. Visual flair: - Glassy backdrop-blur v-cards with gradient-mask borders and hover-lift. - htmx swap fade-in via transient .v-just-swapped class. - Count-up animation on dashboard stats. All animations gated behind prefers-reduced-motion. eBay condition + region filters (auctions-style scoping): - items.condition and items.region columns; threaded through item form, CreateItem/UpdateItem, scheduler eBay plan input, and previewKey so cache invalidates when these change. - ebay.SearchParams gains conditionIds and itemLocationCountry filters. Run Now reload + countdown engine: - Run Now now sets HX-Refresh: true (non-htmx fallback: 303 redirect) so the entire results view — best price, chart, badge, last polled — reflects the new poll, instead of swapping just one partial. Pre-launch hardening (P1 set): - auth.EqualizeLoginTiming on no-such-user branch. - (*App).serverError centralizes 500s; replaces err.Error() leaks across results/settings/items/users/dashboard handlers. - main.go server: ReadTimeout 30s / WriteTimeout 60s / IdleTimeout 120s alongside the existing ReadHeaderTimeout. - noListFS wrapper blocks static directory listings. - Credential fields in settings no longer render value=; blank submission preserves the saved value, with per-field "Saved in settings / Set in config.toml / Not set" status indicator. Misc: - -debug flag wires slog to LevelDebug; raw ZenMarket items logged for format diagnosis. - /healthz public endpoint for reverse-proxy probes. - deploy/veola.service systemd unit template (hardening flags, single ReadWritePaths=/var/lib/veola). - handlers_test.go covers /healthz, setup-gate redirect, auth gate, and /login render with httptest + in-memory sqlite. - best_price_currency on items; templates pick the right symbol per row. - .gitignore now excludes *.log / veola-debug.log. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
134 lines
3.1 KiB
Go
134 lines
3.1 KiB
Go
package models
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type Role string
|
|
|
|
const (
|
|
RoleAdmin Role = "admin"
|
|
RoleUser Role = "user"
|
|
)
|
|
|
|
type User struct {
|
|
ID int64
|
|
Username string
|
|
PasswordHash string
|
|
Role Role
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
type Item struct {
|
|
ID int64
|
|
Name string
|
|
SearchQuery string
|
|
URL string
|
|
Category string
|
|
TargetPrice *float64
|
|
NtfyTopic string
|
|
NtfyPriority string
|
|
PollIntervalMinutes int
|
|
IncludeOutOfStock bool
|
|
MinPrice *float64
|
|
ExcludeKeywords string
|
|
Marketplaces []string
|
|
ListingType string
|
|
// Condition and Region are eBay-only search filters. Condition is
|
|
// Veola's vocabulary ("new", "used", "refurbished", "parts"); Region is
|
|
// an ISO 3166-1 alpha-2 country code constraining item location. Both
|
|
// empty means no filter, and both are ignored for non-eBay marketplaces.
|
|
Condition string
|
|
Region string
|
|
ActorActive string
|
|
ActorSold string
|
|
ActorPriceCompare string
|
|
UsePriceComparison bool
|
|
Active bool
|
|
LastPolledAt *time.Time
|
|
LastPollError string
|
|
BestPrice *float64
|
|
BestPriceCurrency string
|
|
BestPriceStore string
|
|
BestPriceURL string
|
|
BestPriceImageURL string
|
|
BestPriceTitle string
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
type Result struct {
|
|
ID int64
|
|
ItemID int64
|
|
Title string
|
|
Price *float64
|
|
Currency string
|
|
URL string
|
|
Source string
|
|
ImageURL string
|
|
MatchedQuery string
|
|
Alerted bool
|
|
FoundAt time.Time
|
|
// EndsAt is populated for auction-format listings (eBay auctions,
|
|
// Yahoo Auctions JP). Nil for fixed-price listings and for any source
|
|
// that doesn't report an end time.
|
|
EndsAt *time.Time
|
|
}
|
|
|
|
// SearchQueries returns the item's alias list. Splits on newline, comma, and
|
|
// semicolon; trims; drops blanks; dedupes case-insensitively. Result order is
|
|
// the user's input order (first occurrence wins).
|
|
func (it *Item) SearchQueries() []string {
|
|
return SplitList(it.SearchQuery, 10)
|
|
}
|
|
|
|
// ExcludeKeywordsList returns the item's exclude-keyword list, normalized the
|
|
// same way as SearchQueries.
|
|
func (it *Item) ExcludeKeywordsList() []string {
|
|
return SplitList(it.ExcludeKeywords, 20)
|
|
}
|
|
|
|
// SplitList splits a user-entered list on newline, comma, or semicolon,
|
|
// trims whitespace, drops empty entries, dedupes case-insensitively, and caps
|
|
// the result at max entries (0 = no cap).
|
|
func SplitList(s string, max int) []string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
seen := map[string]bool{}
|
|
var out []string
|
|
for _, part := range strings.FieldsFunc(s, func(r rune) bool {
|
|
return r == '\n' || r == '\r' || r == ',' || r == ';'
|
|
}) {
|
|
t := strings.TrimSpace(part)
|
|
if t == "" {
|
|
continue
|
|
}
|
|
k := strings.ToLower(t)
|
|
if seen[k] {
|
|
continue
|
|
}
|
|
seen[k] = true
|
|
out = append(out, t)
|
|
if max > 0 && len(out) >= max {
|
|
break
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type PricePoint struct {
|
|
ID int64
|
|
ItemID int64
|
|
Price float64
|
|
Store string
|
|
PolledAt time.Time
|
|
}
|
|
|
|
type Setting struct {
|
|
Key string
|
|
Value string
|
|
UpdatedAt time.Time
|
|
}
|