Fix bugs found in local testing

- Dashboard auto-refresh rendered the full layout into its own
  refresh container, producing a duplicate sidebar every 60s; it now
  renders only the body partial.
- 'Run Now' runs synchronously with a bounded timeout and returns
  refreshed results plus success/error feedback, instead of
  firing-and-forgetting with no signal.
- Price-history chart data moved from a <script> block to a data-
  attribute: templ does not interpolate expressions inside <script>
  element content, so the JSON was emitted literally.
- The htmx indicator spinner was permanently visible due to CSS
  source order; the indicator rules now follow .v-spinner.

Also refreshes README for this session's changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-14 12:11:07 -07:00
parent 08ff1695e0
commit d87536c879
12 changed files with 550 additions and 366 deletions

View File

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