26 KiB
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 []).
⚠️
createPostpublica por defecto (published = input.published ?? 1) — todo post nuevo sale publicado. ⚠️updatePostinterpola 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,secureen production, sameSite lax, 7d). - Token: JWT HS256, payload
{ authenticated: true, timestamp }, expiración7d. - Secret:
process.env.JWT_SECRET || process.env.WEB_PASSWORD || "worst-scan-web-dev-secret"⚠️ fallback predecible. - Login:
POST /api/auth/logincon{ password }→ compara conWEB_PASSWORD. Si no hayWEB_PASSWORDconfigurada, emite sesión sin verificar nada. - Logout:
POST /api/auth/logoutborra la cookie (el JWT no se revoca, solo expira). - Middleware (
src/middleware.ts): paths enpublicPaths→ allow; páginas/,/p/,/tag/→ allow; sinWEB_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/poststambién destapa/api/posts/[id]y/api/posts/[id]/publish;/api/crondestapa/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_KEYsi 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-Typede vuelta; descartacontent-encoding/content-length/transfer-encoding. - Errores upstream →
502 {error:{code:"proxy_error", message}}. GET /api/proxy/healthpúblico (allowlist); el resto protegido por sesión (siWEB_PASSWORDset).
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.ts→register()(soloNEXT_RUNTIME === "nodejs") llamainitSettings()+startPoller(). pollOnce():- Lista
api.galleries.list("completed", 1, 100)+api.galleries.translated()del pipeline. - Por cada gid:
api.galleries.summary(gid). cleanTitle()+slugify(cleanT, gid); sintetiza summary (synopsisogenerateSummary()).- Moderación:
moderateTags()(ver §Moderación) — tags prohibidos → cuarentena (published: 0) +notifyQuarantine()a Discord de moderación. - 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 existetitle_es. - Auto-publica: no existe →
createPost({...}); existe →updatePost. Si pasó a tener tags dudosos →publishPost(id, false)(cuarentena). cacheCover(gid, "http://127.0.0.1:{PORT||3000}/api/proxy/galleries/{gid}/cover").- Posts limpios nuevos →
notifyNewPost()(Discord de novedades).
- Lista
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, default127.0.0.1:6446/v1, keyFREE_IDE_PROXY_KEY). - Modelos:
deepseek-v4-flash-free(primario) →big-pickle(fallback).max_tokens: 200para 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): devuelvetitle_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(conLIMIT=npara 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 —hololiveNO 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
/galleriesdel pipeline solo lista las galleries del UrlQueue activo (~6), no las traducidas enoutput/artifacts/(~38 invisibles). El poller no puede descubrirlas. Solución: que/galleriesescaneeoutput/artifacts/o use/galleries/translated.
⚠️ BUG self-fetch:
cacheCoverfetchea/api/proxy/...(protegido por middleware); sin cookie de servidor, conWEB_PASSWORDset el fetch recibe redirect 307 a/loginy 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-256oX-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. LuegocacheCover(...)(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.createObjectURLpor 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" siisAnthology(tags anthology/compilation/tankoubon onum_pages >= 100),TagBadge(artist/parody →/tag/...), sinopsis solo siisRealSynopsis(no empieza con**/Fuente:y >50 chars), "Más publicaciones" (4 compact). Botones: Leer Manga (/gallery/[gid]/read— privado), 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]/read— lector: modospaged(zoom 0.5–3, flechas, slider, tap zones, swipe >40px, precarga 2 págs) ywebtoon(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. Rootfixed 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./search—SearchForm: POST/api/proxy/search{query, source}→ grid con estadoblocked; "Encolar" / "Encolar todo (N)" → POST/api/proxy/galleries./submit—SubmitForm: textarea URLs → POST/api/proxy/galleriespor 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:
Sidebardesktop 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→ siconfiguredva a/feed; si no, 4 campos (API_BASE_URL,API_KEY,WEB_PASSWORDmin 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")englobals.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. @utilitycustom:card,card-hover,tag-pill,btn,btn-primary,btn-ghost,btn-danger,input,skeleton(+ shimmer).- Fuente Inter (
@font-facewoff2 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(usuarionextjsuid 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, HEALTHCHECKwget /api/health). - Compose:
expose: 3000(sin mapeo),env_file: .env,extra_hosts: host.docker.internal:host-gateway, redcaddy(external), volumendata:/app/data,restart: unless-stopped. - Caddy en contenedor
caddy-ingresscon auto-HTTPS paramanga.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.jsbajo systemd (Type=exec,Restart=always,HOSTNAME=127.0.0.1,PORT=3000). - ⚠️
install.shdesinstala Docker a propósito — no conviven con la estrategia A. - ⚠️
deploy/setup.shescribeWorkingDirectory=$APP_DIR/standalone(falta.next/). - ⚠️
deploy/setup.sh:22contiene la contraseña de sudo hardcodeada en texto plano — CRÍTICO, parametrizar.
Scripts
npm start→scripts/start.js: sincroniza.next/static→ standalone,NODE_ENV=production, spawnnode server.jsconPORT/HOSTNAME.install.sh: swap 1GB si <512MB → git/curl → Docker cleanup → Node 22 (nodesource) → mirror npm si China → Caddy (cloudsmith) → clonegitea.cbcren.online/renato97/worst-scan-web(reset --hard) →.env(nunca sobreescribe) → build → systemd unit → bloque Caddy:80→ health wait → resumen conhttp://IP/setup.
Bugs Conocidos & Deuda Técnica
🔴 Seguridad
- SQL injection en
updatePost(db.ts:148-170) — nombres de columna del body interpolados sin whitelist; permite sobrescribirpublished/published_aty columnas arbitrarias. - Middleware por prefijo (
middleware.ts:23) —/api/posts*y/api/cron*públicos en la práctica (PATCH/DELETE/publish/cron sin auth). - Sin
WEB_PASSWORD→ app 100% abierta, incluidas rutas admin. /api/setupsin auth — cualquiera configura la app y fija secrets (guardados en texto plano endata/settings.json).- Webhook sin
WEBHOOK_SECRET→ abierto (crear/modificar posts arbitrarios). - JWT fallback predecible (
worst-scan-web-dev-secret); si se usaWEB_PASSWORDcomo clave, quien la conozca forja tokens. - Password de sudo hardcodeada en
deploy/setup.sh:22. - Logout no revoca el JWT (solo expira); sin refresh/rotación.
🐛 Funcionales
- ~38 galerías invisibles —
/galleriesdel pipeline solo lista las del UrlQueue (~6); el poller no descubre las traducidas. Bloqueante para la web. - Cover self-cache rota con auth — fetch de servidor sin cookie recibe 307 → guarda HTML de login como portada.
createPostpublica todo por defecto — no hay flujo de borrador real.getGidsNotInPosts()es un stub (return []).ALTER TABLE posts ADD COLUMN urlenmigrate()es código muerto (columna ya existe → siempre lanza y se traga).- Enlaces rotos público→privado: "Buscar" (
/search) y "Leer Manga" (/gallery/[gid]/read) llevan a rutas con login. - Bug de conteo en
SubmitForm—"X de Y ok"inflado (cuenta todo menos"?"); criterio de fila inconsistente con el contador. - Hooks muertos:
useGallery,useQueue,useQueueStats,useStatus— sin uso; fetcher SWR duplicado en 6 archivos. - Overlap bottom-nav móvil — el layout
(app)no compensa la nav fija (p-0sinpb-14). - Logo del sidebar →
/(saca al usuario del panel al sitio público); sin logout en móvil. decodeURIComponent(tag)sin try/catch → 500 con%literal en el tag.- Sin paginación —
per_page=50/100fijos, sin UI de página pese a que la API devuelvemeta. - Lector: zoom solo desktop; tap zones inertes en webtoon;
touch-none+touch-pan-ypueden trabar scroll; títulog/{gid}si falla summary. window.location.href = "/feed"tras eliminar (recarga completa); errores silenciosos en editor de posts, retry y search.
🟡 Docs / Infra
README.mdes boilerplate de create-next-app;AGENTS.md(viejo) documentaba el pipeline Python que no está en este repo — esta versión lo reemplaza.- Docker vs systemd contradictorios (
install.shdesinstala Docker). .env.examplecon paths Docker (/app/data,host.docker.internal) que en bare-metal hay que sobreescribir.- CORS
*en la API del pipeline (lockear en producción). dominios-disponibles.mddesactualizado (manga.cbcren.onlinefigura libre).
Roadmap / Plan Pendiente (de agy.md, 7 pasos)
- Columnas
pages_count,mobi_stored,cbz_pathenposts. - Nuevo
src/lib/gallery-store.ts(downloadAndExtractCbz con JSZip, downloadMobi, getPagePath…). - Rewrite del poller (que descubra galleries traducidas).
- Endpoints
/api/pages/*y/api/download/*/mobi. - Mover lector a grupo
(reader)/(fuera del layout admin). - 6 UI fixes (enlaces públicos, paginación, hooks, bottom-nav…).
- 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.