- Bulk-load recent price points per item and render a sparkline in the items list (new LoadRecentPriceHistory query avoids N+1). - Add retro.css visual layer and refreshed login/items/layout styling. - Swap the logo from webp to avif. - Pin htmx/Chart.js/Tailwind/templ versions in the Makefile with vendor / tools / update-deps targets; README documents the dependency-bump flow and the hardened systemd deploy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
232 lines
10 KiB
Markdown
232 lines
10 KiB
Markdown
# Veola
|
|
|
|
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
|
|
- 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`](veola-spec.md) for the full specification.
|
|
|
|
## Requirements
|
|
|
|
- Go 1.22+ (developed against 1.25)
|
|
- `make`, `curl`, and a POSIX shell (for the Makefile)
|
|
- 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
|
|
|
|
### Build-time tools
|
|
|
|
All non-Go tool versions are pinned in the Makefile at the top:
|
|
|
|
```make
|
|
TAILWIND_VERSION := v3.4.17
|
|
HTMX_VERSION := 2.0.4
|
|
CHARTJS_VERSION := 4.4.6
|
|
TEMPL_VERSION := v0.3.1020
|
|
```
|
|
|
|
Install `templ` yourself at the pinned version; Tailwind is downloaded for you on first `make css`. htmx and Chart.js are already vendored in `static/vendor/` at the pinned versions above and committed to the repo.
|
|
|
|
```sh
|
|
# Install the pinned templ CLI.
|
|
make tools
|
|
|
|
# (or, equivalently)
|
|
go install github.com/a-h/templ/cmd/templ@v0.3.1020
|
|
|
|
# Confirm it's on PATH (typically $(go env GOPATH)/bin).
|
|
templ --version
|
|
```
|
|
|
|
You **do not need Node, npm, or Yarn**. Tailwind ships as a self-contained binary — the Makefile fetches the [standalone Tailwind CLI](https://github.com/tailwindlabs/tailwindcss/releases) into `./bin/tailwindcss` on first `make css`. The `bin/` directory is gitignored.
|
|
|
|
By default the Makefile downloads the **linux-x64** build. For other platforms, override `TAILWIND_URL` on the command line — pick the matching asset from the [Tailwind releases page](https://github.com/tailwindlabs/tailwindcss/releases/tag/v3.4.17):
|
|
|
|
```sh
|
|
# macOS Apple Silicon
|
|
make css TAILWIND_URL=https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-macos-arm64
|
|
|
|
# macOS Intel
|
|
make css TAILWIND_URL=https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-macos-x64
|
|
|
|
# linux-arm64
|
|
make css TAILWIND_URL=https://github.com/tailwindlabs/tailwindcss/releases/download/v3.4.17/tailwindcss-linux-arm64
|
|
```
|
|
|
|
Or drop a binary at `./bin/tailwindcss` yourself and the Makefile will use it.
|
|
|
|
## Build
|
|
|
|
First time:
|
|
|
|
```sh
|
|
go install github.com/a-h/templ/cmd/templ@latest # if not already
|
|
make build # fetches Tailwind, runs templ, compiles
|
|
```
|
|
|
|
Subsequent builds:
|
|
|
|
```sh
|
|
make build
|
|
```
|
|
|
|
This runs `templ generate`, compiles Tailwind utilities into `static/css/tailwind.css`, 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 ./...` |
|
|
| `make clean` | Remove `veola-bin` |
|
|
|
|
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 or even `make`. 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:
|
|
|
|
```sh
|
|
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:
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
./veola-bin -config config.toml
|
|
```
|
|
|
|
CLI flags:
|
|
|
|
| Flag | Default | Notes |
|
|
| --- | --- | --- |
|
|
| `-config <path>` | `config.toml` | Path to the TOML config file |
|
|
| `-debug` | off | Verbose `log/slog` at `LevelDebug`. Logs raw external API payloads (eBay / ZenMarket / etc.) — useful when diagnosing parse failures. Not for production. |
|
|
|
|
First-run flow:
|
|
|
|
1. Visit `http://localhost:8080/`. With no users, you are redirected to `/setup`.
|
|
2. Create the admin account. The first user is always an admin.
|
|
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.
|
|
|
|
Account registration is admin-only — there is no public signup. Once at least one user exists, `/setup` returns 404. New users are created from the Settings page by an admin.
|
|
|
|
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
|
|
|
|
```sh
|
|
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.
|
|
|
|
## Keeping dependencies current
|
|
|
|
Veola pulls from four sources, all version-pinned for reproducibility.
|
|
|
|
```sh
|
|
# Bump Go module deps to their newest compatible versions, then prints
|
|
# upstream-release URLs for the four pinned tools so you can spot bumps.
|
|
make update-deps
|
|
|
|
# After bumping any of TAILWIND_VERSION / HTMX_VERSION / CHARTJS_VERSION
|
|
# in the Makefile, refetch the vendored assets at the new pins:
|
|
make vendor
|
|
|
|
# After bumping TEMPL_VERSION:
|
|
make tools
|
|
|
|
# Then rebuild and run the test suite:
|
|
make build && make test
|
|
```
|
|
|
|
The pinned tool versions live at the top of the Makefile. The vendored JS at `static/vendor/htmx.min.js` and `static/vendor/chart.umd.min.js` is committed and updated only by `make vendor`. Tailwind v3.4.17 is intentionally pinned — v4 is a breaking release that drops the `tailwind.config.js` format Veola uses.
|
|
|
|
For automated tracking, point [Dependabot](https://docs.github.com/en/code-security/dependabot) or [Renovate](https://docs.renovatebot.com/) at the repo. Both can watch `go.mod` natively; Renovate's custom-regex managers can also track the `*_VERSION` lines in the Makefile.
|
|
|
|
## 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.
|
|
- `GET /healthz` returns 200 `ok` with no auth — wire it up to your proxy/uptime probe.
|
|
|
|
A hardened systemd unit template lives at [`deploy/veola.service`](deploy/veola.service). It assumes:
|
|
|
|
- Binary at `/usr/local/bin/veola-bin`
|
|
- Config at `/etc/veola/config.toml`
|
|
- A `veola` system user with `/var/lib/veola` as its working / data directory (the only writable path under `ReadWritePaths`)
|
|
|
|
Install sketch:
|
|
|
|
```sh
|
|
sudo useradd --system --home /var/lib/veola --shell /usr/sbin/nologin veola
|
|
sudo install -d -o veola -g veola -m 0750 /var/lib/veola /etc/veola
|
|
sudo install -m 0755 veola-bin /usr/local/bin/veola-bin
|
|
sudo install -m 0640 -o veola -g veola config.toml /etc/veola/config.toml
|
|
sudo install -m 0644 deploy/veola.service /etc/systemd/system/veola.service
|
|
sudo systemctl daemon-reload
|
|
sudo systemctl enable --now veola
|
|
```
|
|
|
|
## Aesthetic
|
|
|
|
Sega Genesis blue. Not dark mode, not light mode — blue mode. See the visual design section of `veola-spec.md` for the palette.
|