- 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
10 KiB
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/backendcp -r ../ui-v2 v3/uicp ../scripts/pb-init-v2.sh v3/scripts/pb-init-v3.sh- Verify
v3/backend/go.modmodule path is stillgithub.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—Cacheinterface +ValkeyCacheimplementationGet(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— addValkeyAddr string(default"valkey:6379")internal/backend/server.go— addPresignCache presigncache.CachetoDependenciesinternal/backend/handlers.go— in presign handlers, docache.Getbefore generating,cache.Setafter generatinggo.mod— addgithub.com/redis/go-redis/v9cmd/backend/main.go— wireValkeyCachefrom 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—Clientinterface +MeiliClientimplementationUpsertBook(ctx, book domain.Book) errorSearch(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— addMeiliURL string,MeiliAPIKey stringinternal/bookstore/bookstore.go— addSearchIndexinterface withUpsertBookinternal/runner/runner.go— injectSearchIndex; afterFinishScrapeTasksuccess, callSearchIndex.UpsertBookfor each book scrapedinternal/backend/server.go— addSearchIndextoDependencies; registerGET /api/searchinternal/backend/handlers.go—searchHandler: query Meilisearch first; if 0 results, fall back to novelfire.net live search viaNovelScraper.SearchBooksgo.mod— addgithub.com/meilisearch/meilisearch-gocmd/backend/main.go+cmd/runner/main.go— wireMeiliClient
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:
{
"tasks_pending": 0,
"tasks_running": 1,
"tasks_completed": 42,
"tasks_failed": 2,
"uptime_seconds": 3600
}
Files to create/modify:
internal/runner/metrics.go—Metricsstruct withatomic.Int64counters;ServeHTTPhandler returns JSON;MetricsServerstartsnet/httpon configurable addrinternal/runner/runner.go— embed*Metrics; increment counters at task lifecycle points (claimed → running → completed/failed)internal/config/config.go— addRunnerMetricsAddr string(default":9091")cmd/runner/main.go— startMetricsServerin background goroutine beforerunner.Rundocker-compose.yml(runner service) — expose port 9091 internally; update healthcheck toGET 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 useioredisclientgetPresignedUrl(key: string): Promise<string | null>setPresignedUrl(key: string, url: string, ttlSeconds: number): Promise<void>- Connection string from
VALKEY_URLenv var (defaultredis://valkey:6379) - Keep the same TTL logic (50 min) and sweep logic (not needed — Valkey TTL is native)
package.json— addioredis(or use@redis/client— preferioredisfor 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:8080handle /health→reverse_proxy backend:8080handle /s3/*→ strip prefix +reverse_proxy minio:9000(internal bucket access, requiresAuthorizationpassthrough)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 --buildstarts cleanlycurl https://{DOMAIN}/healthreturns 200 (Caddy → backend)- Book search returns results from Meilisearch after at least one scrape
- Presign URL cache hit visible in backend logs (Valkey
GEThit) curl http://localhost:9091/metricsreturns JSON with counters- Runner healthcheck passes via
/metricsendpoint (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_datavolume / Caddy logs)