Files
libnovel/v3/TODO.md
Admin a85636d5db feat(v3): add v3 stack — backend rewrite, renamed env vars, docs
- 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
2026-03-22 17:27:32 +05:00

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)