direction: right

# ─── Legend ───────────────────────────────────────────────────────────────────

legend: Legend {
  style.fill: "#fafafa"
  style.stroke: "#d4d4d8"

  pub: public {
    style.fill: "#f0fdf4"
    style.font-color: "#15803d"
    style.stroke: "#86efac"
  }
  user: user auth {
    style.fill: "#eff6ff"
    style.font-color: "#1d4ed8"
    style.stroke: "#93c5fd"
  }
  adm: admin only {
    style.fill: "#fff7ed"
    style.font-color: "#c2410c"
    style.stroke: "#fdba74"
  }
}

# ─── Client ───────────────────────────────────────────────────────────────────

client: Browser / iOS App {
  shape: person
  style.fill: "#fff9e6"
}

# ─── Caddy ────────────────────────────────────────────────────────────────────

caddy: Caddy :443 {
  shape: rectangle
  style.fill: "#f1f5f9"
  label: "Caddy :443\ncustom build · caddy-l4 · caddy-ratelimit\nCrowdSec bouncer · security headers\nrate limiting · static error pages\nRedis TCP proxy :6380"
}

# ─── SvelteKit UI ─────────────────────────────────────────────────────────────
# All routes here pass through SvelteKit — auth is enforced server-side.

sk: SvelteKit UI :3000 {
  style.fill: "#fef3c7"

  auth: Auth {
    style.fill: "#fde68a"
    style.stroke: "#f59e0b"
    label: "POST /api/auth/login\nPOST /api/auth/register\nPOST /api/auth/change-password\nGET  /api/auth/session"
  }

  catalogue_sk: Catalogue {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET /api/catalogue-page  (infinite scroll)\nGET /api/search"
  }

  book_sk: Book {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET /api/book/{slug}\nGET /api/chapter/{slug}/{n}\nGET /api/chapter-text-preview/{slug}/{n}"
  }

  scrape_sk: Scrape (admin) {
    style.fill: "#fff7ed"
    style.stroke: "#fdba74"
    label: "GET  /api/scrape/status\nGET  /api/scrape/tasks\nPOST /api/scrape\nPOST /api/scrape/book\nPOST /api/scrape/book/range\nPOST /api/scrape/cancel/{id}"
  }

  audio_sk: Audio {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "POST /api/audio/{slug}/{n}\nGET  /api/audio/status/{slug}/{n}\nGET  /api/voices"
  }

  presign_sk: Presigned URLs (public) {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET /api/presign/chapter/{slug}/{n}\nGET /api/presign/audio/{slug}/{n}\nGET /api/presign/voice-sample/{voice}"
  }

  presign_user: Presigned URLs (user) {
    style.fill: "#eff6ff"
    style.stroke: "#93c5fd"
    label: "GET /api/presign/avatar-upload/{userId}\nGET /api/presign/avatar/{userId}"
  }

  progress_sk: Progress {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET    /api/progress\nPOST   /api/progress/{slug}\nDELETE /api/progress/{slug}"
  }

  library_sk: Library {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET    /api/library\nPOST   /api/library/{slug}\nDELETE /api/library/{slug}"
  }

  comments_sk: Comments {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET  /api/comments/{slug}\nPOST /api/comments/{slug}"
  }
}

# ─── Go Backend ───────────────────────────────────────────────────────────────
# Caddy proxies these paths directly — bypasses SvelteKit entirely.

be: Backend API :8080 {
  style.fill: "#eef3ff"

  health_be: Health / Version {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET /health\nGET /api/version"
  }

  scrape_be: Scrape admin (direct) {
    style.fill: "#fff7ed"
    style.stroke: "#fdba74"
    label: "POST /scrape\nPOST /scrape/book\nPOST /scrape/book/range"
  }

  catalogue_be: Catalogue {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET /api/catalogue  (Meilisearch)\nGET /api/browse  (legacy MinIO cache)\nGET /api/ranking\nGET /api/cover/{domain}/{slug}"
  }

  book_be: Book / Chapter {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET  /api/book-preview/{slug}\nGET  /api/chapter-text/{slug}/{n}\nGET  /api/chapter-markdown/{slug}/{n}\nPOST /api/reindex/{slug}  ⚠ admin"
  }

  audio_be: Audio {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "POST /api/audio/{slug}/{n}\nGET  /api/audio/status/{slug}/{n}\nGET  /api/audio-proxy/{slug}/{n}\nGET  /api/voices"
  }

  presign_be: Presigned URLs {
    style.fill: "#f0fdf4"
    style.stroke: "#86efac"
    label: "GET /api/presign/chapter/{slug}/{n}\nGET /api/presign/audio/{slug}/{n}\nGET /api/presign/voice-sample/{voice}\nGET /api/presign/avatar-upload/{userId}\nGET /api/presign/avatar/{userId}"
  }
}

# ─── Storage ──────────────────────────────────────────────────────────────────

storage: Storage {
  style.fill: "#eaf7ea"

  pb: PocketBase :8090 {
    shape: cylinder
    label: "auth · books · progress\ncomments · library\nscrape_jobs · audio_cache\nranking"
  }
  mn: MinIO :9000 {
    shape: cylinder
    label: "chapters · audio\navatars · catalogue (browse)"
  }
  ms: Meilisearch :7700 {
    shape: cylinder
    label: "index: books\nfilterable: status · genres\nsortable: rank · rating\n  total_chapters · meta_updated"
  }
  vk: Valkey :6379 {
    shape: cylinder
    label: "presign URL cache (TTL ~55 min)\nAsynq job queue (runner)"
  }
}

# ─── Caddy routing ────────────────────────────────────────────────────────────

client -> caddy: HTTPS :443

caddy -> sk: "/* (catch-all)\n→ SvelteKit enforces auth"
caddy -> be: "/health  /scrape*\n/api/browse  /api/catalogue  /api/ranking\n/api/version  /api/book-preview/*\n/api/chapter-text/*  /api/chapter-markdown/*\n/api/reindex/*  /api/cover/*\n/api/audio*  /api/voices  /api/presign/*"
caddy -> storage.mn: "/avatars/*  /audio/*  /chapters/*\n(presigned MinIO GETs)"

# ─── SvelteKit → Backend (server-side proxy) ──────────────────────────────────

sk.catalogue_sk -> be.catalogue_be: internal proxy
sk.book_sk -> be.book_be: internal proxy
sk.audio_sk -> be.audio_be: internal proxy
sk.presign_sk -> be.presign_be: internal proxy
sk.presign_user -> be.presign_be: internal proxy

# ─── SvelteKit → Storage (direct) ────────────────────────────────────────────

sk.auth -> storage.pb: sessions / users
sk.scrape_sk -> storage.pb: scrape job records
sk.progress_sk -> storage.pb
sk.library_sk -> storage.pb
sk.comments_sk -> storage.pb

# ─── Backend → Storage ────────────────────────────────────────────────────────

be.catalogue_be -> storage.ms: full-text search + facets
be.catalogue_be -> storage.pb: ranking records
be.catalogue_be -> storage.mn: cover presign
be.book_be -> storage.mn: chapter objects
be.book_be -> storage.pb: book metadata
be.audio_be -> storage.mn: audio presign
be.audio_be -> storage.vk: presign cache
be.presign_be -> storage.vk: check / set presign cache
be.presign_be -> storage.mn: generate presigned URL
