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>
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
templCLI. 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 veola — go 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(productionorsandbox), anddaily_call_limit(Browse API calls per day; default 5000). All ofclient_id/client_secret/ the limit can also be set or overridden at runtime via/settings.server.secure_cookies— sets theSecureattribute on the session cookie. Defaults totrue; keep it on for any HTTPS-reachable deployment, including behind a TLS-terminating proxy. Setfalseonly for local plain-HTTP testing on a non-localhost address.
Run
./veola-bin -config config.toml
First-run flow:
- Visit
http://localhost:8080/. With no users, you are redirected to/setup. - Create the admin account.
- Log in at
/login. - Add items at
/items/new. Optionally fill in your eBay/Apify credentials and ntfy URL via/settingsif you didn't put them inconfig.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.tomlandveola.db(plus its-wal/-shm) hold secrets and live session tokens — keep themchmod 600and owned by the service user.- The process responds to
SIGINT/SIGTERMwith a graceful HTTP shutdown (30s timeout) followed by scheduler stop. - Logs go to stdout as structured
log/slogrecords.
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, andReferrer-Policyon every response, and trustsX-Forwarded-Forfor client IPs — configure the proxy to strip client-suppliedX-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.