- Caddy: custom image with caddy-ratelimit plugin, security headers (X-Frame-Options, HSTS, CSP-adjacent, etc.), per-IP rate limiting on auth/scrape/global zones, static error pages (502/503/504), fix routing to remove /api/scrape/* and /api/chapter-text-preview/* direct-to-backend (were bypassing SvelteKit auth middleware) - docker-compose: Caddy build context + error volume, Watchtower service (label-enable mode, 5 min poll), watchtower labels on backend/runner/ui - Scraper: ScrapeChapterList uses retryGet (9 attempts, Retry-After backoff) to fix 429-induced chapter list failures; upTo param stops pagination early for range scrapes - UI: Browse→Catalogue rename (routes, API, links), admin scrape page Continue/Retry buttons, +error.svelte branded error page, type cleanup (removed dead exports, added BookPreviewMeta/BookPreviewResponse to scraper.ts) - Meilisearch: meta_updated field, sort=update fix, facet distribution - Docs: reorganise into docs/d2/ and docs/mermaid/ subdirectories, update all diagrams to reflect Caddy/Watchtower/routing changes, add api-routing.d2 ownership map with auth-level colour coding, regenerate SVGs
155 lines
6.2 KiB
Plaintext
155 lines
6.2 KiB
Plaintext
direction: right
|
|
|
|
# ─── External ─────────────────────────────────────────────────────────────────
|
|
|
|
novelfire: novelfire.net {
|
|
shape: cloud
|
|
style.fill: "#f0f4ff"
|
|
}
|
|
|
|
kokoro: Kokoro-FastAPI TTS {
|
|
shape: cloud
|
|
style.fill: "#f0f4ff"
|
|
}
|
|
|
|
letsencrypt: Let's Encrypt {
|
|
shape: cloud
|
|
style.fill: "#f0f4ff"
|
|
}
|
|
|
|
browser: Browser / iOS App {
|
|
shape: person
|
|
style.fill: "#fff9e6"
|
|
}
|
|
|
|
# ─── Init containers (one-shot) ───────────────────────────────────────────────
|
|
|
|
init: Init containers {
|
|
style.fill: "#f5f5f5"
|
|
style.stroke-dash: 4
|
|
|
|
minio-init: minio-init {
|
|
shape: rectangle
|
|
label: "minio-init\n(mc: create buckets)"
|
|
}
|
|
|
|
pb-init: pb-init {
|
|
shape: rectangle
|
|
label: "pb-init\n(bootstrap collections)"
|
|
}
|
|
}
|
|
|
|
# ─── Storage ──────────────────────────────────────────────────────────────────
|
|
|
|
storage: Storage {
|
|
style.fill: "#eaf7ea"
|
|
|
|
minio: MinIO {
|
|
shape: cylinder
|
|
label: "MinIO :9000\n\nbuckets:\n libnovel-chapters\n libnovel-audio\n avatars\n libnovel-browse"
|
|
}
|
|
|
|
pocketbase: PocketBase {
|
|
shape: cylinder
|
|
label: "PocketBase :8090\n\ncollections:\n books chapters_idx\n audio_cache progress\n scrape_jobs app_users\n ranking"
|
|
}
|
|
|
|
valkey: Valkey {
|
|
shape: cylinder
|
|
label: "Valkey :6379\n\n(presign URL cache\nTTL-based, shared)"
|
|
}
|
|
|
|
meilisearch: Meilisearch {
|
|
shape: cylinder
|
|
label: "Meilisearch :7700\n\nindices:\n books"
|
|
}
|
|
}
|
|
|
|
# ─── Application ──────────────────────────────────────────────────────────────
|
|
|
|
app: Application {
|
|
style.fill: "#eef3ff"
|
|
|
|
caddy: caddy {
|
|
shape: rectangle
|
|
label: "Caddy :443 / :80\ncustom build + caddy-ratelimit\n\nfeatures:\n auto-HTTPS (Let's Encrypt)\n security headers\n rate limiting (per-IP)\n static error pages (502/503/504)"
|
|
}
|
|
|
|
backend: backend {
|
|
shape: rectangle
|
|
label: "Backend API :8080\n(Go — HTTP API server)"
|
|
}
|
|
|
|
runner: runner {
|
|
shape: rectangle
|
|
label: "Runner :9091\n(Go — background worker\nscraping + TTS jobs\n/metrics endpoint)"
|
|
}
|
|
|
|
ui: ui {
|
|
shape: rectangle
|
|
label: "SvelteKit UI :3000\n(adapter-node)"
|
|
}
|
|
}
|
|
|
|
# ─── Ops ──────────────────────────────────────────────────────────────────────
|
|
|
|
ops: Ops {
|
|
style.fill: "#fef9ec"
|
|
|
|
watchtower: Watchtower {
|
|
shape: rectangle
|
|
label: "Watchtower\n(containrrr/watchtower)\n\npolls every 5 min\nautopulls + redeploys:\n backend · runner · ui"
|
|
}
|
|
}
|
|
|
|
# ─── Init → Storage deps ──────────────────────────────────────────────────────
|
|
|
|
init.minio-init -> storage.minio: create buckets {style.stroke-dash: 4}
|
|
init.pb-init -> storage.pocketbase: bootstrap schema {style.stroke-dash: 4}
|
|
|
|
# ─── App → Storage ────────────────────────────────────────────────────────────
|
|
|
|
app.backend -> storage.minio: blobs (chapters, audio,\navatars, browse)
|
|
app.backend -> storage.pocketbase: structured records\n(books, progress, jobs…)
|
|
app.backend -> storage.valkey: cache presigned URLs\n(SET/GET with TTL)
|
|
|
|
app.runner -> storage.minio: write chapter markdown\n& audio MP3s
|
|
app.runner -> storage.pocketbase: read/update scrape jobs\nwrite book records
|
|
app.runner -> storage.meilisearch: index books on\nscrape completion
|
|
|
|
app.ui -> storage.valkey: read presigned URL cache
|
|
app.ui -> storage.pocketbase: auth, progress,\ncomments, settings
|
|
|
|
# ─── App internal ─────────────────────────────────────────────────────────────
|
|
|
|
app.ui -> app.backend: REST API calls (server-side)\n/api/catalogue /api/book-preview\n/api/chapter-text /api/audio etc.
|
|
|
|
# ─── Caddy routing ────────────────────────────────────────────────────────────
|
|
# Routes sent directly to backend (no SvelteKit counterpart):
|
|
# /health /scrape*
|
|
# /api/browse /api/book-preview/* /api/chapter-text/*
|
|
# /api/reindex/* /api/cover/* /api/audio-proxy/*
|
|
# Routes sent to MinIO:
|
|
# /avatars/*
|
|
# Everything else → SvelteKit UI (including /api/scrape/*, /api/chapter-text-preview/*)
|
|
|
|
app.caddy -> app.ui: "/* (catch-all)\n/api/scrape/*\n/api/chapter-text-preview/*\n→ SvelteKit (auth enforced)"
|
|
app.caddy -> app.backend: "/health /scrape*\n/api/browse /api/book-preview/*\n/api/chapter-text/*\n/api/reindex/* /api/cover/*\n/api/audio-proxy/*"
|
|
app.caddy -> storage.minio: "/avatars/*\n(presigned avatar GETs)"
|
|
|
|
# ─── External → App ───────────────────────────────────────────────────────────
|
|
|
|
app.runner -> novelfire: scrape\n(HTTP GET)
|
|
app.runner -> kokoro: TTS generation\n(HTTP POST)
|
|
app.caddy -> letsencrypt: ACME certificate\n(TLS-ALPN-01)
|
|
|
|
# ─── Ops → Docker socket ──────────────────────────────────────────────────────
|
|
|
|
ops.watchtower -> app.backend: watch (label-enabled)
|
|
ops.watchtower -> app.runner: watch (label-enabled)
|
|
ops.watchtower -> app.ui: watch (label-enabled)
|
|
|
|
# ─── Browser ──────────────────────────────────────────────────────────────────
|
|
|
|
browser -> app.caddy: HTTPS :443\n(single entry point)
|