diff --git a/agy.md b/agy.md new file mode 100644 index 0000000..81ce70c --- /dev/null +++ b/agy.md @@ -0,0 +1,348 @@ +# 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>` (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`). + +### 6. Fuente nhentai es texto, no link + +**Problema:** `post.source` se muestra como span estático. + +**Fix:** Si `source==="nhentai"` → `` con icono externo. + +### 7. Admin link en header público + +**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 +```sql +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 + +```ini +# 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 + +```bash +# 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 +``` diff --git a/src/lib/api.ts b/src/lib/api.ts index 13b2d2c..555925e 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -55,7 +55,7 @@ export const api = { list: (status?: string, page = 1, perPage = 20) => { const params = new URLSearchParams({ page: String(page), per_page: String(perPage) }) if (status) params.set("status", status) - return fetchApi>(`/galleries?${params}`) + return fetchApi>>(`/galleries?${params}`) }, get: (gid: string) => @@ -126,7 +126,7 @@ export const api = { list: (status?: string, page = 1, perPage = 20) => { const params = new URLSearchParams({ page: String(page), per_page: String(perPage) }) if (status) params.set("status", status) - return fetchApi>(`/queue?${params}`) + return fetchApi>>(`/queue?${params}`) }, stats: () => diff --git a/src/lib/poller.ts b/src/lib/poller.ts index 82c38a7..82dd747 100644 --- a/src/lib/poller.ts +++ b/src/lib/poller.ts @@ -25,7 +25,7 @@ export async function pollOnce(): Promise<{ newPosts: number }> { try { const res = await api.galleries.list("completed", 1, 100) - const galleries = res.data || [] + const galleries = res.data?.data || res.data || [] for (const g of galleries) { try {