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:
52
README.md
52
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.
|
||||
|
||||
Reference in New Issue
Block a user