- New Go backend binary (backend + runner) replacing old scraper/ - Rename SCRAPER_API_URL → BACKEND_API_URL in UI env and docker-compose - Rename scraperFetch → backendFetch across all 19 UI server files - Remove SCRAPER_PROXY env var and proxy transport from browser.Config - Add Meilisearch, Valkey, Caddy to docker-compose - Add docs/: api-endpoints.md, request-flow.mermaid.md, data-flow.mermaid.md
216 lines
10 KiB
Markdown
216 lines
10 KiB
Markdown
# v3 Implementation Plan
|
|
|
|
v3 is a self-contained directory. All services live under `v3/`. Nothing shares
|
|
code with the root `backend/` or `ui-v2/` directories.
|
|
|
|
## New in v3 vs v2
|
|
|
|
| Addition | Why |
|
|
|----------|-----|
|
|
| **Caddy** reverse proxy | Single public entry point, automatic HTTPS via Let's Encrypt, no manual cert management |
|
|
| **Meilisearch** | Full-text search over locally scraped books; replaces live novelfire.net scrape on every search |
|
|
| **Valkey** (Redis-compatible) | Shared presign URL cache; replaces the per-process in-memory Map in the UI Node process |
|
|
| **Runner `/metrics` HTTP endpoint** | Exposes task counters (pending, running, failed, completed) for healthcheck / Caddy visibility |
|
|
|
|
## Directory layout (target)
|
|
|
|
```
|
|
v3/
|
|
├── backend/ # copied from ../backend/, then modified
|
|
│ ├── cmd/
|
|
│ │ ├── backend/ # unchanged
|
|
│ │ └── runner/ # add metrics HTTP server
|
|
│ ├── internal/
|
|
│ │ ├── backend/
|
|
│ │ │ ├── server.go # add /api/search endpoint (Meilisearch)
|
|
│ │ │ └── handlers.go # add searchHandler, presign now reads Valkey
|
|
│ │ ├── runner/
|
|
│ │ │ ├── runner.go # add atomic counters + metrics HTTP server
|
|
│ │ │ └── metrics.go # NEW: MetricsServer (net/http, /metrics JSON)
|
|
│ │ ├── meili/ # NEW: Meilisearch client package
|
|
│ │ │ └── client.go
|
|
│ │ ├── presigncache/ # NEW: Valkey-backed presign cache
|
|
│ │ │ └── cache.go
|
|
│ │ └── config/
|
|
│ │ └── config.go # add Meilisearch + Valkey + metrics addr vars
|
|
│ └── go.mod # add meilisearch-go, go-redis/v9
|
|
├── ui/ # copied from ../ui-v2/, then modified
|
|
│ └── src/lib/server/
|
|
│ └── presignCache.ts # replace in-process Map with Valkey (ioredis)
|
|
├── docs/
|
|
│ ├── architecture.d2 # DONE
|
|
│ └── architecture.mermaid.md # DONE
|
|
├── scripts/
|
|
│ └── pb-init-v3.sh # same as v2 unless schema changes
|
|
├── Caddyfile # NEW
|
|
├── docker-compose.yml # NEW
|
|
└── TODO.md # this file
|
|
```
|
|
|
|
---
|
|
|
|
## Tasks
|
|
|
|
### 1. Copy source trees
|
|
|
|
- [ ] `cp -r ../backend v3/backend`
|
|
- [ ] `cp -r ../ui-v2 v3/ui`
|
|
- [ ] `cp ../scripts/pb-init-v2.sh v3/scripts/pb-init-v3.sh`
|
|
- [ ] Verify `v3/backend/go.mod` module path is still `github.com/libnovel/backend`
|
|
(no rename needed — it's a copy, not a Go workspace dep)
|
|
|
|
### 2. Add Valkey presign cache (`v3/backend`)
|
|
|
|
**Goal**: Backend generates presigned URLs → stores in Valkey with TTL.
|
|
UI reads from Valkey before calling backend.
|
|
|
|
Files to create/modify:
|
|
|
|
- [ ] `internal/presigncache/cache.go` — `Cache` interface + `ValkeyCache` implementation
|
|
- `Get(ctx, key) (string, bool, error)`
|
|
- `Set(ctx, key, url string, ttl time.Duration) error`
|
|
- Uses `github.com/redis/go-redis/v9`
|
|
- TTL: match presigned URL lifetime (e.g. 45 min; MinIO default is 1h → use 55 min)
|
|
- [ ] `internal/config/config.go` — add `ValkeyAddr string` (default `"valkey:6379"`)
|
|
- [ ] `internal/backend/server.go` — add `PresignCache presigncache.Cache` to `Dependencies`
|
|
- [ ] `internal/backend/handlers.go` — in presign handlers, do `cache.Get` before generating,
|
|
`cache.Set` after generating
|
|
- [ ] `go.mod` — add `github.com/redis/go-redis/v9`
|
|
- [ ] `cmd/backend/main.go` — wire `ValkeyCache` from config
|
|
|
|
### 3. Add Meilisearch integration (`v3/backend`)
|
|
|
|
**Goal**: On each book scrape completion, upsert the book document into Meilisearch.
|
|
Backend `/api/search` returns Meilisearch results for local books, falling back to
|
|
novelfire.net live search for books not in the index.
|
|
|
|
Files to create/modify:
|
|
|
|
- [ ] `internal/meili/client.go` — `Client` interface + `MeiliClient` implementation
|
|
- `UpsertBook(ctx, book domain.Book) error`
|
|
- `Search(ctx, query string, limit int) ([]domain.Book, error)`
|
|
- Uses `github.com/meilisearch/meilisearch-go`
|
|
- Index name: `books`, primary key: `slug`
|
|
- Searchable attributes: `title`, `author`, `tags`, `description`
|
|
- Filterable attributes: `status`, `tags`
|
|
- [ ] `internal/config/config.go` — add `MeiliURL string`, `MeiliAPIKey string`
|
|
- [ ] `internal/bookstore/bookstore.go` — add `SearchIndex` interface with `UpsertBook`
|
|
- [ ] `internal/runner/runner.go` — inject `SearchIndex`; after `FinishScrapeTask` success,
|
|
call `SearchIndex.UpsertBook` for each book scraped
|
|
- [ ] `internal/backend/server.go` — add `SearchIndex` to `Dependencies`; register `GET /api/search`
|
|
- [ ] `internal/backend/handlers.go` — `searchHandler`: query Meilisearch first; if 0 results,
|
|
fall back to novelfire.net live search via `NovelScraper.SearchBooks`
|
|
- [ ] `go.mod` — add `github.com/meilisearch/meilisearch-go`
|
|
- [ ] `cmd/backend/main.go` + `cmd/runner/main.go` — wire `MeiliClient`
|
|
|
|
### 4. Add runner `/metrics` HTTP endpoint (`v3/backend`)
|
|
|
|
**Goal**: Runner exposes `GET /metrics` returning JSON with task counters. Caddy can
|
|
healthcheck this; operators can scrape it from a monitoring tool.
|
|
|
|
Payload example:
|
|
```json
|
|
{
|
|
"tasks_pending": 0,
|
|
"tasks_running": 1,
|
|
"tasks_completed": 42,
|
|
"tasks_failed": 2,
|
|
"uptime_seconds": 3600
|
|
}
|
|
```
|
|
|
|
Files to create/modify:
|
|
|
|
- [ ] `internal/runner/metrics.go` — `Metrics` struct with `atomic.Int64` counters;
|
|
`ServeHTTP` handler returns JSON; `MetricsServer` starts `net/http` on configurable addr
|
|
- [ ] `internal/runner/runner.go` — embed `*Metrics`; increment counters at task lifecycle
|
|
points (claimed → running → completed/failed)
|
|
- [ ] `internal/config/config.go` — add `RunnerMetricsAddr string` (default `":9091"`)
|
|
- [ ] `cmd/runner/main.go` — start `MetricsServer` in background goroutine before `runner.Run`
|
|
- [ ] `docker-compose.yml` (runner service) — expose port 9091 internally; update healthcheck
|
|
to `GET http://localhost:9091/metrics` (replaces file-based liveness)
|
|
|
|
### 5. Update UI presign cache to use Valkey (`v3/ui`)
|
|
|
|
**Goal**: Replace `src/lib/server/presignCache.ts` (module-scope `Map`) with Valkey via
|
|
`ioredis`. Cache survives UI restarts and is shared if multiple UI replicas run.
|
|
|
|
Files to modify:
|
|
|
|
- [ ] `src/lib/server/presignCache.ts` — rewrite to use `ioredis` client
|
|
- `getPresignedUrl(key: string): Promise<string | null>`
|
|
- `setPresignedUrl(key: string, url: string, ttlSeconds: number): Promise<void>`
|
|
- Connection string from `VALKEY_URL` env var (default `redis://valkey:6379`)
|
|
- Keep the same TTL logic (50 min) and sweep logic (not needed — Valkey TTL is native)
|
|
- [ ] `package.json` — add `ioredis` (or use `@redis/client` — prefer `ioredis` for its
|
|
robust reconnection handling)
|
|
- [ ] Any callers of old `presignCache` — update import if interface changes
|
|
|
|
### 6. Write `v3/Caddyfile`
|
|
|
|
- [ ] Global options block: email for ACME, staging CA override for local dev
|
|
- [ ] Single site block matching `{$DOMAIN}` (env var injection):
|
|
- `handle /api/*` → `reverse_proxy backend:8080`
|
|
- `handle /health` → `reverse_proxy backend:8080`
|
|
- `handle /s3/*` → strip prefix + `reverse_proxy minio:9000` (internal bucket access,
|
|
requires `Authorization` passthrough)
|
|
- `handle` (catch-all) → `reverse_proxy ui:3000`
|
|
- [ ] Health/liveness endpoints exposed without auth
|
|
- [ ] No ports on MinIO/PocketBase exposed to public (internal Docker network only)
|
|
|
|
### 7. Write `v3/docker-compose.yml`
|
|
|
|
Services (in dependency order):
|
|
|
|
| Service | Image / Build | Ports (host) | Notes |
|
|
|---------|--------------|--------------|-------|
|
|
| `minio` | `minio/minio:latest` | none (internal only) | Remove public port exposure; Caddy proxies `/s3/*` |
|
|
| `minio-init` | `minio/mc:latest` | — | Same as v2 |
|
|
| `pocketbase` | `ghcr.io/muchobien/pocketbase:latest` | none (internal only) | Remove public port |
|
|
| `pb-init` | `alpine:3.19` | — | Same as v2, uses `v3/scripts/pb-init-v3.sh` |
|
|
| `valkey` | `valkey/valkey:8-alpine` | none (internal) | `valkey-server --save "" --appendonly no` |
|
|
| `meilisearch` | `getmeili/meilisearch:v1.7` | none (internal) | `MEILI_NO_ANALYTICS=true`, persistent volume |
|
|
| `backend` | build `v3/backend` target `backend` | none (internal) | Add `VALKEY_ADDR`, `MEILI_URL`, `MEILI_API_KEY` |
|
|
| `runner` | build `v3/backend` target `runner` | `9091` (metrics) | Add `RUNNER_METRICS_ADDR`, `MEILI_URL`, `MEILI_API_KEY`; healthcheck via `/metrics` |
|
|
| `ui` | build `v3/ui` | none (internal) | Add `VALKEY_URL`; remove `PUBLIC_MINIO_PUBLIC_URL` (all through Caddy now) |
|
|
| `caddy` | `caddy:2-alpine` | `80`, `443` | Bind-mount `v3/Caddyfile`; persistent `caddy_data` volume for certs |
|
|
|
|
Volumes: `minio_data`, `pb_data`, `meili_data`, `caddy_data`
|
|
|
|
- [ ] Write the full file with healthchecks, depends_on, env vars
|
|
|
|
### 8. Write `v3/scripts/pb-init-v3.sh`
|
|
|
|
- [ ] Start from `scripts/pb-init-v2.sh`
|
|
- [ ] Assess if any schema changes needed (Meilisearch sync does not require new PB collections
|
|
— search is entirely Meilisearch-side)
|
|
- [ ] If identical to v2 script, note that in a comment at the top; keep as separate file
|
|
|
|
---
|
|
|
|
## Environment variables (new in v3)
|
|
|
|
| Variable | Service | Default | Description |
|
|
|----------|---------|---------|-------------|
|
|
| `VALKEY_ADDR` | backend | `valkey:6379` | Valkey TCP address |
|
|
| `VALKEY_URL` | ui | `redis://valkey:6379` | Valkey URL (ioredis format) |
|
|
| `MEILI_URL` | backend, runner | `http://meilisearch:7700` | Meilisearch HTTP URL |
|
|
| `MEILI_API_KEY` | backend, runner | `""` | Meilisearch master key (empty = no auth for dev) |
|
|
| `RUNNER_METRICS_ADDR` | runner | `:9091` | Runner metrics HTTP listen address |
|
|
| `DOMAIN` | caddy | `localhost` | Public domain for HTTPS cert |
|
|
| `CADDY_ACME_EMAIL` | caddy | `""` | Let's Encrypt notification email |
|
|
|
|
---
|
|
|
|
## Testing checklist (before considering v3 stable)
|
|
|
|
- [ ] `docker compose -f v3/docker-compose.yml up --build` starts cleanly
|
|
- [ ] `curl https://{DOMAIN}/health` returns 200 (Caddy → backend)
|
|
- [ ] Book search returns results from Meilisearch after at least one scrape
|
|
- [ ] Presign URL cache hit visible in backend logs (Valkey `GET` hit)
|
|
- [ ] `curl http://localhost:9091/metrics` returns JSON with counters
|
|
- [ ] Runner healthcheck passes via `/metrics` endpoint (not file-based)
|
|
- [ ] MinIO console NOT reachable from the internet (no public port)
|
|
- [ ] PocketBase NOT reachable from the internet (no public port)
|
|
- [ ] TLS cert obtained from Let's Encrypt (check `caddy_data` volume / Caddy logs)
|