Files
worst-scan-web/AGENTS.md
T

26 KiB
Raw Blame History

worst-scan-web — Portal Web del Fansub (Next.js 16)

Portal web público + panel admin del fansub de manga worst-scan (escaneado y traducción automática JP/EN/KR/CN → ES). Este repo es SOLO el frontend web (Next.js). El pipeline de traducción (descarga, OCR, LLM, inpainting, render, MOBI, bot de Telegram) vive en un repo aparte y expone una API REST FastAPI en :8080 (API_BASE_URL), que esta web consume vía proxy.

Dominio de producción: manga.cbcren.online (vía Caddy con auto-HTTPS) Doc de arquitectura original: ver agy.md (AGY = Arquitectura, Galerías, Yapa)


Stack

Capa Tecnología
Framework Next.js 16.2.11 (App Router, Turbopack, output: standalone)
UI React 19.2.4, Tailwind CSS 4, iconos lucide-react
Datos SQLite vía better-sqlite3 ^13 (modo WAL), SWR ^2.4.2 en cliente
Auth JWT HS256 vía jose ^6 (cookie session, expiración 7d)
Utilidades JSZip ^3 (parseo CBZ en cliente), TypeScript 5
Deploy Docker (multi-stage Node 22-alpine) o systemd + Node directo; Caddy como reverse proxy

Arquitectura General

                            ┌─────────────────────────────────────┐
                            │       Caddy (manga.cbcren.online)    │
                            │         auto-HTTPS + TLS            │
                            └──────────────┬──────────────────────┘
                                           │
                            ┌──────────────▼──────────────────────┐
                            │      worst-scan-web (Next.js)       │
                            │        puerto 3000 (container)      │
                            │                                     │
                            │  ┌─────────────────────────────┐    │
                            │  │  Sitio PÚBLICO (server-side)│    │
                            │  │  /  /p/[slug]  /tag/[tag]   │    │
                            │  │  → SQLite directo (posts)   │    │
                            │  └─────────────────────────────┘    │
                            │  ┌─────────────────────────────┐    │
                            │  │  Panel PRIVADO (client-side)│    │
                            │  │  /feed /queue /search       │    │
                            │  │  /submit /admin/posts       │    │
                            │  │  → SWR → /api/proxy/*       │    │
                            │  └─────────────────────────────┘    │
                            │  ┌─────────────────────────────┐    │
                            │  │  API propia: /api/auth,      │    │
                            │  │  /api/posts, /api/cover,     │    │
                            │  │  /api/cron, /api/setup,      │    │
                            │  │  /api/webhooks/manga,        │    │
                            │  │  /api/proxy/[...path]        │    │
                            │  └─────────────────────────────┘    │
                            └──────────────┬──────────────────────┘
                                           │ API_BASE_URL (proxy)
                            ┌──────────────▼──────────────────────┐
                            │   Pipeline REST (FastAPI :8080)      │
                            │   /api/v1/health|status|galleries    │
                            │   /summary|artifacts|cover|pages     │
                            └─────────────────────────────────────┘

Estructura de Archivos

src/
├── lib/
│   ├── db.ts            # SQLite: tabla posts + capa de datos completa
│   ├── auth.ts          # JWT HS256: createSession / verifySession
│   ├── api.ts           # Cliente del pipeline REST (API_BASE_URL)
│   ├── types.ts         # Interfaces (GallerySummary, etc.)
│   ├── cbz.ts           # loadPagesFromCbz (JSZip) — cliente
│   ├── cover-cache.ts   # cacheCover / getCoverPath (disco, por gid)
│   ├── poller.ts        # startPoller / pollOnce — sync pipeline→posts
│   ├── settings.ts      # initSettings / get / saveSettings (JSON)
│   ├── slug.ts          # slugify / cleanTitle
│   └── utils.ts         # formatSize, extractTag, cn, etc.
├── middleware.ts        # Auth por prefijo de path (ver §Auth)
├── instrumentation.ts   # register() → initSettings + startPoller (1 vez)
├── app/
│   ├── layout.tsx       # Root: <html lang="es">, metadata, Inter
│   ├── globals.css      # Tema oscuro + @utility Tailwind v4
│   ├── (public)/        # GRUPO PÚBLICO — server-side, sin login
│   │   ├── layout.tsx   # Header sticky + nav "Buscar"
│   │   ├── page.tsx     # / grid de posts publicados
│   │   ├── p/[slug]/page.tsx    # detalle de post (fansub)
│   │   └── tag/[tag]/page.tsx   # posts por tag
│   ├── (app)/           # GRUPO PRIVADO — client-side, login requerido
│   │   ├── layout.tsx   # Sidebar desktop + bottom-nav móvil
│   │   ├── feed/page.tsx        # /feed galleries del pipeline
│   │   ├── gallery/[gid]/page.tsx       # detalle gallery
│   │   ├── gallery/[gid]/read/page.tsx  # lector (paged/webtoon)
│   │   ├── queue/page.tsx       # /queue estado pipeline
│   │   ├── search/page.tsx      # /search nhentai/e-hentai
│   │   ├── submit/page.tsx      # /submit encolar URLs
│   │   └── admin/posts/page.tsx + [id]/page.tsx  # CRUD posts
│   ├── login/page.tsx   # /login (password)
│   ├── setup/page.tsx   # /setup primer arranque (4 campos)
│   └── api/             # Route handlers (ver §API)
├── components/          # cover-image, gallery-card, gallery-grid,
│   │                    # post-card, tag-badge, filter-bar,
│   │                    # search-form, submit-form, empty-state,
│   │                    # layout/sidebar
├── hooks/               # use-galleries, use-queue, use-status
data/                    # worst-scan.db (SQLite), covers/, (runtime)
deploy/                  # Caddyfile, setup.sh, worst-scan-web.service
scripts/                 # start.js, setup.sh

Base de Datos (src/lib/db.ts)

SQLite vía better-sqlite3. Ruta: process.env.DB_PATH o data/worst-scan.db. WAL + foreign_keys = ON.

Tabla posts:

Columna Tipo Notas
id INTEGER PK AUTOINCREMENT
gid TEXT NOT NULL, UNIQUE — id de la gallery del pipeline
title TEXT NOT NULL
title_jpn TEXT nullable
artist TEXT nullable
parody TEXT nullable
tags TEXT JSON serializado (string[])
num_pages INTEGER DEFAULT 0
source TEXT nhentai / ehentai
cover_url TEXT nullable
url TEXT nullable
summary TEXT nullable
slug TEXT UNIQUE NOT NULL — URL amigable /p/{slug}
published INTEGER DEFAULT 0
created_at TEXT datetime('now')
updated_at TEXT datetime('now')
published_at TEXT nullable

Índices: idx_posts_gid, idx_posts_slug (redundantes con UNIQUE), idx_posts_published, idx_posts_created(created_at DESC).

Funciones de la capa de datos: getDb(), migrate(), rowToPost(), getAllPosts(publishedOnly), getPostById(), getPostByGid(), getPostBySlug(), createPost(), updatePost(), publishPost(), deletePost(), getPostsByTag() (usa json_each), getPostCount(), getGidsNotInPosts() (stub — devuelve []).

⚠️ createPost publica por defecto (published = input.published ?? 1) — todo post nuevo sale publicado. ⚠️ updatePost interpola los nombres de columna del body sin whitelist → riesgo de SQL injection (ver §Bugs).


Autenticación (src/lib/auth.ts + src/middleware.ts)

  • Cookie: session (httpOnly, secure en production, sameSite lax, 7d).
  • Token: JWT HS256, payload { authenticated: true, timestamp }, expiración 7d.
  • Secret: process.env.JWT_SECRET || process.env.WEB_PASSWORD || "worst-scan-web-dev-secret" ⚠️ fallback predecible.
  • Login: POST /api/auth/login con { password } → compara con WEB_PASSWORD. Si no hay WEB_PASSWORD configurada, emite sesión sin verificar nada.
  • Logout: POST /api/auth/logout borra la cookie (el JWT no se revoca, solo expira).
  • Middleware (src/middleware.ts): paths en publicPaths → allow; páginas /, /p/, /tag/ → allow; sin WEB_PASSWORD → TODO abierto; si no, exige cookie válida → redirect /login.

publicPaths: /login, /api/auth/login, /api/auth/logout, /api/health, /api/proxy/health, /api/posts, /api/cover, /api/cron, /api/setup, /setup, /_next, /favicon.ico, /fonts.

⚠️ BUG: la coincidencia es por prefijo (pathname.startsWith(p)): /api/posts también destapa /api/posts/[id] y /api/posts/[id]/publish; /api/cron destapa /api/cron/poll. Consecuencia: crear/editar/borrar/publicar posts y disparar el cron nunca requieren auth.


API Propia (route handlers)

Ruta Métodos Auth Función
/api/auth/login POST pública Valida password → setea cookie session
/api/auth/logout POST pública Borra cookie
/api/health GET pública {status:"ok", uptime} — healthcheck
/api/cover/[gid] GET pública Sirve portada cacheada (Cache-Control: public, max-age=86400); si falta, self-cachea desde /api/proxy/galleries/{gid}/cover; 404 si no hay
/api/cron/poll GET pública (bug) Invoca pollOnce(){ok, newPosts}
/api/posts GET/POST pública (bug) GET: lista posts (?published=1 filtra); POST: crea (409 si el gid existe)
/api/posts/[id] GET/PATCH/DELETE pública (bug) GET detalle; PATCH updatePost (⚠️ SQLi); DELETE borra + cover
/api/posts/[id]/publish POST pública (bug) {publish: bool}publishPost
/api/proxy/[...path] GET/POST/PUT/DELETE/PATCH protegida* Proxy inverso al pipeline (API_BASE_URL + / + subpath)
/api/setup GET/POST pública GET: {configured}; POST: guarda allowlist ["API_BASE_URL","API_KEY","WEB_PASSWORD","WEBHOOK_SECRET"] en data/settings.json ⚠️ sin auth
/api/webhooks/manga POST firma HMAC* Crea/actualiza post desde el pipeline + cachea cover

* cuando WEB_PASSWORD/WEBHOOK_SECRET están configurados; si no, abiertos.


Proxy (src/app/api/proxy/[...path]/route.ts)

  • Target: API_BASE = process.env.API_BASE_URL || "http://127.0.0.1:8080/api/v1".
  • Autentica upstream con X-API-Key: process.env.API_KEY si está set.
  • Mapeo: /api/proxy/galleries/123?x=1{API_BASE}/galleries/123?x=1 (query string se pasa tal cual).
  • Reenvía body (blob) solo en métodos no-GET/HEAD; copia Content-Type de vuelta; descarta content-encoding/content-length/transfer-encoding.
  • Errores upstream → 502 {error:{code:"proxy_error", message}}.
  • GET /api/proxy/health público (allowlist); el resto protegido por sesión (si WEB_PASSWORD set).

Poller (src/lib/poller.ts + src/instrumentation.ts)

  • POLL_INTERVAL_MS = 60_000 — corre cada 1 minuto (más una vez al arranque).
  • Arranque: instrumentation.tsregister() (solo NEXT_RUNTIME === "nodejs") llama initSettings() + startPoller().
  • pollOnce():
    1. Lista api.galleries.list("completed", 1, 100) + api.galleries.translated() del pipeline.
    2. Por cada gid: api.galleries.summary(gid).
    3. cleanTitle() + slugify(cleanT, gid); sintetiza summary (synopsis o generateSummary()).
    4. Moderación: moderateTags() (ver §Moderación) — tags prohibidos → cuarentena (published: 0) + notifyQuarantine() a Discord de moderación.
    5. Traducción de título: translateTitle()title_es (vía proxy free-ide local, deepseek-v4-flash-free con fallback big-pickle). Solo si no existe title_es.
    6. Auto-publica: no existe → createPost({...}); existe → updatePost. Si pasó a tener tags dudosos → publishPost(id, false) (cuarentena).
    7. cacheCover(gid, "http://127.0.0.1:{PORT||3000}/api/proxy/galleries/{gid}/cover").
    8. Posts limpios nuevos → notifyNewPost() (Discord de novedades).

Traducción de títulos (src/lib/translate.ts)

  • Traduce títulos de manga al español latinoamericano usando el proxy free-ide local (FREE_IDE_PROXY_URL, default 127.0.0.1:6446/v1, key FREE_IDE_PROXY_KEY).
  • Modelos: deepseek-v4-flash-free (primario) → big-pickle (fallback). max_tokens: 200 para que el razonamiento no consuma toda la respuesta.
  • Heurística isProbablySpanish(): no traduce lo que ya parece español (evita re-traducir nombres propios).
  • displayTitle(post) (src/lib/title.ts): devuelve title_es || title — usarlo en TODA la UI pública (cards, home hero, página de post, RSS).
  • Migración masiva de existentes: node scripts/translate_existing.js (con LIMIT=n para probar un lote).

Moderación de contenido (src/lib/moderation.ts)

  • Capa 1 — tags explícitos: lolicon, shotacon, loli, shota, child, children, kids, minor (regex de palabra completa — hololive NO cae).
  • Capa 2 — contexto: combinaciones de tags que sugieren menores sin tag explícito (small breasts + crying + defloration, etc.).
  • Posts dudosos → cuarentena (published=0) + aviso a Discord de moderación (DISCORD_MODERATION_WEBHOOK_URL) con links Aprobar / 🗑️ Rechazar.
  • Decisión: GET /api/review/[id]?action=approve|reject&token=DISCORD_MOD_TOKEN (protegido por token, no sesión).
  • Auditoría manual: node scripts/audit_lolicon.js.

⚠️ BLOQUEANTE CONOCIDO: el endpoint /galleries del pipeline solo lista las galleries del UrlQueue activo (~6), no las traducidas en output/artifacts/ (~38 invisibles). El poller no puede descubrirlas. Solución: que /galleries escanee output/artifacts/ o use /galleries/translated.

⚠️ BUG self-fetch: cacheCover fetchea /api/proxy/... (protegido por middleware); sin cookie de servidor, con WEB_PASSWORD set el fetch recibe redirect 307 a /login y guarda HTML de login como portada.


Webhook (src/app/api/webhooks/manga/route.ts)

  • Solo POST. Firma HMAC-SHA256 del raw body; header x-signature-256 o X-Signature-256. Secret: WEBHOOK_SECRET || FANSUB_WEBHOOK_SECRET. Sin secret configurado → verificación omitida, endpoint abierto.
  • Payload: gid (req), title (req), tags[], num_pages|pages, synopsis, url, source, title_jpn, artist, parody, cover_url.
  • Acción: no existe → createPost({..., published: 1}); existe → updatePost. Luego cacheCover(...) (mismo bug de self-fetch).
  • Respuesta {success, gid, post}; 401 firma mala; 400 sin gid/title.

Caché de Portadas (src/lib/cover-cache.ts)

  • Storage: process.env.COVERS_DIR || data/covers/. Archivo por gid: {gid}.{ext} (sin hash).
  • cacheCover(gid, proxyUrl): fetch con timeout 15s → buffer a disco; falla silenciosa (null).
  • getCoverPath(gid): readdirSync().filter(f => f.startsWith(gid + ".")). getCoverContentType, deleteCover.

CBZ (src/lib/cbz.ts) — solo cliente

  • loadPagesFromCbz(url, signal?){pages, pageCount}: fetch → JSZip → filtra imágenes (.jpg/.jpeg/.png/.webp/.gif/.bmp) → ordena por primer número en el nombre → URL.createObjectURL por página.
  • revokePageUrls(urls) libera los object URLs.
  • No hay lectura de CBZ en servidor ni endpoints de páginas reales (plan: /api/pages/[gid]).

Frontend — Grupos y Páginas

(public) — server-side, sin login (lee SQLite directo)

  • / — grid de posts publicados (getAllPosts(true)), PostCard, grid-cols-2 sm:3 md:4 xl:5.
  • /p/[slug] — detalle de post: portada /api/cover/{gid}, título + title_jpn, badge "Antología/Tomo" si isAnthology (tags anthology/compilation/tankoubon o num_pages >= 100), TagBadge (artist/parody → /tag/...), sinopsis solo si isRealSynopsis (no empieza con **/Fuente: y >50 chars), "Más publicaciones" (4 compact). Botones: Leer Manga (/gallery/[gid]/readprivado), Ver en nhentai/e-hentai, Archivos (desktop).
  • /tag/[tag] — posts por tag (getPostsByTag), decodeURIComponent(tag) ⚠️ sin try/catch.

(app) — client-side, login requerido (SWR → /api/proxy/*)

  • /feed — galleries del pipeline (useGalleries(status, 1) → SWR /api/proxy/galleries?page=1&per_page=50&status=..., refresh 30s/10s), FilterBar + GalleryGrid + skeletons.
  • /gallery/[gid] — detalle: SWR /summary + /artifacts, CoverImage, tags, badge ESP, lista de archivos colapsable, botones Leer/Original/Eliminar.
  • /gallery/[gid]/readlector: modos paged (zoom 0.53, flechas, slider, tap zones, swipe >40px, precarga 2 págs) y webtoon (scroll continuo, lazy desde 5ª); controles auto-ocultos 3.5s; fullscreen (f); keyboard ←→, +/-/0; cascada de fallback para páginas: /pages/summary (num_pages) → /artifacts → HEAD /cover → error. Root fixed inset-0 z-50 bg-[#09090b] text-white touch-none.
  • /queue — estado pipeline: SWR /api/proxy/queue?status=...&per_page=100 + /api/proxy/status (refresh 10s), SlotGauge, StatBox, botón Retry.
  • /searchSearchForm: POST /api/proxy/search {query, source} → grid con estado blocked; "Encolar" / "Encolar todo (N)" → POST /api/proxy/galleries.
  • /submitSubmitForm: textarea URLs → POST /api/proxy/galleries por línea.
  • /admin/posts — CRUD posts: toggle publicar (POST /api/posts/{id}/publish), eliminar (confirm), links a /p/{slug} y /gallery/{gid}; editor [id]: PATCH {title, summary, slug}.
  • Layout: Sidebar desktop fija (w-56, 5 links + Salir) + bottom-nav móvil (h-14, sin Salir); logo apunta a / (público).

Standalone

  • /login — password → POST /api/auth/login/feed.
  • /setup — primer arranque: GET /api/setup → si configured va a /feed; si no, 4 campos (API_BASE_URL, API_KEY, WEB_PASSWORD min 6, WEBHOOK_SECRET) → POST /api/setup.

Hooks

useGalleries(status, page) (usado), useGallery (muerto), useQueue/useQueueStats (muertos), useStatus/useSystemStatus (muerto). QueuePage y GalleryDetailPage reimplementan SWR inline con fetcher duplicado.


Estilos (Tailwind CSS 4)

  • @import "tailwindcss" source("../../src") en globals.css. Sin CSS modules.
  • Tema oscuro exclusivo (html { color-scheme: dark }): --background:#09090b, --surface:#18181b, --border:#27272a, --foreground:#fafafa, --muted:#a1a1aa, --accent:#6366f1 (indigo), --success:#22c55e, --warning:#eab308, --error:#ef4444 (+ variantes -subtle), --radius:.5rem.
  • @utility custom: card, card-hover, tag-pill, btn, btn-primary, btn-ghost, btn-danger, input, skeleton (+ shimmer).
  • Fuente Inter (@font-face woff2 de fonts.gstatic.com, fallback system-ui). Iconos lucide-react. Scrollbars finos.
  • Hardcodeos menores: lector bg-[#09090b] fijo; badge ESP emerald fijo.

Variables de Entorno (.env / .env.example)

Variable Default Propósito
API_BASE_URL http://host.docker.internal:8080/api/v1 Base URL del pipeline REST (bare-metal: 127.0.0.1)
API_KEY (vacío) API key del pipeline (vacío = sin auth upstream)
WEB_PASSWORD (vacío) Password del panel admin (vacío = todo abierto)
WEBHOOK_SECRET (vacío) Validación HMAC del webhook
DB_PATH data/worst-scan.db Ruta SQLite
COVERS_DIR data/covers Directorio de portadas
JWT_SECRET (usado por auth.ts si existe; si no, cae a WEB_PASSWORD)
PORT 3000 Puerto del server (Dockerfile/systemd)
HOSTNAME 0.0.0.0 Bind (Docker/systemd)

Despliegue

Dos estrategias documentadas, MUTUAMENTE EXCLUYENTES:

A) Docker (agy.md — la real en producción)

  • Multi-stage Node 22-alpine: deps (python3/make/g++ para better-sqlite3 + npm ci) → builder (npm run build) → runner (usuario nextjs uid 1001, EXPOSE 3000, ENV PORT=3000 HOSTNAME=0.0.0.0 DB_PATH=/app/data/worst-scan.db COVERS_DIR=/app/data/covers, VOLUME /app/data, HEALTHCHECK wget /api/health).
  • Compose: expose: 3000 (sin mapeo), env_file: .env, extra_hosts: host.docker.internal:host-gateway, red caddy (external), volumen data:/app/data, restart: unless-stopped.
  • Caddy en contenedor caddy-ingress con auto-HTTPS para manga.cbcren.online; recarga: docker exec caddy-ingress caddy reload --config /etc/caddy/Caddyfile. Caddyfile canónico del host: /home/ren/immich/Caddyfile.

B) systemd + Node directo (install.sh / deploy/setup.sh)

  • Instala Node 22 + Caddy, npm run build, corre .next/standalone/server.js bajo systemd (Type=exec, Restart=always, HOSTNAME=127.0.0.1, PORT=3000).
  • ⚠️ install.sh desinstala Docker a propósito — no conviven con la estrategia A.
  • ⚠️ deploy/setup.sh escribe WorkingDirectory=$APP_DIR/standalone (falta .next/).
  • ⚠️ deploy/setup.sh:22 contiene la contraseña de sudo hardcodeada en texto plano — CRÍTICO, parametrizar.

Scripts

  • npm startscripts/start.js: sincroniza .next/static → standalone, NODE_ENV=production, spawn node server.js con PORT/HOSTNAME.
  • install.sh: swap 1GB si <512MB → git/curl → Docker cleanup → Node 22 (nodesource) → mirror npm si China → Caddy (cloudsmith) → clone gitea.cbcren.online/renato97/worst-scan-web (reset --hard) → .env (nunca sobreescribe) → build → systemd unit → bloque Caddy :80 → health wait → resumen con http://IP/setup.

Bugs Conocidos & Deuda Técnica

🔴 Seguridad

  1. SQL injection en updatePost (db.ts:148-170) — nombres de columna del body interpolados sin whitelist; permite sobrescribir published/published_at y columnas arbitrarias.
  2. Middleware por prefijo (middleware.ts:23) — /api/posts* y /api/cron* públicos en la práctica (PATCH/DELETE/publish/cron sin auth).
  3. Sin WEB_PASSWORD → app 100% abierta, incluidas rutas admin.
  4. /api/setup sin auth — cualquiera configura la app y fija secrets (guardados en texto plano en data/settings.json).
  5. Webhook sin WEBHOOK_SECRET → abierto (crear/modificar posts arbitrarios).
  6. JWT fallback predecible (worst-scan-web-dev-secret); si se usa WEB_PASSWORD como clave, quien la conozca forja tokens.
  7. Password de sudo hardcodeada en deploy/setup.sh:22.
  8. Logout no revoca el JWT (solo expira); sin refresh/rotación.

🐛 Funcionales

  1. ~38 galerías invisibles/galleries del pipeline solo lista las del UrlQueue (~6); el poller no descubre las traducidas. Bloqueante para la web.
  2. Cover self-cache rota con auth — fetch de servidor sin cookie recibe 307 → guarda HTML de login como portada.
  3. createPost publica todo por defecto — no hay flujo de borrador real.
  4. getGidsNotInPosts() es un stub (return []).
  5. ALTER TABLE posts ADD COLUMN url en migrate() es código muerto (columna ya existe → siempre lanza y se traga).
  6. Enlaces rotos público→privado: "Buscar" (/search) y "Leer Manga" (/gallery/[gid]/read) llevan a rutas con login.
  7. Bug de conteo en SubmitForm"X de Y ok" inflado (cuenta todo menos "?"); criterio de fila inconsistente con el contador.
  8. Hooks muertos: useGallery, useQueue, useQueueStats, useStatus — sin uso; fetcher SWR duplicado en 6 archivos.
  9. Overlap bottom-nav móvil — el layout (app) no compensa la nav fija (p-0 sin pb-14).
  10. Logo del sidebar → / (saca al usuario del panel al sitio público); sin logout en móvil.
  11. decodeURIComponent(tag) sin try/catch → 500 con % literal en el tag.
  12. Sin paginaciónper_page=50/100 fijos, sin UI de página pese a que la API devuelve meta.
  13. Lector: zoom solo desktop; tap zones inertes en webtoon; touch-none+touch-pan-y pueden trabar scroll; título g/{gid} si falla summary.
  14. window.location.href = "/feed" tras eliminar (recarga completa); errores silenciosos en editor de posts, retry y search.

🟡 Docs / Infra

  1. README.md es boilerplate de create-next-app; AGENTS.md (viejo) documentaba el pipeline Python que no está en este repo — esta versión lo reemplaza.
  2. Docker vs systemd contradictorios (install.sh desinstala Docker).
  3. .env.example con paths Docker (/app/data, host.docker.internal) que en bare-metal hay que sobreescribir.
  4. CORS * en la API del pipeline (lockear en producción).
  5. dominios-disponibles.md desactualizado (manga.cbcren.online figura libre).

Roadmap / Plan Pendiente (de agy.md, 7 pasos)

  1. Columnas pages_count, mobi_stored, cbz_path en posts.
  2. Nuevo src/lib/gallery-store.ts (downloadAndExtractCbz con JSZip, downloadMobi, getPagePath…).
  3. Rewrite del poller (que descubra galleries traducidas).
  4. Endpoints /api/pages/* y /api/download/*/mobi.
  5. Mover lector a grupo (reader)/ (fuera del layout admin).
  6. 6 UI fixes (enlaces públicos, paginación, hooks, bottom-nav…).
  7. Cover fallback a pages/001.jpg.

What NOT to Add

  • No CI/CD pipelines, no pre-commit hooks, no linter/formatter configs nuevos (seguir el espíritu del repo).
  • No escribir raw SQL fuera de db.ts.
  • No exponer secrets en texto plano (ni en scripts ni en settings.json).
  • No nuevos entry points sin discusión previa.