diff --git a/README.md b/README.md index ec1f8a1..d1ae0cd 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # Veola -Self-hosted Go web app that tracks items across e-commerce platforms (eBay, Amazon family, Yahoo Auctions JP, Mercari JP) via the [Apify](https://apify.com) scraping API and pushes deal alerts to a self-hosted [ntfy](https://ntfy.sh) instance. +Self-hosted Go web app that tracks items across e-commerce platforms and pushes deal alerts to a self-hosted [ntfy](https://ntfy.sh) instance. eBay marketplaces are polled through eBay's official [Browse API](https://developer.ebay.com/api-docs/buy/browse/overview.html); Amazon family, Yahoo Auctions JP, and Mercari JP go through the [Apify](https://apify.com) scraping API. Track. Watch. Notice. ## Features - Watch arbitrary items across multiple marketplaces with per-item search queries, target prices, and poll intervals -- Active-listing, sold-listing, and price-comparison actors per item +- eBay via the official eBay Browse API, with a per-day call quota tracked and enforced — polling halts before the limit and resets on eBay's Pacific-time clock +- Apify active-listing, sold-listing, and price-comparison actors for the non-eBay marketplaces - Price-history chart and best-price badge once enough history accumulates - Deal alerts pushed to ntfy when current price falls at or below target - Single-binary deploy, SQLite storage, no CGO @@ -17,22 +18,30 @@ See [`veola-spec.md`](veola-spec.md) for the full specification. ## Requirements - Go 1.22+ (developed against 1.25) -- An [Apify](https://apify.com) account + API key - A reachable [ntfy](https://ntfy.sh) instance (self-hosted or ntfy.sh) +- An [eBay developer](https://developer.ebay.com) keyset (App ID + Cert ID) — for eBay marketplaces +- An [Apify](https://apify.com) account + API key — for the non-eBay marketplaces +- To build from source: the [`templ`](https://templ.guide) CLI. The Tailwind standalone CLI is fetched automatically by the Makefile — no Node toolchain required. ## Build ```sh -go build -o veola-bin . +make build ``` +This runs `templ generate`, compiles Tailwind, and produces `veola-bin`. Makefile targets: + +| Target | What it does | +| --- | --- | +| `make generate` | Regenerate templ Go from `.templ` sources | +| `make css` | Compile `static/css/tailwind.css` from `static/css/input.css` (fetches the Tailwind standalone CLI into `bin/` on first run) | +| `make build` | `generate` + `css` + `go build -o veola-bin .` | +| `make run` | `build`, then run against `config.toml` | +| `make test` | `go test ./...` | + The binary is named `veola-bin` rather than `veola` because the module is also `veola` — `go build` cannot write a binary with the same name as the module dir. -If you change any `.templ` files, regenerate first: - -```sh -~/go/bin/templ generate -``` +`static/css/tailwind.css` is committed, so a deploy box can `go build -o veola-bin .` without the Tailwind CLI. Re-run `make css` whenever you change templates or `static/css/input.css`. The hand-written component layer in `static/css/app.css` is loaded separately and needs no rebuild. ## Configure @@ -48,7 +57,12 @@ Both `session_secret` and `encryption_key` must be at least 32 bytes and differe openssl rand -hex 32 ``` -`encryption_key` encrypts secrets at rest in SQLite (Apify keys, ntfy settings). Rotating it invalidates stored secrets — re-enter them through `/settings` after rotation. +`encryption_key` encrypts secrets at rest in SQLite (Apify key, eBay credentials, ntfy settings). Rotating it invalidates stored secrets — re-enter them through `/settings` after rotation. + +Other notable config: + +- `[ebay]` — `client_id` (App ID), `client_secret` (Cert ID), `environment` (`production` or `sandbox`), and `daily_call_limit` (Browse API calls per day; default 5000). All of `client_id` / `client_secret` / the limit can also be set or overridden at runtime via `/settings`. +- `server.secure_cookies` — sets the `Secure` attribute on the session cookie. Defaults to `true`; keep it on for any HTTPS-reachable deployment, **including behind a TLS-terminating proxy**. Set `false` only for local plain-HTTP testing on a non-localhost address. ## Run @@ -61,7 +75,7 @@ First-run flow: 1. Visit `http://localhost:8080/`. With no users, you are redirected to `/setup`. 2. Create the admin account. 3. Log in at `/login`. -4. Add items at `/items/new`. Optionally fill in your Apify key and ntfy URL via `/settings` if you didn't put them in `config.toml`. +4. Add items at `/items/new`. Optionally fill in your eBay/Apify credentials and ntfy URL via `/settings` if you didn't put them in `config.toml`. The Settings page also shows the running eBay API call count for the day. The scheduler starts with the server and polls each active item on its configured interval. The bottom-of-hour global poll runs every `scheduler.global_poll_interval_minutes`. @@ -75,12 +89,15 @@ internal/ db/ SQLite schema, migrations, store models/ domain types apify/ Apify API client + ebay/ eBay Browse API client (OAuth2 + item search) ntfy/ ntfy push client auth/ session + CSRF scheduler/ poll loop, alert/dedup/badge logic handlers/ HTTP handlers templates/ templ components -static/ CSS, vendored htmx +static/ compiled Tailwind + app.css, vendored htmx/Chart.js, JS +tailwind.config.js Tailwind content globs +Makefile build targets ``` ## Test @@ -89,14 +106,23 @@ static/ CSS, vendored htmx go test ./... ``` -Unit tests cover crypto round-trip, db round-trip and dedup, and scheduler alert/badge logic. No handler-level tests yet. +Unit tests cover crypto round-trip, db round-trip and dedup, scheduler alert/badge logic, and eBay marketplace/filter mapping. No handler-level tests yet. ## Operate - The SQLite file lives at `server.db_path` (default `./veola.db`). Back this up — it holds your watched items, history, encrypted secrets, and user accounts. +- `config.toml` and `veola.db` (plus its `-wal`/`-shm`) hold secrets and live session tokens — keep them `chmod 600` and owned by the service user. - The process responds to `SIGINT` / `SIGTERM` with a graceful HTTP shutdown (30s timeout) followed by scheduler stop. - Logs go to stdout as structured `log/slog` records. +### Deploying publicly + +Veola speaks plain HTTP and is meant to sit behind a TLS-terminating reverse proxy (e.g. Traefik, Caddy, nginx). + +- Keep `server.secure_cookies = true` (the default). +- Terminate TLS at the proxy and set HSTS there — Veola does not emit HSTS itself. +- Veola sets `Content-Security-Policy`, `X-Frame-Options`, `X-Content-Type-Options`, and `Referrer-Policy` on every response, and trusts `X-Forwarded-For` for client IPs — configure the proxy to strip client-supplied `X-Forwarded-*` headers so they cannot be spoofed. + ## Aesthetic Sega Genesis blue. Not dark mode, not light mode — blue mode. See the visual design section of `veola-spec.md` for the palette. diff --git a/internal/handlers/dashboard.go b/internal/handlers/dashboard.go index 825df01..fcdd532 100644 --- a/internal/handlers/dashboard.go +++ b/internal/handlers/dashboard.go @@ -24,10 +24,11 @@ func (a *App) GetDashboardRefresh(w http.ResponseWriter, r *http.Request) { http.Error(w, err.Error(), http.StatusInternalServerError) return } - // Render only the inner block by reusing the full body component; the - // outer hx-swap="outerHTML" replaces the same wrapper. The full Dashboard - // template is overkill but keeps a single source of truth. - render(w, r, templates.Dashboard(d)) + // 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) { diff --git a/internal/handlers/items.go b/internal/handlers/items.go index 5d38d66..412b19a 100644 --- a/internal/handlers/items.go +++ b/internal/handlers/items.go @@ -2,13 +2,13 @@ package handlers import ( "context" - "encoding/json" "fmt" "log/slog" "net/http" "sort" "strconv" "strings" + "time" "veola/internal/apify" "veola/internal/models" @@ -261,46 +261,16 @@ func (a *App) runPreview(ctx context.Context, it models.Item) ([]apify.UnifiedRe var merged []apify.UnifiedResult primarySource := "" for _, p := range plans { - actorID := p.ActorID() - if actorID == "" { - continue - } - raw, err := a.Apify.Run(ctx, actorID, p.Input()) + decoded, err := a.Scheduler.ExecutePlan(ctx, p) if err != nil { - slog.Warn("preview run failed", "actor", actorID, "query", p.Query(), "err", err) + slog.Warn("preview plan failed", + "provider", p.Provider(), + "marketplace", p.Marketplace(), + "query", p.Query(), + "err", err, + ) continue } - decoded, _ := apify.Decode(raw, p.Source()) - for i := range decoded { - decoded[i].MatchedQuery = p.Query() - } - usable := 0 - for _, r := range decoded { - if r.URL != "" && r.Price > 0 { - usable++ - } - } - slog.Info("preview decoded", - "marketplace", previewMarket, - "actor", actorID, - "query", p.Query(), - "raw", len(raw), - "decoded", len(decoded), - "usable", usable, - ) - if usable == 0 && len(raw) > 0 { - var sample map[string]any - if err := json.Unmarshal(raw[0], &sample); err == nil { - ks := make([]string, 0, len(sample)) - for k := range sample { - ks = append(ks, k) - } - slog.Warn("preview decoded zero usable rows; raw item keys", - "actor", actorID, - "keys", ks, - ) - } - } merged = append(merged, decoded...) if primarySource == "" { primarySource = p.Source() @@ -431,9 +401,40 @@ func (a *App) PostRunItem(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } - go a.Scheduler.RunPoll(context.Background(), *it) - // Re-render the row immediately so HTMX has something to swap in. - render(w, r, templates.ItemRow(*it, a.Auth.CSRFToken(r.Context()))) + + // Run synchronously so the response reflects the finished poll. Bounded so + // a slow Apify actor run can't tie the request up indefinitely (eBay + // Browse API polls finish in seconds, well within this). Detached from the + // request context so a client disconnect mid-run doesn't abort DB writes. + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) + defer cancel() + a.Scheduler.RunPoll(ctx, *it) + + // RunPoll writes best price, last_polled_at, and last_poll_error; re-fetch + // so the rendered partial shows the post-poll state. + fresh, err := a.Store.GetItem(r.Context(), id) + if err != nil || fresh == nil { + http.Error(w, "could not reload item after run", http.StatusInternalServerError) + return + } + + // The results page asks for a refreshed listing table; the items list + // asks for a refreshed row. Both POST to this same endpoint. + if r.PostFormValue("from") == "results" { + d, err := a.buildItemResultsData(r, fresh, 1, "found_desc") + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if fresh.LastPollError != "" { + d.RunError = "Run finished with errors: " + fresh.LastPollError + } else { + d.RunMsg = fmt.Sprintf("Run complete. Showing %d listing(s).", len(d.Results)) + } + render(w, r, templates.ItemResultsTable(d)) + return + } + render(w, r, templates.ItemRow(*fresh, a.Auth.CSRFToken(r.Context()))) } func (a *App) GetItemError(w http.ResponseWriter, r *http.Request) { diff --git a/internal/handlers/results.go b/internal/handlers/results.go index 6620999..d6dbc92 100644 --- a/internal/handlers/results.go +++ b/internal/handlers/results.go @@ -21,20 +21,29 @@ func (a *App) GetItemResults(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } + page, _ := strconv.Atoi(r.URL.Query().Get("page")) + d, err := a.buildItemResultsData(r, it, page, r.URL.Query().Get("order")) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + render(w, r, templates.ItemResults(d)) +} - order := r.URL.Query().Get("order") +// buildItemResultsData assembles the per-item results view: paginated results, +// price history, badge, and chart JSON. Shared by GetItemResults and the +// "Run Now" handler so both render identical data. +func (a *App) buildItemResultsData(r *http.Request, it *models.Item, page int, order string) (templates.ItemResultsData, error) { if order == "" { order = "found_desc" } - page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 } - total, err := a.Store.CountResults(r.Context(), id) + total, err := a.Store.CountResults(r.Context(), it.ID) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return templates.ItemResultsData{}, err } totalPages := (total + resultsPerPage - 1) / resultsPerPage if totalPages < 1 { @@ -45,36 +54,31 @@ func (a *App) GetItemResults(w http.ResponseWriter, r *http.Request) { } results, err := a.Store.ListResults(r.Context(), db.ResultsQuery{ - ItemID: id, + ItemID: it.ID, Limit: resultsPerPage, Offset: (page - 1) * resultsPerPage, Order: order, }) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return templates.ItemResultsData{}, err } - history, err := a.Store.ListPriceHistory(r.Context(), id) + history, err := a.Store.ListPriceHistory(r.Context(), it.ID) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return + return templates.ItemResultsData{}, err } - badge := scheduler.PickBadge(*it, history, time.Now()) - chart := buildChartJSON(history) - - render(w, r, templates.ItemResults(templates.ItemResultsData{ + return templates.ItemResultsData{ Page: a.page(r, it.Name, "items"), Item: *it, - Badge: badge, + Badge: scheduler.PickBadge(*it, history, time.Now()), History: history, Results: results, Page_: page, TotalPages: totalPages, Order: order, - HistoryChartJSON: chart, - })) + HistoryChartJSON: buildChartJSON(history), + }, nil } func buildChartJSON(history []models.PricePoint) string { diff --git a/static/css/app.css b/static/css/app.css index 4f9b992..c3a2f21 100644 --- a/static/css/app.css +++ b/static/css/app.css @@ -193,11 +193,6 @@ table.v-table tr:hover td { background: rgba(255,255,255,0.03); } margin-bottom: 1rem; } -/* htmx indicator: hidden by default, visible during in-flight requests. */ -.htmx-indicator { display: none; } -.htmx-request .htmx-indicator, -.htmx-request.htmx-indicator { display: inline-flex; } - .v-spinner { display: inline-block; width: 14px; height: 14px; @@ -207,3 +202,11 @@ table.v-table tr:hover td { background: rgba(255,255,255,0.03); } animation: v-spin 0.8s linear infinite; } @keyframes v-spin { to { transform: rotate(360deg); } } + +/* htmx indicator: hidden by default, visible during in-flight requests. + Declared AFTER .v-spinner on purpose: an element carrying both classes + (e.g. ) must stay hidden until a + request is active, and equal-specificity rules resolve by source order. */ +.htmx-indicator { display: none; } +.htmx-request .htmx-indicator, +.htmx-request.htmx-indicator { display: inline-flex; } diff --git a/static/js/price-chart.js b/static/js/price-chart.js new file mode 100644 index 0000000..ecc8f6d --- /dev/null +++ b/static/js/price-chart.js @@ -0,0 +1,35 @@ +// Renders the price-history line chart on the per-item results page. +// Chart data is read from the canvas's data-chart attribute: templ +// interpolates attribute values but treats - - + } + @ItemResultsTable(d) + +} + +// ItemResultsTable is the results listing + pagination, plus optional "Run +// Now" feedback. It is both part of the initial page (via itemResultsBody) +// and the standalone response to POST /items/{id}/run from the results page, +// so the Run Now button targets #item-results-table with hx-swap="outerHTML". +templ ItemResultsTable(d ItemResultsData) { +
+ if d.RunError != "" { +
{ d.RunError }
+ } else if d.RunMsg != "" { +
{ d.RunMsg }
+ }
diff --git a/templates/results_templ.go b/templates/results_templ.go index 7147d10..3efcd4c 100644 --- a/templates/results_templ.go +++ b/templates/results_templ.go @@ -25,6 +25,10 @@ type ItemResultsData struct { TotalPages int Order string HistoryChartJSON string + // RunMsg / RunError carry feedback from a "Run Now" poll. Both empty on a + // normal page load; PostRunItem sets exactly one. + RunMsg string + RunError string } type BadgeData struct { @@ -74,7 +78,7 @@ func itemResultsBody(d ItemResultsData) templ.Component { var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(d.Item.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 45, Col: 52} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 49, Col: 52} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -92,7 +96,7 @@ func itemResultsBody(d ItemResultsData) templ.Component { var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(d.Item.Category) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 47, Col: 43} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 51, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -115,7 +119,7 @@ func itemResultsBody(d ItemResultsData) templ.Component { var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(d.Item.BestPrice, "USD")) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 52, Col: 72} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 56, Col: 72} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -133,7 +137,7 @@ func itemResultsBody(d ItemResultsData) templ.Component { var templ_7745c5c3_Var5 templ.SafeURL templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(d.Item.BestPriceURL)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 54, Col: 66} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 58, Col: 66} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -146,7 +150,7 @@ func itemResultsBody(d ItemResultsData) templ.Component { var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(d.Item.BestPriceStore) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 54, Col: 123} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 58, Col: 123} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -193,7 +197,7 @@ func itemResultsBody(d ItemResultsData) templ.Component { var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(d.Badge.Label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 61, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 65, Col: 62} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -204,33 +208,33 @@ func itemResultsBody(d ItemResultsData) templ.Component { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "

Price History

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\"> Running...

Price History

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -240,275 +244,370 @@ func itemResultsBody(d ItemResultsData) templ.Component { return templ_7745c5c3_Err } } else { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "
") + if d.RunError != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(d.RunError) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 109, Col: 42} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } else if d.RunMsg != "" { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(d.RunMsg) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 111, Col: 34} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "
Title") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 templ.SafeURL - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/items/%d/results?order=%s", d.Item.ID, toggleOrder(d.Order, "price")))) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 118, Col: 115} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + templ_7745c5c3_Err = ItemResultsTable(d).Render(ctx, templ_7745c5c3_Buffer) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\">PriceStore") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 templ.SafeURL - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs(templ.SafeURL(fmt.Sprintf("/items/%d/results?order=%s", d.Item.ID, toggleOrder(d.Order, "found")))) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 122, Col: 115} + return nil + }) +} + +// ItemResultsTable is the results listing + pagination, plus optional "Run +// Now" feedback. It is both part of the initial page (via itemResultsBody) +// and the standalone response to POST /items/{id}/run from the results page, +// so the Run Now button targets #item-results-table with hx-swap="outerHTML". +func ItemResultsTable(d ItemResultsData) templ.Component { + return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { + templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context + if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { + return templ_7745c5c3_CtxErr } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) + if !templ_7745c5c3_IsBuffer { + defer func() { + templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) + if templ_7745c5c3_Err == nil { + templ_7745c5c3_Err = templ_7745c5c3_BufErr + } + }() + } + ctx = templ.InitializeContext(ctx) + templ_7745c5c3_Var13 := templ.GetChildren(ctx) + if templ_7745c5c3_Var13 == nil { + templ_7745c5c3_Var13 = templ.NopComponent + } + ctx = templ.ClearChildren(ctx) + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Alert
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, r := range d.Results { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var19 = []any{"font-mono", priceClass(r.Price, d.Item.TargetPrice)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var19...) + var templ_7745c5c3_Var23 = []any{"font-mono", priceClass(r.Price, d.Item.TargetPrice)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "
TitlePriceStoreFoundAlert
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if r.ImageURL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" alt=\"\" class=\"w-10 h-10 object-cover rounded\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if r.URL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "\" target=\"_blank\" rel=\"noopener\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) + var templ_7745c5c3_Var20 string + templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 137, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 139, Col: 82} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) + var templ_7745c5c3_Var21 string + templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 139, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 141, Col: 18} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.MatchedQuery != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "
via \"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "
via \"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(r.MatchedQuery) + var templ_7745c5c3_Var22 string + templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(r.MatchedQuery) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 142, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 144, Col: 59} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\"
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\"
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var21 string - templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(r.Price, r.Currency)) + var templ_7745c5c3_Var25 string + templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(r.Price, r.Currency)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 145, Col: 105} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 147, Col: 105} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var22 string - templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(r.Source) + var templ_7745c5c3_Var26 string + templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(r.Source) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 146, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 148, Col: 37} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var23 string - templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(humanTime(r.FoundAt)) + var templ_7745c5c3_Var27 string + templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(humanTime(r.FoundAt)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 147, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 149, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if r.Alerted { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "sent") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "sent") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if d.TotalPages > 1 { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for i := 1; i <= d.TotalPages; i++ { - var templ_7745c5c3_Var24 = []any{pageClass(i, d.Page_)} - templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var24...) + var templ_7745c5c3_Var28 = []any{pageClass(i, d.Page_)} + templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var28...) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i)) + var templ_7745c5c3_Var31 string + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", i)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 161, Col: 159} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 163, Col: 159} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -555,9 +654,9 @@ func ItemResults(d ItemResultsData) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var28 := templ.GetChildren(ctx) - if templ_7745c5c3_Var28 == nil { - templ_7745c5c3_Var28 = templ.NopComponent + templ_7745c5c3_Var32 := templ.GetChildren(ctx) + if templ_7745c5c3_Var32 == nil { + templ_7745c5c3_Var32 = templ.NopComponent } ctx = templ.ClearChildren(ctx) templ_7745c5c3_Err = Layout(d.Page, itemResultsBody(d)).Render(ctx, templ_7745c5c3_Buffer) @@ -584,238 +683,238 @@ func globalResultsBody(d GlobalResultsData) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var29 := templ.GetChildren(ctx) - if templ_7745c5c3_Var29 == nil { - templ_7745c5c3_Var29 = templ.NopComponent + templ_7745c5c3_Var33 := templ.GetChildren(ctx) + if templ_7745c5c3_Var33 == nil { + templ_7745c5c3_Var33 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "

All Results

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } for _, it := range d.Items { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, r := range d.Results { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
ItemTitlePriceStoreFoundAlert
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, ">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var35 string - templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(r.ItemName) + templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(it.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 226, Col: 93} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 206, Col: 90} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, r := range d.Results { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
ItemTitlePriceStoreFoundAlert
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var39 string + templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(r.ItemName) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 228, Col: 93} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if r.URL != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "\" target=\"_blank\" rel=\"noopener\">") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var37 string - templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) + var templ_7745c5c3_Var41 string + templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 229, Col: 82} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 231, Col: 82} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } else { - var templ_7745c5c3_Var38 string - templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) + var templ_7745c5c3_Var42 string + templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(r.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 231, Col: 18} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 233, Col: 18} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, " ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, " ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } if r.MatchedQuery != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "
via \"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "
via \"") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var39 string - templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(r.MatchedQuery) + var templ_7745c5c3_Var43 string + templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(r.MatchedQuery) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 234, Col: 59} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 236, Col: 59} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, "\"
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "\"
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var40 string - templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(r.Price, r.Currency)) + var templ_7745c5c3_Var44 string + templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(fmtPrice(r.Price, r.Currency)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 237, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 239, Col: 60} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 69, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var41 string - templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(r.Source) + var templ_7745c5c3_Var45 string + templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(r.Source) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 238, Col: 37} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 240, Col: 37} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var42 string - templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(humanTime(r.FoundAt)) + var templ_7745c5c3_Var46 string + templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(humanTime(r.FoundAt)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 239, Col: 57} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/results.templ`, Line: 241, Col: 57} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } if r.Alerted { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "sent") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "sent") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -839,9 +938,9 @@ func GlobalResults(d GlobalResultsData) templ.Component { }() } ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var43 := templ.GetChildren(ctx) - if templ_7745c5c3_Var43 == nil { - templ_7745c5c3_Var43 = templ.NopComponent + templ_7745c5c3_Var47 := templ.GetChildren(ctx) + if templ_7745c5c3_Var47 == nil { + templ_7745c5c3_Var47 = templ.NopComponent } ctx = templ.ClearChildren(ctx) templ_7745c5c3_Err = Layout(d.Page, globalResultsBody(d)).Render(ctx, templ_7745c5c3_Buffer)