prosolis edb732ee1f Auction end times, visual flair, and pre-launch cleanup
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>
2026-05-15 17:47:09 -07:00
2026-05-14 12:11:07 -07:00
2026-05-13 19:42:49 -07:00
2026-05-13 19:42:49 -07:00

Veola

Self-hosted Go web app that tracks items across e-commerce platforms and pushes deal alerts to a self-hosted ntfy instance. eBay marketplaces are polled through eBay's official Browse API; Amazon family, Yahoo Auctions JP, and Mercari JP go through the Apify scraping API.

Track. Watch. Notice.

Features

  • Watch arbitrary items across multiple marketplaces with per-item search queries, target prices, and poll intervals
  • 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

See veola-spec.md for the full specification.

Requirements

  • Go 1.22+ (developed against 1.25)
  • A reachable ntfy instance (self-hosted or ntfy.sh)
  • An eBay developer keyset (App ID + Cert ID) — for eBay marketplaces
  • An Apify account + API key — for the non-eBay marketplaces
  • To build from source: the templ CLI. The Tailwind standalone CLI is fetched automatically by the Makefile — no Node toolchain required.

Build

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 veolago build cannot write a binary with the same name as the module dir.

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

Copy the example and edit:

cp config.toml.example config.toml

Both session_secret and encryption_key must be at least 32 bytes and different from each other. Generate with:

openssl rand -hex 32

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

./veola-bin -config config.toml

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 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.

Layout

main.go              entry point: config, db open, scheduler, http server
internal/
  config/            TOML config loading and validation
  crypto/            AES-GCM encryption for secrets at rest
  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/              compiled Tailwind + app.css, vendored htmx/Chart.js, JS
tailwind.config.js   Tailwind content globs
Makefile             build targets

Test

go test ./...

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.

Description
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.
Readme 573 KiB
Languages
Go 67%
templ 21.4%
CSS 7.3%
JavaScript 3%
Makefile 1.3%