Files
Renato 269762557e docs: agy.md — architecture, routes, bugfix, and pending plan
- Fix poller shape mismatch (res.data -> res.data?.data) that caused
  silent TypeError and 0 posts despite completed galleries (#1)
- Align types in api.ts to reflect ApiResponse<PaginatedResponse<>> shape
- Comprehensive agy.md documenting architecture, routes, 43 CBZ galleries,
  known bugs, blocker (API listing), and implementation plan
2026-07-24 00:50:15 +02:00

13 KiB

worst-scan web — AGY (Arquitectura, Galerías, Yapa)

Generado: 2026-07-23 — Post-mortem del setup inicial + deuda técnica


Stack

Capa Tecnología
Framework Next.js 16.2.11 (Turbopack, standalone output)
Lenguaje TypeScript 5
UI Tailwind 4 + Lucide icons
DB SQLite via better-sqlite3 (WAL mode)
API client fetch plano (sin axios/fetch wrapper pesado)
CBZ parsing JSZip (vía src/lib/cbz.ts)
Auth JWT via jose (cookie session)
Deploy Docker (Node 22-alpine, multi-stage build)
Proxy reverso Caddy (dominio manga.cbcren.online)

Arquitectura

manga.cbcren.online (Caddy)
  └─ worst-scan-web:3000 (Docker container, red caddy)
       │
       ├─ API_BASE_URL=http://host.docker.internal:8080/api/v1
       │    └─ Pipeline API (kindle_final, FastAPI en :8080)
       │
       └─ /app/data/ (volumen persistente worst-scan-web-data)
            ├─ worst-scan.db (SQLite — posts, estados)
            ├─ covers/ (portadas cacheadas)
            └─ galleries/ (páginas extraídas + MOBIs — futuro)

Conexiones

Origen Destino Método
worst-scan-web host.docker.internal:8080 HTTP (pipeline API)
worst-scan-web --add-host host.docker.internal:host-gateway Docker

Route map (público)

Ruta Tipo Descripción Status
/ Página Home — grid de posts
/p/[slug] Página Detalle de post (cover, tags, botones)
/gallery/[gid]/read Página Lector de páginas (full-screen) 🔧 mover a grupo (reader)/
/tag/[tag] Página Filtro por tag
/login Página Login con password
/feed Página Admin feed (grupo (app), con sidebar)
/admin/posts Página Admin posts
/queue Página Cola del pipeline
/search Página Buscar en nhentai/ehentai
/submit Página Enviar URL al pipeline

API endpoints (propios de la web)

Ruta Método Función Status
/api/auth/login POST Login con WEB_PASSWORD
/api/auth/logout POST Logout
/api/cover/[gid] GET Portada (caché local → proxy → 404) + fallback mount en plan
/api/pages/[gid] GET Nuevo — conteo de páginas extraídas 🔧 plan
/api/pages/[gid]/[n] GET Nuevo — stream de página N 🔧 plan
/api/download/[gid]/mobi GET Nuevo — descarga MOBI guardado 🔧 plan
/api/cron/poll GET Trigger manual del poller
/api/posts GET Listar posts (público)
/api/posts/[id] GET/PUT/DELETE CRUD posts admin
/api/posts/[id]/publish POST Publicar/despublicar
/api/proxy/[...path] ALL Proxy inverso al pipeline API

Pipeline API endpoints (consumidos vía proxy)

Los que funcionan y la web ya usa o va a usar:

Ruta Status Uso
GET /api/v1/health 200 Health check
GET /api/v1/status 200 Estado del pipeline
GET /api/v1/galleries?status= 200 Listar galerías (SOLO 6 trackeadas)
GET /api/v1/galleries/{gid}/summary 200 Metadata de galería (funciona para cualquier gid)
GET /api/v1/galleries/{gid}/artifacts 200 Archivos generados (paths, no bytes)
GET /api/v1/galleries/{gid}/cover 200/404 Página 1 (portada)
GET /api/v1/galleries/{gid}/download/cbz 200 CBZ traducido — 43 galerías (nuevo)
GET /api/v1/galleries/{gid}/download/mobi 200/404 MOBI descargable
GET /api/v1/galleries POST Enviar URL para procesar
POST /api/v1/search 200 Buscar en nhentai/ehentai
POST /api/v1/search/process 200 Buscar + auto-encolar

Galerías — estado real

Métrica Valor
Galerías en output/artifacts/ 69
Con rendered/ (traducidas) 44
Con CBZ traducido (post-backfill) 43 únicas / 46 archivos
3 con CBZ duplicado (named + translated.cbz) 666886, 666887, 666888
1 con rendered pero sin CBZ 615512 (4 páginas)
API lista (via /galleries) 6 (5 completed + 1 failed)
Web tiene como posts 5
Invisibles para la web ~38

Bugs conocidos (web)

1. Shape mismatch poller → no crea posts (FIXED 2026-07-23)

Causa: La API devuelve ApiResponse<PaginatedResponse<>> (doble data), pero poller.ts:28 accedía res.data esperando un array. res.data era el objeto {data:[], meta:{}}. for...of lanzaba TypeError, el catch{} vacío lo tragaba silenciosamente.

Fix: galleries = res.data?.data || res.data || []

Archivos: src/lib/poller.ts:28, src/lib/api.ts:55-59,126-130 (tipos)

2. Portada 404 para galería 665602

Causa: La galería 665602 ("Inakax [Spanish]") es Spanish (skip translate). El endpoint /cover del pipeline 404a porque no tiene images/ ni originals/ — solo rendered/. El cover route no tiene fallback.

Fix pendiente: Fallback a rendered/001.jpg del CBZ extraído (o mount). Se resuelve cuando el poller extraiga las páginas del CBZ y la cover pueda servirse de pages/001.jpg.

3. Reader repite portada N veces

Causa: (app)/gallery/[gid]/read/page.tsx no tiene acceso a páginas renderizadas porque el API no las sirve individualmente. El código genera N urls a /cover — todas página 1.

Fix plan: Poller descarga CBZ → JSZip extrae → guarda en local → reader sirve desde /api/pages/[gid]/N.

4. Reader está en grupo (app) con sidebar admin

Problema: Hereda el layout del admin (sidebar). No debería. Y el middleware no tiene /gallery como ruta pública (aunque sin WEB_PASSWORD pasa igual).

Fix: Mover a grupo (reader)/ con layout bare + agregar /gallery y /api/pages a publicPaths en middleware.

5. Botón "Archivos" en detalle

Problema: Linkea a /api/proxy/galleries/{gid}/artifacts que devuelve JSON (paths, no archivos). Inútil.

Fix: Eliminar botón (líneas 88-96 de src/app/(public)/p/[slug]/page.tsx).

Problema: post.source se muestra como span estático.

Fix: Si source==="nhentai"<a href="https://nhentai.net/g/{gid}/" target=_blank> con icono externo.

Problema: Cualquiera ve "Admin" en el header.

Fix: Eliminar link de src/app/(public)/layout.tsx.


Dependencias bloqueantes

🚫 BLOQUEANTE: Listado de galerías traducidas

La API solo lista 6 galerías en /galleries (las del UrlQueue activo). Hay ~38 galerías traducidas con CBZ que la web no puede descubrir. El poller descubre galerías vía listado — sin los 43 gids, el poller nunca las importa.

Solución del lado API: Que /galleries escanee output/artifacts/ (o un glob de CBZs) y devuelva todos los gids traducidos. Opción B: endpoint GET /galleries/translated.

Impacto en la web: El poller cambia su fuente de ?status=completed → listado completo. Nada más.

RESUELTO: CBZ traducido

El endpoint download/cbz ahora sirve el CBZ con las páginas renderizadas. La web puede descargarlo con JSZip y extraer las páginas. Sin esto no hay reader posible.

Nota: 666886, 666887, 666888 tienen translated.cbz duplicado además del nombrado. El endpoint debe deduplicar o asegurar consistencia.


Plan de implementación pendiente (la web)

Paso 1: DB schema — añadir columnas de storage

ALTER TABLE posts ADD COLUMN pages_count INTEGER DEFAULT 0;
ALTER TABLE posts ADD COLUMN mobi_stored INTEGER DEFAULT 0;
ALTER TABLE posts ADD COLUMN cbz_path TEXT;

Paso 2: src/lib/gallery-store.ts (nuevo)

  • downloadAndExtractCbz(gid): fetch proxy → JSZip → guardar en /app/data/galleries/{gid}/pages/
  • downloadMobi(gid): fetch proxy → guardar MOBI
  • getPagePath(gid, n): resolver path de página local
  • getPageCount(gid): contar páginas locales
  • getCoverFromPages(gid): primera página como cover fallback

Paso 3: Poller reescrito

  • Descubrimiento via listado completo (depende de API fix)
  • Por cada nuevo gid:
    1. downloadAndExtractCbz — si 404 → SKIP (no traducida)
    2. Cachear cover desde página 1 extraída
    3. downloadMobi — best effort
    4. Crear post con pages_count, mobi_stored

Paso 4: Endpoints nuevos

  • GET /api/pages/[gid]{count, available}
  • GET /api/pages/[gid]/[n] → stream imagen N
  • GET /api/download/[gid]/mobi → stream MOBI

Paso 5: Reader rewrite

  • Mover a (reader)/gallery/[gid]/read con layout bare
  • Fetch páginas de /api/pages/[gid]/N

Paso 6: UI fixes (5 items)

  • Sacar "Archivos", agregar "Descargar MOBI"
  • Link nhentai externo
  • "Leer" solo si pages_count > 0
  • Sacar "Admin" del header
  • Middleware: abrir /gallery, /api/pages, /api/download

Paso 7: Cover fallback

  • Si proxy 404a y hay páginas locales → servir pages/001.jpg

Container

# Docker run actual (sin mount)
docker run -d \
  --name worst-scan-web \
  --network caddy \
  --add-host host.docker.internal:host-gateway \
  -v worst-scan-web-data:/app/data \
  -e API_BASE_URL=http://host.docker.internal:8080/api/v1 \
  -e WEB_PASSWORD= \
  -e PORT=3000 \
  -e DB_PATH=/app/data/worst-scan.db \
  -e COVERS_DIR=/app/data/covers \
  worst_web-web:latest

# Rebuild
docker compose build
docker stop worst-scan-web && docker rm worst-scan-web
# luego el docker run de arriba

Variables de entorno

Variable Default Descripción
API_BASE_URL http://127.0.0.1:8080/api/v1 URL de la API del pipeline
API_KEY "" API key (vacío = sin auth)
WEB_PASSWORD "" Password admin (vacío = sin auth)
PORT 3000 Puerto del servidor
DB_PATH data/worst-scan.db Ruta a SQLite
COVERS_DIR data/covers Directorio de portadas cacheadas
ARTIFACTS_DIR data/galleries Directorio de páginas extraídas (futuro)

Estructura de archivos relevante

src/
├── app/
│   ├── (app)/                    # Admin group (sidebar)
│   │   ├── admin/posts/          # Admin posts CRUD
│   │   ├── feed/                 # Admin feed page
│   │   ├── gallery/[gid]/read/   # READER (mover a (reader)/)
│   │   ├── queue/                # Queue management
│   │   ├── search/               # Search nhentai/ehentai
│   │   ├── submit/               # Submit URL form
│   │   └── layout.tsx            # Sidebar layout
│   ├── (public)/                 # Public group (header+footer)
│   │   ├── p/[slug]/             # Post detail page
│   │   ├── tag/[tag]/            # Tag filter page
│   │   ├── layout.tsx            # Public header + footer
│   │   └── page.tsx              # Home page (grid)
│   ├── api/
│   │   ├── auth/                 # Login/logout
│   │   ├── cover/[gid]/          # Cover serve
│   │   ├── cron/poll/            # Poller trigger
│   │   ├── pages/                # (FUTURO) page serve
│   │   ├── posts/                # Posts CRUD
│   │   └── proxy/[...path]/      # Proxy inverso al pipeline
│   ├── favicon.ico
│   ├── globals.css
│   └── layout.tsx                # Root layout
├── components/
│   ├── cover-image.tsx
│   ├── empty-state.tsx
│   ├── filter-bar.tsx
│   ├── gallery-card.tsx
│   ├── gallery-grid.tsx
│   ├── layout/sidebar.tsx
│   ├── post-card.tsx
│   ├── search-form.tsx
│   ├── submit-form.tsx
│   └── tag-badge.tsx
├── lib/
│   ├── api.ts                # API client (fetch wrapper)
│   ├── auth.ts               # JWT session
│   ├── cbz.ts                # JSZip CBZ loader (LISTO, esperando uso)
│   ├── cover-cache.ts        # Cover caching
│   ├── db.ts                 # SQLite schema + queries
│   ├── poller.ts             # Poller (BUG FIXED, necesita rewrite)
│   ├── slug.ts               # URL slug helper
│   ├── types.ts              # TypeScript interfaces
│   └── utils.ts              # formatDate, etc
├── hooks/
├── instrumentation.ts        # Poller startup
└── middleware.ts              # Auth middleware

Comandos útiles

# Logs del container
docker logs -f worst-scan-web

# Rebuild + redeploy
docker compose build && docker stop worst-scan-web && docker rm worst-scan-web
docker run -d --name worst-scan-web --network caddy \
  --add-host host.docker.internal:host-gateway \
  -v worst-scan-web-data:/app/data \
  -e API_BASE_URL=http://host.docker.internal:8080/api/v1 \
  worst_web-web:latest

# Verificar DB interna
docker exec worst-scan-web sh -c 'node -e "const D=require(\"better-sqlite3\"); const db=new D(process.env.DB_PATH); console.log(db.prepare(\"SELECT COUNT(*) FROM posts\").get())"'

# Curl a la API del pipeline desde host
curl -s http://127.0.0.1:8080/api/v1/health
curl -s http://127.0.0.1:8080/api/v1/galleries/666408/download/cbz -o /tmp/test.cbz

# Recargar Caddy
docker exec caddy-ingress caddy reload --config /etc/caddy/Caddyfile

# Forzar ciclo de poller
curl -s https://manga.cbcren.online/api/cron/poll