fase-2-release-tracking #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fase-2-release-tracking"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Export al vuelo del perfil + biblioteca + cursor de lectura del user logueado. Incluye source_url y notes (campos privados del dueño, ocultos en /u/{id}). format_version para que herramientas downstream detecten shape. Link a la descarga en /settings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Flow: · POST /settings/delete-account/request manda código 6-dígitos (tabla deletion_codes, vence 15 min, single-use). · POST /settings/delete-account/confirm valida y marca users.deleted_at + destruye sesiones del user. · El middleware pendingDeletionRedirect manda a /restore-account a cualquier request autenticada con deleted_at != NULL (paths exentos: logout, static, unsubscribe). · /restore-account permite cancelar (deleted_at = NULL) o forzar hard delete inmediato. · CleanupWorker (tick 24h) borra hard a los 7 días, inserta hash one-way en deletion_log (DELETION_LOG_PEPPER env var) y limpia deletion_codes viejos. Hard delete usa transacción + CASCADE para library_entries, sessions, deletion_codes; otp_codes se limpia explícito (sin FK). Migration 0006 agrega deletion_codes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Snapshot del repo refleja la fase 2 completa: nuevos paquetes (admin, mangadex, releases/{handler,matcher,worker,digest}, unsubscribe), env vars requeridas en prod (UNSUBSCRIBE_SECRET, DELETION_LOG_PEPPER) y optional (ADMIN_TOKEN), capacidades operativas nuevas (workers en proceso, multipart emails, GDPR compliance). Backlog reordenado: lo postergado de fase 2 (widget en home, rate limit en /unsubscribe, tests en CD) queda arriba. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Cuando el user hace triple-click en el código formateado del email (ej. "123 456"), copia con espacios + whitespace al borde y a veces non-breaking spaces (U+00A0) que Gmail/Apple Mail inyectan. El maxlength del input cortaba el paste antes de llegar al server. Solución sin JS: · server-side sanitiza con sanitizeCode/sanitizeDeletionCode que extrae solo dígitos del input (cubre NBSP, tabs, separadores varios) · quitar maxlength del input en verify.html y delete-account.html para que el browser no trunque el paste — el server filtra después Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Header global con "mokuji" → / y, si hay sesión, links a biblioteca, releases y settings. Resuelve el "no hay cómo volver al home" + "no hay botón a /releases". Para páginas anónimas (/auth) muestra solo el logo porque .Session es nil; los structs startData/verifyData ahora incluyen el campo Session *session.Session (siempre nil ahí) para que el template no falle al evaluar {{if .Session}}. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Cada entry ahora muestra un badge según m.mangadex_status: · matched + N caps nuevos → "N nuevos" (link a /releases) · matched + 0 nuevos → "al día" · needs_review → "matching incierto" · no_results → "sin source" · unmatched (no procesado) → "buscando…" UnreadCount viene de un subselect correlated sobre chapter filtrando por preferred_language del user y respetando el cursor (last_read_chapter_id). Sin indexes adicionales — idx_chapter_manga_published ya cubre. Quito los links a /settings y /u/{id} del footer de library porque ahora están en el nav del header. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>GET /library/entry/{id}: header con cover + status + counters + lista de caps cronológica. · sort=desc (default) | asc — toggle "más nuevos / más viejos primero" · include_read=1 — checkbox para mostrar caps leídos (default oculta) · cada cap tiene dos acciones: "leer →" POST /library/entry/{id}/read → marca leído + 303 al external_url del cap; form con target="_blank" para abrir el lector en pestaña nueva. "marcar" POST /library/entry/{id}/mark-read → marca + vuelve. Autorización en cascada: loadEntry filtra por user_id (404 si no es del user logueado); markChapterRead exige que chapter.manga_id == entry.manga_id en el mismo UPDATE (rows-affected=0 si no matchea). El badge "N nuevos" / "al día" de /library ahora linkea a esta detail en vez de al feed global. El nav /releases sigue como dashboard cronológico cross-manga. mangadex-badge se movió a library/_partials.html para reusarlo desde list.html y entry.html sin parsearlo dos veces. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>El badge "buscando…" / "needs_review" / "no_results" no eran clickables, único camino al detail era el badge "matched" — si el manga no estaba matcheado, no había cómo entrar. Solución: · el <strong>title</strong> del manga ahora está envuelto en un <a> al detail (browsers manejan correctamente <a> dentro de <summary>: click en el link navega, no toggle el details). · todos los estados del badge son ahora links al detail, no sólo los "matched". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Dos problemas reportados por el user al agregar mangas nuevos: 1. Los caps tardaban hasta una hora en aparecer porque el worker tickea cada 1h y el manga recién matcheado tenía que esperar al siguiente tick para hacer el primer FetchFeed. 2. El badge mostraba "al día" incluso cuando no había ningún capítulo en la DB para ese manga, porque UnreadCount=0 cubría tanto "todo leído" como "todavía no halamos nada". Fix 1 — Poller sincrónico: · worker.PollOne(mangaID, mangadexID) ahora es público · library.Config tiene un Poller opcional (interface mínima) · tryMatchManga, después de updateMangadexMatch exitoso con status matched, dispara PollOne sincrónicamente. Tarda ~1-2s extra al POST /library/add pero el manga ya tiene caps al volver a /library. · errores del poller (rate limit, network) se loguean como warn; el match persiste y el worker periódico retoma porque next_check_at queda NULL hasta que el poll termine. · main.go construye el worker antes que el library handler para poder pasarlo como Poller. Fix 2 — Badge "pendiente": · Entry tiene un TotalChapters nuevo (caps en idioma preferido del user) además de UnreadCount. · partials.html distingue: matched + total=0 → "pendiente" (gris) matched + unread>0 → "N nuevos" (azul) matched + total>0, unread=0 → "al día" (verde) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>El form tenía target="_blank" y POST a /library/entry/{id}/read; el server respondía 303 + Location a la URL externa del cap (mangadex.org/...). Resultado en el browser: pestaña nueva en blanco. Causa: el CSP del server tiene `form-action 'self'` por defensa contra forms con destino malicioso. La spec dice que la directiva aplica a TODOS los URLs del redirect chain de un form submission — incluido el destino final. El browser bloquea el 303 cross-origin silenciosamente, sin mensaje en consola visible al user. Fix: en lugar de http.Redirect, el endpoint responde 200 con HTML mínimo que tiene <meta http-equiv="refresh" content="0; url=EXTERNAL"> + fallback <a> manual. La navegación cross-origin se hace por meta refresh (no es form-action chain), así que pasa la CSP. Mismo resultado visible para el user: la nueva pestaña abre el lector. URL escapada con html/template para defensa contra externals con caracteres raros. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>includeEmptyPages=0 en /manga/{id}/feed excluía los capítulos publicados en plataformas externas (MangaPlus de Shueisha, ComiKey, etc). MangaDex solo mantiene el meta + externalUrl de esos caps — los marca como "empty pages" porque no tiene las imágenes. Para mangas oficiales con licencia internacional (One Piece, One Punch Man, Spy x Family, etc.) TODOS los caps son externos, así que el filtro los borraba todos: total: 0 returned: 0 Sin el filtro: total: 6 returned: 6 (1180, 1181, 1182, etc. con externalUrl=mangaplus) El cliente ya guarda externalUrl en chapter.external_url, y entry.html usa esa URL en el botón "leer →", así que sólo había que dejar de filtrar. Bug detectado debuggeando por qué One Piece quedaba con 0 caps después de matchear OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>El digest cambia el ReaderURL de cada cap: en vez de apuntar directo al lector externo, ahora apunta a /r/{token} en mokuji. El handler valida el token, marca leído el cap (con check de ownership por SQL join: cap → library_entries activo del user que lo firmó → user no soft-deleted), y responde con HTML meta-refresh al external_url. Resultado: un click en el email = "leí esto + abrir lector" sin pasar por la app. Componentes: · unsubscribe.Signer ya era genérico; agregamos scope ScopeReadChapter + SignReadChapter / VerifyReadChapter (payload userID:chapterID:scope: issuedAt, 30d validity). · internal/library/emailread.go monta GET /r/{token} si library.Config tiene un Signer cableado; sin signer, el endpoint no existe. · digest.Sender ya tenía signer; agregamos shortcutURL(userID, chapterID) y sobrescribimos Item.ReaderURL antes de renderear. · main.go construye el signer arriba (antes de library.New) y lo reusa para el handler /unsubscribe. Tokens son self-contained — no requieren sesión. La cadena SQL del handler asegura ownership: si alguien hace replay con un token ajeno o el user fue borrado, devuelve 410 con mensaje neutral. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>· borrar var _ = email.Message{} y el import "email" del delete.go: no eran necesarios — h.email es un campo (tipo de profile.go), el archivo delete.go no nombra el package "email" directamente. · borrar itoa local, usar strconv.Itoa (ya hay strconv import). · acortar dev unsubscribe secret a un literal honesto ("no-usar-en- produccion" sin mentir sobre longitud). · LIMIT 500 en cleanupWorker.tickOnce — defensa por si la cohorte de hard-delete se acumuló por estar caído. · borrar comentarios narrativos en main.go sobre el orden de las construcciones (el orden ya está impuesto por el código). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>El patrón r = r.WithContext(session.NewContext(context.Background(), &session.Session{UserID: uid})) aparecía 20+ veces en tests. Nuevo helper: session.WithUserID(r, uid). Migra los tests que solo necesitaban UserID (no CSRFToken u otros campos). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>· session.Load cachea deleted_at en PendingDeletion: test que verifica el comportamiento antes y después del soft-delete. · worker.PollOne (público desde el fix de poll inmediato): test que bypasea la query "due" + propaga ErrRateLimited reagendando. · listEntries calcula TotalChapters vs UnreadCount con cursor parcial: test directo de los counts (sin pasar por render del badge). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>