- Me gusta con contador y toggle (al volver a pulsar se retira).
- Copiar enlace (permalink al post con ancla #post-ID).
- Denunciar: modal con motivos (troleo, amenaza, spam, etc.) + detalle.
- Guardar en marcadores + popover de recordatorio (2h/mañana/3 días/más)
y modal "Editar marcador" con nota y recordatorio (nota + remind_at en BD).
- Tablas acore_web: forum_post_like, forum_post_bookmark (+note,+remind_at), forum_post_report.
- APIs /api/forum/{like,bookmark,report,bookmark-reminder}, gated por sesión.
- Estilo oscuro+dorado de la web.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Modal "Cambiar personaje" (diseño de la web) con filtro, avatar de raza,
nombre en color de clase y subtítulo nivel/raza/clase.
- Barra "Publicas como: X · Cambiar personaje" sobre el formulario de respuesta
(solo con sesión iniciada).
- Preferencia persistente en acore_web.forum_character (por cuenta), validada.
- resolvePosters usa el personaje fijado si existe; si no, el de más nivel.
- API /api/forum/character (GET lista + actual, POST fijar), gated por sesión.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- El nombre del autor muestra un personaje ALEATORIO de la cuenta (no la id).
- Nombre con <Hermandad> en dorado (oculto si el personaje no tiene guild).
- Avatar = imagen de raza del personaje (raza+género).
- Cajas nuevas: puntos de logros, nivel y clase (con su color de clase).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PvP: pestaña Campos de batalla (pvpstats) con resultado, stats por partida y fecha/hora.
- Logros: rejilla por categoría con anillos de progreso + detalle por categoría
(datos de Achievement.db2 + Achievement_Category.db2, iconos vía wowhead).
- Bandas: progreso por dificultad (10/25/heroico) con jefes reales de DungeonEncounter.db2
y tooltip de jefes; agrupado por expansión (Clásico/TBC/WotLK).
- Mazmorras: mismo sistema, Normal + Heroico, Clásico/TBC/WotLK.
- Página de hermandad estilo armería: cabecera (emblema, puntos = unión de logros de
miembros sin duplicar, nº miembros, fundada), pestañas Hermandad/Logros, roster con
iconos de clase/raza, columna Función (rol por spec), filtros clase/función,
orden por rango/puntos/nivel de objeto/nivel/nombre, paginación.
- Barra lateral: mensaje diario con login (iron-session, gated por pertenencia),
logros recientes y desgloses de clases y funciones.
- Cabecera de personaje: enlace a la hermandad + nombre del reino.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repaso de la carpeta sql/ heredada. De sus 8 ficheros:
- forum_schema.sql y forum_views.sql: SE BORRAN. Son del foro anterior, que
af707ad reemplazó por el port de FusionCMS (web-next/sql/forum_fusion.sql).
El primero crea `forums`, tabla que ya no existe (el código usa forum_forums);
el segundo añade un contador `views` que ni existe en prod ni usa el código.
Quedan en el historial por si hicieran falta.
- achievement_points.sql, battlepay_sumup.sql, seed_news.sql y
seed_recruit_rewards.sql: siguen siendo válidos y se mueven a web-next/sql/,
que es la carpeta viva. El primero pasa a achievement_points_world.sql para no
chocar con la copia de django_wow que usa la armería; cada uno avisa del otro.
- rename_home_prefix.sql y drop_home_securitytoken_authuser_fk.sql: se quedan en
sql/ como registro de migraciones ya aplicadas.
Se añade battlepay_purchases.sql: la tabla la escribe el core y no estaba en
ningún SQL, así que no había forma de recrearla. Sus 61 filas son de pruebas
locales (2 cuentas, 127.0.0.1), sin datos de jugadores.
Verificado importando todo en BD limpias: seed_news deja 50 noticias,
seed_recruit_rewards 6, battlepay_sumup crea sus 2 tablas y
battlepay_purchases reproduce prod fila a fila con el mismo esquema.
El renombrado de las 41 tablas (cccc703) no llegó a los ficheros SQL ni a los
generadores, así que seguían creando y consultando tablas home_* que ya no
existen: aplicarlos hoy no reproducía el portal.
- 12 ficheros pasan a las tablas reales (store_item, stripelog, noticia, ...).
- Los 6 que llevaban el prefijo en el NOMBRE se renombran también; nada los
referenciaba.
- gen_store_catalog.py leía FROM home_store_item de django_wow (roto) y
db2_to_sql.py generaba home_item_data; corregidos, con sus comentarios.
- add_stripelog_service.sql usaba ADD COLUMN IF NOT EXISTS, que es de MariaDB:
en MySQL 9.7 daba error de sintaxis y no se podía aplicar (la columna se había
puesto a mano en prod). Ahora es un ADD COLUMN normal, como su hermano.
Se dejan intactos sql/rename_home_prefix.sql y
sql/drop_home_securitytoken_authuser_fk.sql: son migraciones ya aplicadas y los
nombres home_* son parte de su historia.
Verificado importando todo en BD limpias: los ALTER dejan stripelog y noticia
con el esquema idéntico a producción, y la consulta de gen_store_catalog.py
devuelve filas.
Las tres tablas propias que consulta la armería (el resto son de AzerothCore):
- achievement_names: no tenía SQL en el repo. Se añade el volcado (1912 filas).
- achievement_points: solo existía sql/achievement_points.sql, que crea la tabla
en acore_world para el top de /<realm>-players. La armería la lee de django_wow,
así que se añade el volcado de esa copia.
- item_data: el fichero (y su generador) seguían creando `home_item_data`, nombre
que dejó de existir al quitar el prefijo home_ en cccc703; se renombra a
`item_data`, que es lo que consulta el código.
Verificado importando los tres en una BD limpia: coinciden fila a fila con
producción, con las tildes y los apóstrofos correctos.
Copias de los ficheros de la armería tal y como estaban antes del rediseño
(paperdoll, talentos y actividad), por si hace falta consultarlas o volver atrás.
Se sube la caché en disco (live/, icons/, spellicons/, achievements/) para no
depender de que wowhead/zamimg siga sirviendo los assets: aunque el código la
repuebla bajo demanda, lo ya descargado queda respaldado en el repo.
La ficha de personaje pasa a mostrar el equipo sobre el paperdoll (con
encantamientos y gemas en los tooltips de wowhead), el árbol de talentos y
la actividad reciente del personaje.
El proxy de /modelviewer/* deja de ser un rewrite de Next y pasa a un route
handler que cachea en disco los assets de zamimg, para no repetir la descarga
en cada visita.
Tres arreglos:
1) Búsqueda insensible a mayúsculas/minúsculas. Las consultas usaban LIKE directo
(sensible según la colación de la tabla), así que 'test' no encontraba 'Test'.
Ahora comparan con LOWER(col) LIKE LOWER(?).
2) Autocompletar tipo wowhead. Nuevo /api/armory/suggest + ArmorySearchBox
reescrito: al teclear (≥1 carácter, con debounce) muestra un desplegable con
personajes, ítems y hermandades que coinciden; clic va directo a la ficha/wowhead.
Enter o el botón siguen haciendo la búsqueda completa.
3) Visor 3D «no disponible». Causa: el contenido de wow.zamimg.com/modelviewer no
envía cabeceras CORS (el propio README de wow-model-viewer pide un proxy). Se
añade un rewrite en next.config (/modelviewer/* -> zamimg) para servirlo
same-origin, y el visor usa esas rutas. Además, si falla la resolución de ítems
se renderiza el personaje sin equipo, y los errores se registran en consola para
diagnóstico.
Verificado: 'test' encuentra 'Test'; el autocompletar responde; el proxy
/modelviewer/live/viewer/viewer.min.js sirve 200 same-origin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Nueva sección Armería (menú Jugadores del header, pública). Migra la idea del
módulo armory de FusionCMS a Next, ajustada al diseño del sitio y en ES/EN.
DATOS (verificado antes de construir): item_template no existe en 3.4.3, pero los
datos de ítem ya están en MySQL (django_wow.item_data, 44.873 filas con nombre
es/en, calidad, item_level, inventory_type y display_id). El equipo del personaje
sale de acore_characters (character_inventory + item_instance, con transmog) y se
resuelve contra item_data en dos pasos (sin joins entre esquemas). lib/armory.ts +
lib/wow-data.ts (razas/clases/slots ES/EN y colores de clase).
RUTAS:
- /armory: buscador con pestañas personajes/ítems/hermandades (SSR por searchParams),
ítems con tooltip/color/icono de wowhead.
- /armory/character/[guid]: identidad, equipo (tooltips wowhead, color por calidad)
y visor 3D.
- /armory/guild/[guid]: miembros con enlace a su ficha.
VISOR 3D (components/ArmoryModel3D): usa la librería wow-model-viewer (generateModels)
con jQuery + ZamModelViewer de wowhead cargados bajo demanda, alimentado con
raza+género+display_ids del equipo (transmog si lo hay). Es integración cliente que
depende del CDN de zamimg y NO se puede verificar sin navegador: va todo en try/catch
con temporizador de reserva y aviso si no carga, para no romper la ficha.
Verificado en producción (salvo el render 3D): buscador, ficha con equipo real
(«Camisa de acólito»/«Acolyte's Shirt»), tooltips de wowhead y página de hermandad,
en ES y EN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los enlaces de wowhead se guardan con el locale de quien los postea (subdominio,
domain y nombre), así que un ítem posteado en /es se veía en español aunque lo
leyeras en /en, y al revés. Ahora siguen el idioma de QUIEN LEE.
Al renderizar el tema (server component, conoce el locale del lector) se localizan
los enlaces: se reescribe el subdominio y el domain del data-wowhead y se vuelve a
resolver el nombre en el idioma del lector (lib/forum-wowhead + lib/wowhead-resolve,
compartido con la API del editor y cacheado). Es solo de presentación: no cambia lo
guardado ni añade parpadeo (el nombre llega ya resuelto del servidor).
Verificado: un post guardado con nombre inglés y domain=wotlk se lee como «Bolsa de
tejido de Escarcha» + es.wotlk en /es, y «Frostweave Bag» + www en /en.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El editor salía siempre en inglés. Ahora su interfaz (barra, menús, diálogos) usa
el locale de la web: inglés por defecto (idioma nativo de TinyMCE) y español desde
public/tinymce-langs/es.js (localización oficial de TinyMCE, versionada porque
public/tinymce se regenera en postinstall y no la incluye).
Los enlaces de wowhead del foro ya seguían el locale (subdominio del idioma en la
URL y domain=es.wotlk en data-wowhead), así que el tooltip sale en español en /es y
en inglés en /en.
Verificado: /tinymce-langs/es.js se sirve y language_url queda compilado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Se añade una vista previa DEBAJO del editor que renderiza el texto actual y se ve
igual que el post publicado: los ítems de wowhead con icono, nombre y color de
calidad. Vive en el documento principal (no en el iframe del editor), así que la
procesa el tooltips.js global sin ensuciar lo que se guarda (el editor sigue
enviando el enlace plano). Un efecto refresca los enlaces de la preview al cambiar
el texto (con debounce).
forum.css: caja .forum-preview + la regla de «sin subrayado» de wowhead se
generaliza a cualquier .post_container (post y preview).
Verificado: el CSS de la preview se sirve y el componente queda compilado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El post salía "doble y fatal" porque se ejecutaba wowhead DENTRO del iframe del
editor: wowhead mutaba el DOM editable (icono, clase icontinyl y una tabla enorme
oculta con el tooltip completo) y TinyMCE serializaba todo eso al publicar, así que
la basura se guardaba en el mensaje.
Se deja de inyectar wowhead en el editor. Ahora, igual que recruit:
- El botón inserta un enlace PLANO con el nombre real como texto (resuelto en el
servidor vía /api/forum/wowhead, sin el parpadeo de item=xxx). Nada de <img> ni
clases/estilos horneados.
- El icono (como fondo, icontinyl) y el color de calidad los añade wowhead en la
PÁGINA publicada, donde muta el DOM en vivo sin que se guarde nada.
Se revierte el <img> horneado y su CSS (.wh-icon) y el class en img del saneador.
Se conservan los colores de calidad q0..q7 en forum.css (los necesita el qN que
wowhead pone al vuelo, porque theme.css solo traía q3..q6).
Verificado: crear tema funciona y el post se guarda como enlace plano + nombre, sin
icontinyl/tabla/img.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Al hornear la clase de calidad (qN), el script de wowhead daba el enlace por
procesado y dejaba de añadir el icono, así que el icono desapareció. Ahora el icono
se hornea junto al enlace: el endpoint /api/forum/wowhead ya devuelve el nombre del
icono, y el botón inserta <img class="wh-icon" src="…zamimg…/small/<icono>.jpg">
antes del enlace.
Así el ítem queda con nombre, color de calidad e icono TODO guardado, sin parpadeo
y sin depender del script (que solo aporta ya el tooltip al pasar el ratón). El
saneador permite class en img; forum.css estiliza .wh-icon.
Verificado en producción: el post guarda la img del icono (clase wh-icon + URL de
zamimg) junto al nombre y la clase q2, y el CSS del icono se sirve.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dos problemas restantes con los enlaces de wowhead:
1) Texto blanco en vez del color de calidad. theme.css solo define q3..q6, así que
un ítem de calidad 0/1/2 (p.ej. verde=q2) se quedaba sin color y salía blanco.
Se completan .q0/.q1/.q2/.q7 en forum.css (colores estándar de WoW) con
!important para ganar al color del enlace. Además el saneador deja los enlaces
de wowhead SIN nofollow (el script de wowhead ignora los nofollow al colorear).
2) Parpadeo de «item=xxx» al recargar. Salía porque el texto del enlace era un
marcador que wowhead renombraba de forma asíncrona. Ahora el botón resuelve el
NOMBRE y la CALIDAD en el servidor (nuevo /api/forum/wowhead, que consulta el
endpoint de tooltip de wowhead en la rama WotLK y el idioma de la web) e inserta
el enlace ya con su nombre real y su clase qN: sin parpadeo y ya coloreado. Si la
API falla, se cae al modo anterior (data-wh-rename-link).
El editor añade los colores de calidad a su content_style para que el preview salga
igual. El icono lo sigue poniendo wowhead (iconizeLinks) tanto en el editor como al
postear.
Verificado en producción: el endpoint devuelve nombre (es), calidad e icono; el CSS
sirve q2; un post con nombre real + q2 se guarda sin nofollow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dos ajustes sobre los enlaces de wowhead:
1) Color en el post. forum.css teñía TODOS los enlaces del post de dorado y los
subrayaba, con más especificidad que la clase de calidad de wowhead, así que el
nombre salía dorado/azul en vez del color de calidad. Ahora la regla dorada
excluye los enlaces con data-wowhead (`a:not([data-wowhead])`) y a esos se les
quita el subrayado, para que se vean con su color de calidad como en el tooltip.
2) Vista previa en el editor. TinyMCE vive en un iframe con su propio documento, al
que no llega el tooltips.js de la página, por eso en el editor salía item=41599
en crudo. Se carga el script de wowhead dentro del iframe (con renameLinks:true)
y se refresca tras insertar, así el ítem se ve con icono, nombre y calidad ya al
redactar. No afecta a lo que se guarda: los formularios envían el HTML del estado
de React (lo insertado, limpio), no la mutación visual del iframe.
Verificado: el CSS servido excluye los enlaces de wowhead del dorado; la inyección
del script en el iframe queda compilada en el bundle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los enlaces de wowhead de un post salían en crudo (item=41599) en vez de con
nombre, color e icono. Causa: tooltips.js solo recorre el DOM al cargar, así que el
contenido que monta React (los posts, inyectados con dangerouslySetInnerHTML, y las
páginas abiertas por navegación SPA) se quedaba sin procesar.
Se añade WowheadRefresh, un componente cliente que llama a
$WowheadPower.refreshLinks() al montar (con reintentos, porque tooltips.js carga
afterInteractive) y se repite al cambiar de tema/página. Es exactamente lo que ya
hacía la tienda (StoreBrowser) tras cada AJAX. Con esto los enlaces reciben color
por calidad, icono y —al llevar data-wh-rename-link— el nombre automático.
Verificado: refreshLinks compilado en el bundle; la página del tema carga
tooltips.js y sirve el enlace con data-wh-rename-link + href /wotlk/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Al insertar un enlace de wowhead sin texto, ahora sale el NOMBRE real del ítem/
misión/etc., su color por calidad y su icono, obtenidos automáticamente de wowhead
—sin escribir nada—.
El script global va con renameLinks:false (para no pisar los nombres en español de
la tienda), así que se marca cada enlace del foro con data-wh-rename-link="true",
que sobrescribe ese ajuste solo para esos enlaces. El botón Wowhead del editor deja
el texto opcional: vacío = nombre automático; con texto = se respeta el del usuario.
El saneador conserva data-wh-rename-link y data-wh-icon-size.
Verificado: un enlace insertado sin texto se guarda con data-wh-rename-link="true"
y el href /wotlk/, listo para que tooltips.js le ponga nombre, color e icono.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
En el editor de crear/responder/editar aparece un botón «Wowhead» que abre un
diálogo (tipo: ítem/hechizo/misión/PNJ/logro/objeto, ID y texto opcional) e
inserta el enlace de wowhead ya listo: con su href a la rama /wotlk/ en el
subdominio del idioma de la web y su data-wowhead, para que salga el tooltip, el
color por calidad y el icono (lo pinta el script global tooltips.js).
Reutiliza wowheadUrl/wowheadData de lib/wowhead (mismo formato que los enlaces de
wowhead del resto del sitio). Si no se pone texto, se usa `<tipo>=<id>`. El ID se
limpia a dígitos. El HTML resultante lo sigue saneando el servidor.
Verificado: el registro del botón, los textos ES/EN del diálogo y el toolbar con
«wowhead» quedan compilados en el bundle; la página del editor responde.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Al escribir un post se puede pegar un enlace de wowhead y sale con su tooltip,
color por calidad e icono, igual que en el resto de la web (lo pinta el script
global tooltips.js del layout, que actúa sobre los enlaces a wowhead.com y sobre
data-wowhead).
El saneador de posts (lib/forum-sanitize.ts) lo impedía; ahora:
- conserva data-wowhead (en <a> y <span>) y class, que el tooltip necesita;
- normaliza los enlaces de wowhead a la rama /wotlk/ manteniendo el subdominio
(www=inglés, es=español…), que es lo que fija el idioma del tooltip. Así un
enlace pegado en retail muestra igualmente el tooltip de WotLK;
- abre esos enlaces en pestaña nueva, como los del resto del sitio.
Los enlaces que no son de wowhead se quedan igual.
Verificado en producción: un enlace retail pegado en un post se guarda como
.../wotlk/item=… y la página del tema carga tooltips.js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los nombres de categorías, foros, descripciones, clases e idiomas venían del seed
solo en inglés. Ahora se muestran en el idioma de la página.
Como es un conjunto fijo y conocido, se resuelve con un mapa de traducción en
código (lib/forum-i18n.ts, inglés→español) que se aplica al vuelo, sin tocar el
esquema (el inglés sigue siendo el valor guardado). Un texto no mapeado (p. ej. un
foro nuevo creado desde el admin) se muestra tal cual.
getForumIndex/getForum/getForumPath/getTopicPath aceptan el locale y localizan
name/description/category (nunca el título de un tema, que es contenido de usuario).
Las páginas pasan su locale.
Verificado en producción: /es/forum en español (Noticias, Caballero de la Muerte,
Foros por idioma) y /en/forum en inglés.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los avatares salían vacíos (el original usaba getAvatar, que no teníamos). Ahora
cada post muestra el avatar de FusionCMS correspondiente a la CLASE del personaje
de más nivel de la cuenta que postea, usando datos reales de acore_characters.
- Se descargan 10 avatares (uno por clase jugable en WotLK 3.4.3: 1..9 y 11) a
public/forum/avatars/class-<id>.gif.
- resolvePosters() consulta la clase del personaje top de cada cuenta y mapea a su
avatar; sin personaje (o acore_characters vacía) cae a guerrero por defecto.
- La página de tema pinta el avatar en el recuadro lateral.
Nota: la tabla characters de AzerothCore usa `guid` como clave (no `id`); ordenar
por `id` lanzaba error y devolvía siempre el avatar por defecto. Corregido.
Verificado en producción: una cuenta con Paladín de nivel máximo muestra class-2.gif.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los botones del foro usaban un dorado propio; ahora comparten el lenguaje del
<button> del tema (fondo translúcido, texto crema en mayúsculas, aclara al hover),
el mismo que sale como «Editar» en el resto de la web.
Arreglos de maquetación:
- Altura fija (42px) con inline-flex en .nice_button: los <button> heredaban
height:50px del tema y los <a> no, y en la misma fila quedaban a distinta altura
(se notaba sobre todo en «Borrar», que es un <button>).
- Se quita el height:20px del <li> de post_controls, que aplastaba los botones.
- Texto de los enlaces-botón forzado a crema: el a:link/:visited del tema
(más específico) pintaba de azul el texto de «Crear tema».
- «Editar», «Borrar» y «Desbloquear» pasan a .nice_button para que toda la fila
de acciones sea homogénea.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Se sustituye por completo el foro anterior de Next (que estaba vacío: 0 temas, 0
posts) por una adaptación del foro PHP de FusionCMS que tenía el usuario, con su
estructura, sus imágenes y su estética, llevadas a Next.
BASE DE DATOS (acore_web), esquema idéntico al original (sql/forum_fusion.sql):
forum_categories(id, order, name)
forum_forums(id, category, order, name, description, icon, colortitle, type)
forum_topics(id, forum, name, sticky, locked, deleted, created)
forum_posts(id, topic, poster, text, time, deleted)
Se hizo DROP de las tablas antiguas (forums, forum_categories, forum_topics,
forum_posts) y CREATE de estas, con el seed original (News/Reports/General,
foros con icono, clases con color, subforos por idioma con bandera). Única
diferencia con el dump: forum_topics.created, que faltaba y usan las vistas, y
el recorte del salto de línea final en los nombres de clase.
Los temas no guardan autor ni última actividad: se derivan del primer/último post,
como en el original. `poster` es el AccountID de acore_auth; el nombre se resuelve
al render (username sin el sufijo #N de Battle.net). El texto se guarda como HTML
saneado con nh3 (no BBCode) para encajar con el editor.
EDITOR: TinyMCE community self-hosted (licenseKey gpl), servido desde /tinymce
(scripts/copy-tinymce.mjs en postinstall; public/tinymce en .gitignore por ser
artefacto). El HTML se sanea SIEMPRE en el servidor.
PÁGINAS (app/[locale]/forum): índice con los 3 tipos de foro (fila normal, tarjeta
de clase tipo 1, tarjeta de idioma tipo 2), subforo, tema, crear y editar. La
lógica de forum.js (crear/responder/editar/moderar) se portó a fetch + los avisos
del sitio, sin jQuery/SweetAlert, y la moderación pasa por POST (no GET).
CSS: forum.css adaptado al tema (app/forum.css), rutas de imagen a /forum, dorado
alineado al acento del sitio (#d79602), y se aportan .nice_button/.main-wide/
.pagination que no estaban en theme.css.
ADMIN: lib/admin-forum.ts, sus rutas y AdminForumManager reescritos al esquema
nuevo (categoría, icono, color, tipo, orden; sin _en/visibility).
Se eliminan /forum/search y /forum/user (no existen en el foro nuevo) y los
componentes que quedaban huérfanos (ForumSearchBox, PostActions, TopicModBar,
NewTopicForm). i18n ES/EN completado (next-intl).
Verificado en producción: el índice renderiza en es/en con categorías, clases,
banderas e iconos; los assets y TinyMCE sirven 200; crear tema y responder guardan
las filas con el esquema correcto y las páginas las muestran (autor resuelto, HTML
saneado). Datos de prueba borrados: el foro arranca vacío.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AdSense avisaba de que no encontraba ads.txt («No se ha encontrado»), y sin él no
puede mostrar anuncios. Se añade en public/, que Next sirve tal cual en la raíz del
dominio (https://www.nightspire.gg/ads.txt), autorizando a Google a vender el
inventario del editor pub-3719103750106396.
`f08c47fec0942fa0` es el ID de certificación de Google (fijo, igual para todos).
Verificado en producción: apex y www devuelven el fichero con http 200 y
content-type text/plain.
Nota: cuando se active Monetag habrá que añadir aquí también sus líneas de ads.txt
(las da su panel), o no podrá vender su parte del inventario.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Se añade la integración del script de Monetag (MultiTag: popunder + push + in-page
push + vignette) como alternativa a AdSense para monetizar sin depender de que
Google apruebe el sitio.
Tres interruptores (ver components/Monetag.tsx):
1. `MONETAG_SRC` + `MONETAG_ZONE` en el .env (no versionadas, documentadas en el
README). Cualquiera de las dos vacía = no se carga nada. Se leen en el servidor
y se pasan como props, así que encender/apagar es tocar el .env + reiniciar,
SIN rebuild. Ahora mismo van vacías: la integración queda instalada pero inerte
hasta que se pegue la zona del panel de Monetag.
2. El filtro común de publicidad, extraído a lib/ads.ts (`shouldServeAds`): fuera
para admins y para quien sea rango 2+ o haya pagado alguna vez. Antes esta lógica
vivía dentro de AdSense.tsx; ahora la comparten la etiqueta de AdSense y el
script de Monetag desde un único sitio, para que no se puedan desincronizar.
3. El consentimiento «Marketing» del banner, en components/MonetagScript.tsx
(cliente). A diferencia de la etiqueta de AdSense, MultiTag escribe cookies y
pide permisos, así que NO puede cargarse sin aceptar la categoría. Mismo patrón
que Analytics.tsx: useConsent reacciona en caliente, arranca en false.
El push de MultiTag necesitará además un sw.js servido desde la raíz del dominio
(public/sw.js, descargado del panel). Sin él, el resto de formatos funcionan igual.
Verificado: con las variables vacías la web queda idéntica (ni script de Monetag ni
cambios) y la etiqueta de AdSense sigue intacta. Con una zona de prueba, el filtro
de servidor deja pasar a anónimos y rango 1 sin pagos, y corta a los admins. Sin
consentimiento no se inyecta ningún <script> (la zona solo viaja como prop inerte
en el payload de React).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El paso 1 de la guía de macOS enlazaba a insmac.org, un repositorio de software de
pago pirateado. Ahora apunta a la web oficial de CodeWeavers, que además tiene
prueba gratuita.
Se cambia solo el destino del enlace: el texto de macStep1 ya era neutro («Descarga
e instala CrossOver»), así que no hay que tocar es.json ni en.json.
Motivo, por orden de importancia: metíamos a nuestros propios jugadores en un sitio
de warez con el riesgo de malware que eso supone, y de paso era una infracción de
copyright en una web que quiere servir anuncios de AdSense.
Verificado en producción: el chunk servido contiene codeweavers.com/crossover y no
queda ninguna referencia a insmac en el repo ni en el bundle.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Se añade `<meta name="google-adsense-account">` al <head> de todas las páginas, que
es lo que pide Google para verificar la propiedad del sitio. OJO: esto NO muestra
anuncios ni carga adsbygoogle.js todavía; solo permite que Google confirme que el
dominio es nuestro y arranque la revisión.
TRES interruptores (ver components/AdSense.tsx):
1. `ADSENSE_CLIENT_ID` en el .env (no versionado). Vacío o sin poner = no se emite
nada. Documentada en el README. NO lleva prefijo NEXT_PUBLIC_ a propósito: la
etiqueta se pinta en el servidor, así que no hace falta exponerla al bundle ni
rebuildear para activarla/desactivarla, al revés que NEXT_PUBLIC_GA_ID.
2. Admin (ADMIN_EMAILS o gmlevel de AzerothCore): sin etiqueta, para que la
navegación del staff no cuente como tráfico con anuncios.
3. Rango de la cuenta (ns-ranks) Y pagos: solo ve anuncios el rango 1 (Newbie, el
de por defecto) que ADEMÁS nunca ha pagado.
Los dos cortes del punto 3 son distintos a propósito y no basta con mirar el rango:
el mínimo de SumUp son 0,20 €, que a 100 PD/unidad son 20 PD, y eso sigue siendo
rango 1. Mirando solo el rango, quien pagara el mínimo seguiría viendo anuncios,
que es justo lo contrario de lo que queremos.
Para no duplicar el SQL, las consultas de saldo y de total donado salen de
getAccountDashboard a dos helpers (`getPoints`, `getDonatedPD`) que reutiliza el
nuevo `getAccountRankInfo`, con las dos consultas mínimas en vez del panel entero
(personajes, baneos, correo…). El criterio de rango es el mismo que ya usaba
/my-account: el máximo entre lo donado y el saldo actual.
Verificado contra la web real: anónimo y rango 1 sin pagos SÍ reciben la etiqueta;
rango 1 con 99 PD también; 100 PD de saldo (rango 2) no; rango 1 tras pagar 0,20 €
tampoco; admin tampoco; y con la variable vacía no la recibe nadie. Los dos últimos
casos no existían en la BD, así que se probaron con datos temporales en la cuenta
14 (fila en stripelog y en api_points), ya borrados. /my-account sigue pintando el
rango correcto tras mover las consultas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Se añade GA4 (gtag.js) con el componente oficial `@next/third-parties`, que es lo
que recomienda el doc de Next 16 y carga el mismo gtag pero tras la hidratación.
DOS interruptores:
1. `NEXT_PUBLIC_GA_ID` en el .env (no versionado). Vacío o sin poner = no se carga
nada. Documentada en el README.
2. El consentimiento del visitante. El sitio YA tenía un banner con la categoría
«Analíticas» que se puede rechazar: soltar gtag en el <head> lo habría
convertido en mentira, y con visitantes en la UE, en un problema legal.
Para lo segundo se expone `useConsent(categoría)` desde CookieConsent, que además
reacciona en caliente: `writeConsent`/`revokeConsent` ya disparaban el evento
`ns:consentchange`, así que la analítica entra en cuanto se acepta y desaparece
si se revoca, sin recargar. Arranca en `false` a propósito: hasta confirmar el
consentimiento no se carga nada.
Verificado en el build: sin consentimiento no hay ni rastro de googletagmanager
en el HTML, y la condición compilada es `id && consent ? <GoogleAnalytics/> : null`.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`oldEmailNotificationHtml` estaba portada del Django original pero NO la llamaba
nadie: `confirmNewEmail` cambiaba el correo de la cuenta y al antiguo no le
llegaba nada. Importa porque al entrar se usa el correo: quien secuestrara una
sesión podía cambiarlo y el dueño se quedaba sin cuenta en silencio, sin ningún
aviso en ninguna parte.
Se envía DESPUÉS de que el cambio haya cuajado (si el UPDATE falla, no hay nada
que avisar) y sin enlaces: solo informa, y dice a quién contactar. `sendMail`
nunca lanza (devuelve false), así que un fallo de correo no puede tumbar un
cambio ya hecho.
Verificado ejecutando confirmNewEmail() contra la BD con el SMTP interceptado y
cuentas de usar y tirar: 1 correo al antiguo (antes 0), la cuenta cambia, y las
filas de prueba quedan borradas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Decían «Hola 15#1»: el username interno, que no le dice nada a nadie. Ahora
«Hola INNA@INNA.CL (WOW1)» — el correo, que es la cuenta con Battle.net, y la
cuenta de juego con su nombre visible. Si no se sabe cuál es, va el correo solo,
sin paréntesis vacío.
Afecta a las 3 plantillas del cambio de correo. En la que va al correo NUEVO se
saluda con `old_email`: el cambio aún no ha terminado, así que la cuenta se sigue
identificando con el correo viejo (hay que añadirlo al SELECT).
El original de Django también decía «Hola {{ username }}», así que esto es un
arreglo, no una copia.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Se admite cualquier dominio de correo. Quedaba en dos validaciones (registro y
cambio de correo) y en tres textos:
- Register.email «Correo electrónico de Gmail» -> «Correo electrónico»
- Register.emailRule «debe pertenecer a Gmail y con acceso al mismo» -> «debe ser
válido y tener acceso al mismo» (el correo de activación sigue siendo real, así
que se conserva el «con acceso»)
- ChangeEmail.info se cae la frase «El nuevo correo debe ser de Gmail»
`GMAIL_RE` se queda sin uso y desaparece: ahora `EMAIL_RE` es la ÚNICA validación
de correo del sitio (registro, cambio, recuperación y login), así que no pueden
volver a divergir como pasaba (recuperar exigía Gmail y dejaba fuera a 16 de las
17 cuentas existentes).
Verificado: pasan inna@inna.cl, hotmail, outlook, proton, nightspire.gg y
dominios con doble punto; siguen fuera 15#1, textos sin @, dobles arrobas y
espacios. En la /es/create-account que sirve producción ya no aparece «Gmail».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La página pintaba el formulario sin mirar el token: rellenabas la contraseña
nueva y solo al enviarla te enterabas de que el enlace ya no valía. El API sí
marcaba `used = 1` y rechazaba el reintento, pero la pantalla no lo reflejaba.
Ahora la página comprueba el token al abrirse con `isResetTokenValid()` (mismas
condiciones que `resetPassword` —existe, sin usar, dentro de la hora— pero SIN
consumirlo) y, si no vale, muestra el aviso del tema en vez del formulario.
Textos: `invalidLink` pasa a «El enlace de restablecer la contraseña es inválido»
y se añade `needHelp`, como en la página de activación.
Verificado en producción con enlaces reales: válido -> formulario; usado,
caducado, inexistente y sin token -> aviso, sin formulario. Y el ciclo entero:
restablecer con un enlace válido lo marca usado y al reabrirlo ya sale el aviso.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Las 4 opciones exigían Gmail (`GMAIL_RE`), así que las cuentas ya creadas no
podían recuperarse: de las 17 bnet que existen, 16 NO son de Gmail. Ahora solo
se valida que sea un correo (`EMAIL_RE`), en un único sitio antes del tipo.
El REGISTRO sigue exigiendo Gmail: eso no se toca.
- «Contraseña» se pedía por nombre de usuario («15#1»); ahora por correo, como
las otras tres. El campo del formulario deja de alternar texto/correo.
En el flujo de contraseña, el correo que se guarda en `passwordreset` se toma de
la BD y NO del tecleado: el verifier SRP6 de bnet se deriva del correo, así que
`resetPassword` necesita exactamente la misma forma (MAYÚSCULAS) o generaría un
verifier con el que no se podría entrar.
Quedan sin uso y se borran: `usesUsername`, la clave de error `invalidUsername` y
el texto `username`. `Recover.invalidEmail` decía «Introduce un correo de Gmail
válido» y pasa a ser genérico.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Con Battle.net se entra con el correo, pero el campo aceptaba cualquier cosa: el
<form> lleva `noValidate`, así que el navegador no comprobaba su propio
type="email", y el servidor solo miraba que no estuviera vacío. Entrar con «15#1»
llegaba hasta `authenticate` y devolvía «Correo o contraseña incorrectos», que
manda a buscar el fallo donde no está.
Se valida en el servidor (autoritativo) y en el cliente (aviso inmediato), con un
mensaje propio: «Introduce tu correo electrónico, no el nombre de la cuenta.».
`EMAIL_RE` es a propósito permisiva —solo exige una @ con algo a cada lado, sin
punto en el dominio ni restricción a Gmail—: el registro solo admite Gmail HOY,
pero de las 17 cuentas Battle.net que existen, 16 NO son de Gmail (INNA@INNA.CL,
y hasta Q@Q sin dominio). Validar de más al entrar las habría dejado fuera a
todas. Comprobado contra los 14 correos reales: 0 bloqueadas, y rechaza 15#1,
17#1 y demás nombres de cuenta.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dos fallos que se juntaban justo en «recuperar nombre de cuenta»:
- La validación de Gmail era sensible a mayúsculas (`/@gmail\.com$/` sin la `i`),
así que NOVAWOW86@GMAIL.COM se rechazaba con invalidEmail. Y desde que los
correos muestran la cuenta en MAYÚSCULAS, quien la copiara de ahí y la pegara
no podía recuperar la cuenta, ni registrarse, ni cambiar el correo: la misma
regex estaba repetida en register/change-email/recover. Ahora es una sola, con
la `i`, en lib/bnet.ts (junto a normalizeEmail). El dominio de un correo NO
distingue mayúsculas.
- Los correos enseñaban el username interno («17#1»), que no le dice nada a
nadie, en vez del nombre visible («WOW1»). Afectaba a DOS plantillas: la de
nombres de cuenta y la de la lista de tokens. La conversión existía, pero como
helper local de my-account; sube a lib/bnet.ts como `gameAccountDisplayName`
(el inverso de `makeGameAccountUsername`) y la aplican las propias plantillas,
para que ningún sitio que las use pueda olvidarse.
Verificado: la regex acepta las 3 variantes de caja y sigue rechazando no-Gmail;
los dos correos renderizados muestran «Cuenta: WOW1 / WOW2».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Token: el asunto era «Token de seguridad - NightSpire» a secas. El original
decía «Token de seguridad de la cuenta {username} - {NOMBRE_SERVIDOR}»; la
reescritura se dejó la cuenta por el camino. Ese correo sale de
`session.bnetEmail`, que ya va normalizado, así que las MAYÚSCULAS son gratis.
- Activación: asunto y cuerpo pasan a normalizar el correo, para que se vea igual
que en el del token. El DESTINATARIO va sin normalizar: es la dirección tal
cual la escribió el usuario.
El correo de activación se manda desde DOS sitios: `registerAccount` y
`recover.ts` (el reenvío del enlace, «no me llegó»). Se cambian los dos o saldría
distinto según por dónde lo pidieras. Lo delató el build: /api/auth/activate
cargaba un chunk y /api/auth/recover otro con la versión vieja.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tras un envío correcto se ponía `sent` y el botón quedaba desactivado para
siempre: no volvía a «Solicitar token» y, sobre todo, ya no podías pulsarlo para
ver el aviso de «cada 7 días», que es la respuesta que hay que ver. Había que
recargar la página para recuperarlo.
Ese bloqueo no venía del original: la versión anterior (isla React) tenía
`disabled={busy}` y nada más. Se vuelve a eso. La clave `sent` («Token enviado»)
queda sin uso y se borra de ES/EN.
Verificado en producción: dos POST seguidos responden el cooldown, en vez de que
el segundo fuera inalcanzable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regresión del commit anterior. /log-out es un GET que destruye la sesión, y yo
lo enlacé con <Link>: el router de Next lo prefetchea al entrar en pantalla, así
que la cookie se borraba sin que nadie pulsara. La página se renderizaba en el
servidor con la sesión («¡Ya estás conectado!») y al recargar ya no había sesión.
Reproducido: una petición de prefetch (RSC: 1, Next-Router-Prefetch: 1) a
/es/log-out responde `set-cookie: nightspire_session=; Max-Age=0`.
Se enlaza con <a> plano, que es justo como lo hace SiteHeader. Queda comentado
en ambos sitios para que no vuelva a colarse.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ninguna de las dos miraba la sesión, así que quien ya estaba conectado veía el
formulario de login o el de registro. Ahora, con `bnetId` en sesión (lo mismo que
da por buena la sesión en el resto del sitio), sale «¡Ya estás conectado!» y un
enlace a /log-out, como las plantillas del tema.
En crear cuenta el cuadro informativo se mantiene: sigue explicando cómo son las
cuentas aunque ya tengas sesión. Solo se sustituye el formulario.
Las dos pasan a `force-dynamic`: leen cookies y no se pueden prerenderizar.
Verificado en producción sellando una cookie de sesión real con iron-session:
con sesión salen los dos avisos y desaparece el formulario; sin sesión el
formulario sigue ahí y el aviso no se renderiza.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Éxito: «Se ha enviado el Token de seguridad al correo de la cuenta.» (decía
«el token de seguridad a tu correo»).
- Cooldown: «Sólo puedes solicitar un nuevo Token de seguridad cada 7 días.»
(decía «Solo puedes solicitar un token cada 7 días»).
El flujo ya funcionaba: tras enviarlo el botón queda desactivado («Token
enviado»), así que el aviso rojo sale al recargar y volver a pulsar dentro de
los 7 días. Comprobado que MySQL y Node van ambos en UTC sin desfase, que es de
lo que depende el cálculo de los 7 días (NOW() al insertar, Date.now() al
comparar).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`registerAccount` ya rechazaba un correo con cuenta bnet, pero `activateAccount`
insertaba SIN volver a comprobarlo, y entre registrarse y activar el correo puede
dejar de estar libre. Como `battlenet_accounts.email` no tiene índice único, la
BD tampoco lo frenaba: hay 3 filas TEST@TEST.COM de 2024 que lo demuestran.
Ahora se recomprueba antes del INSERT y se borra la activación, que ya no sirve.
Nuevo error `emailExists` (ES/EN) para no soltar un «enlace inválido» que despista.
MAX_GAME_ACCOUNTS sube a lib/bnet.ts y lo usa el correo de activación: el texto
prometía 10 cuentas mientras el código cortaba en 8, justo por estar el número
escrito a mano en los dos sitios. El límite del panel ya funcionaba (API + aviso
«Has alcanzado el máximo de cuentas»); solo se centraliza la constante.
Verificado en producción con una activación pendiente de un correo que ya tenía
bnet: devuelve emailExists, no crea la cuenta y el contador no sube.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- El <h1> decía «Bienvenido» a secas; ahora lleva la cuenta detrás, como la
plantilla original (`Bienvenido, {{ username }}`), donde `username` era el
mismo correo que sale en «Cuenta:».
- «Para empezar a usarla, actívala pulsando el botón» -> «Para poder empezar a
usar tu cuenta, deberás activarla haciendo clic en el botón ACTIVAR LA CUENTA».
- La segunda línea de «Recuerda» pasa a hablar del panel de usuario en vez de
«hasta 10 cuentas de juego».
Verificado renderizando el correo y extrayendo su texto visible.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Salía literal «La cuenta {email} ha sido creada.». En ICU MessageFormat la
comilla simple es el carácter de escape, así que `'{email}'` no son unas
comillas alrededor del placeholder: le dicen a ICU que trate `{email}` como
texto literal. Por eso la segunda línea, sin comillas, sí interpolaba.
La comilla literal en ICU se escribe doblada: `''{email}''`.
Barridos los dos ficheros de mensajes (consumiendo antes los `''` para no dar
falsos positivos): no había ningún otro caso.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La página mostraba un genérico «¡Cuenta activada!» + enlace a iniciar sesión.
Se recupera la estructura de las plantillas del tema (activation_success.html /
activation_invalid.html): bienvenida, la cuenta en `yellow-info`, y el aviso de
enlace inválido en `red-info2` con la línea de contacto.
El original dejaba un hueco: renderizaba `{{ username }}` pero la vista hacía
`render(...)` SIN contexto, así que salía «La cuenta ha sido activada». Aquí
`activateAccount` devuelve el email y sí se muestra. Va el email tal cual se
registró, no el normalizado: ese va en MAYÚSCULAS porque lo exige el SRP6 de
Battle.net y en pantalla quedaría como INNA@INNA.CL.
El nombre del servidor sale de `getRealmName()` (acore_auth.realmlist), como en
el resto de páginas, en vez de escribirlo a mano.
Verificado en producción: el caso inválido lo pinta el servidor y sale idéntico
al del tema; y activando una fila de prueba, el API devuelve {success, email} y
reutilizar el enlace o mandar un hash que no existe dan `invalidLink` (filas de
prueba borradas después).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>