architecture.d2: - Split app into prod VPS (165.22.70.138) and homelab runner (192.168.0.109) - Add CrowdSec, Dozzle agent, pocket-tts (voice samples) - Valkey now shown as Asynq job queue in addition to presign cache - Add caddy-l4 Redis TCP proxy (:6380) to Caddy label - Add CI/CD node (Gitea Actions) with full job list incl. releases.json bake - Remove runner from prod app group (it runs on homelab only) - Watchtower: note runner is label-disabled on prod api-routing.d2: - Add /api/presign/* routes to backend (presign_be group) - Add /api/audio POST + status GET to both sk and be - Add /api/scrape/book and /api/scrape/book/range to scrape_sk - Catalogue: annotate Meilisearch vs legacy browse - Add Meilisearch filter/sort fields to storage node - Add Asynq queue note to Valkey storage node - Fix presign proxy: sk routes through be.presign_be, not directly to storage
181 lines
7.5 KiB
Plaintext
181 lines
7.5 KiB
Plaintext
direction: right
|
|
|
|
# ─── External ─────────────────────────────────────────────────────────────────
|
|
|
|
novelfire: novelfire.net {
|
|
shape: cloud
|
|
style.fill: "#f0f4ff"
|
|
label: "novelfire.net\n(scrape source)"
|
|
}
|
|
|
|
kokoro: Kokoro-FastAPI TTS {
|
|
shape: cloud
|
|
style.fill: "#f0f4ff"
|
|
label: "Kokoro-FastAPI TTS\n(self-hosted · homelab)\nchapter audio"
|
|
}
|
|
|
|
pockettts: pocket-tts {
|
|
shape: cloud
|
|
style.fill: "#f0f4ff"
|
|
label: "pocket-tts\n(self-hosted · homelab)\nvoice sample MP3s"
|
|
}
|
|
|
|
letsencrypt: Let's Encrypt {
|
|
shape: cloud
|
|
style.fill: "#f0f4ff"
|
|
label: "Let's Encrypt\n(ACME TLS-ALPN-01)"
|
|
}
|
|
|
|
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\n chapters · audio\n avatars · catalogue)"
|
|
}
|
|
|
|
pb-init: pb-init {
|
|
shape: rectangle
|
|
label: "pb-init\n(bootstrap PocketBase\n collections + schema)"
|
|
}
|
|
}
|
|
|
|
# ─── Storage ──────────────────────────────────────────────────────────────────
|
|
|
|
storage: Storage {
|
|
style.fill: "#eaf7ea"
|
|
|
|
minio: MinIO {
|
|
shape: cylinder
|
|
label: "MinIO :9000\nbuckets:\n chapters · audio\n avatars · catalogue"
|
|
}
|
|
|
|
pocketbase: PocketBase {
|
|
shape: cylinder
|
|
label: "PocketBase :8090\ncollections:\n books · chapters_idx\n audio_cache · progress\n scrape_jobs · app_users\n ranking · library\n comments"
|
|
}
|
|
|
|
valkey: Valkey {
|
|
shape: cylinder
|
|
label: "Valkey :6379\npresign URL cache (TTL ~55 min)\nAsynq job queue (runner tasks)"
|
|
}
|
|
|
|
meilisearch: Meilisearch {
|
|
shape: cylinder
|
|
label: "Meilisearch :7700\nindex: books\n(filterable: status · genres\n sortable: rank · rating\n total_chapters · meta_updated)"
|
|
}
|
|
}
|
|
|
|
# ─── Application — prod VPS (165.22.70.138) ───────────────────────────────────
|
|
|
|
app: Application — prod (165.22.70.138) {
|
|
style.fill: "#eef3ff"
|
|
|
|
caddy: caddy {
|
|
shape: rectangle
|
|
label: "Caddy :443 / :80 / :6380\ncustom build\n+ caddy-l4 (Redis TCP proxy)\n+ caddy-ratelimit\nauto-HTTPS · security headers\nrate limiting (per-IP)\nstatic error pages (404/502/503/504)\nCrowdSec bouncer"
|
|
}
|
|
|
|
backend: backend {
|
|
shape: rectangle
|
|
label: "Backend API :8080\n(Go)\nHTTP API server\nffmpeg (audio sample conv.)\nOpenTelemetry tracing\nSentry / GlitchTip errors"
|
|
}
|
|
|
|
ui: ui {
|
|
shape: rectangle
|
|
label: "SvelteKit UI :3000\n(adapter-node)\nSSR · session auth\nserver-side API proxy"
|
|
}
|
|
|
|
crowdsec: CrowdSec {
|
|
shape: rectangle
|
|
label: "CrowdSec :8080\nsecurity engine\nreads Caddy JSON logs\nbouncer integrated in Caddy"
|
|
}
|
|
|
|
dozzle: Dozzle agent {
|
|
shape: rectangle
|
|
label: "Dozzle agent\n127.0.0.1:7007\nlog relay → homelab dashboard"
|
|
}
|
|
}
|
|
|
|
# ─── Runner — homelab (192.168.0.109) ────────────────────────────────────────
|
|
|
|
homelab: Runner — homelab (192.168.0.109) {
|
|
style.fill: "#fef9ec"
|
|
|
|
runner: runner {
|
|
shape: rectangle
|
|
label: "Runner :9091\n(Go background worker)\nscrape pipeline\nTTS audio job queue\nPrometheus /metrics\ncron: catalogue refresh\nAsynq worker → Valkey"
|
|
}
|
|
}
|
|
|
|
# ─── Ops ──────────────────────────────────────────────────────────────────────
|
|
|
|
ops: Ops {
|
|
style.fill: "#f5f5f5"
|
|
|
|
watchtower: Watchtower {
|
|
shape: rectangle
|
|
label: "Watchtower\n(containrrr/watchtower)\npolls Docker Hub every 5 min\nautopulls + redeploys:\n backend · ui\n(runner: label-disabled on prod)"
|
|
}
|
|
}
|
|
|
|
# ─── CI / CD ──────────────────────────────────────────────────────────────────
|
|
|
|
cicd: CI / CD {
|
|
style.fill: "#f0f9ff"
|
|
|
|
gitea: Gitea Actions {
|
|
shape: rectangle
|
|
label: "Gitea Actions\n(homelab runner)\ntag v* trigger:\n test-backend\n check-ui (type-check + build)\n docker-backend\n docker-runner\n docker-ui (bakes releases.json)\n docker-caddy\n → push Docker Hub\n → Gitea Release"
|
|
}
|
|
}
|
|
|
|
# ─── Init → Storage ───────────────────────────────────────────────────────────
|
|
|
|
init.minio-init -> storage.minio: create buckets {style.stroke-dash: 4}
|
|
init.pb-init -> storage.pocketbase: bootstrap schema {style.stroke-dash: 4}
|
|
|
|
# ─── App internal ─────────────────────────────────────────────────────────────
|
|
|
|
app.caddy -> app.ui: "/* (catch-all)\nSvelteKit — auth enforced"
|
|
app.caddy -> app.backend: "/health /scrape*\n/api/browse /api/catalogue\n/api/ranking /api/version\n/api/book-preview/*\n/api/chapter-text/*\n/api/chapter-markdown/*\n/api/reindex/* /api/cover/*\n/api/audio-proxy/* /api/voices\n/api/audio* /api/presign/*"
|
|
app.caddy -> storage.minio: "/avatars/* /audio/*\n/chapters/*\n(presigned GETs)"
|
|
app.caddy -> app.crowdsec: bouncer check (15 s poll)
|
|
app.caddy -> letsencrypt: ACME cert (TLS-ALPN-01)
|
|
|
|
app.ui -> app.backend: "internal REST proxy\n(server-side only)"
|
|
app.ui -> storage.pocketbase: "auth · sessions\nprogress · library\ncomments"
|
|
|
|
app.backend -> storage.minio: "chapter objs · audio MP3s\navatars · browse cache"
|
|
app.backend -> storage.pocketbase: "books · scrape_jobs\naudio_cache · ranking"
|
|
app.backend -> storage.valkey: "presign URL cache\n(SET/GET TTL ~55 min)"
|
|
app.backend -> storage.meilisearch: "catalogue search\nfacets: genres · status"
|
|
app.backend -> pockettts: "voice sample gen.\n(on-demand · ffmpeg conv.)"
|
|
|
|
# ─── Runner → deps ────────────────────────────────────────────────────────────
|
|
|
|
homelab.runner -> novelfire: "HTTP scrape\nHTML → Markdown"
|
|
homelab.runner -> kokoro: "TTS generation\ntext → MP3"
|
|
homelab.runner -> storage.minio: "write chapters\n& audio MP3s"
|
|
homelab.runner -> storage.pocketbase: "read/update scrape_jobs\nwrite book records"
|
|
homelab.runner -> storage.meilisearch: "index books\n(on scrape completion)"
|
|
homelab.runner -> storage.valkey: "Asynq job queue\n(task consume)"
|
|
|
|
# ─── Client ───────────────────────────────────────────────────────────────────
|
|
|
|
browser -> app.caddy: HTTPS :443\n(single entry point)
|
|
|
|
# ─── Ops / CI ─────────────────────────────────────────────────────────────────
|
|
|
|
ops.watchtower -> app.backend: watch (label-enabled)
|
|
ops.watchtower -> app.ui: watch (label-enabled)
|
|
cicd.gitea -> ops.watchtower: push to Docker Hub\n→ Watchtower detects new tag
|