333 Commits

Author SHA1 Message Date
Inna a73fe4d8d5 Foro: barra de acciones por publicación (me gusta, enlace, denuncia, marcador)
- 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>
2026-07-17 08:30:57 +00:00
Inna aaf106ef55 Foro: selector de personaje también al crear tema
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 08:17:15 +00:00
Inna 0f6e0d514f Foro: selector para fijar el personaje del autor
- 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>
2026-07-17 08:14:54 +00:00
Inna bf9931e042 Foro: usar el personaje de más nivel de la cuenta (en vez de aleatorio)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 07:59:06 +00:00
Inna 24bd28fcca Foro: autor con personaje aleatorio de la cuenta
- 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>
2026-07-17 07:57:41 +00:00
Inna beba7301a1 Armería: PvP campos de batalla, logros por categoría, bandas/mazmorras y página de hermandad
- 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>
2026-07-17 01:18:30 +00:00
Inna 476e5dba87 sql: consolidar en web-next/sql y tirar lo del foro viejo
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.
2026-07-16 21:29:12 +00:00
Inna 9caa7b6c92 sql: quitar el prefijo home_ de los ficheros que quedaron atrás
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.
2026-07-16 21:23:29 +00:00
Inna 011cae27fb Armería: SQL de las tablas que necesita (django_wow)
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.
2026-07-16 21:19:24 +00:00
Inna c0f53deede Armería: versionar .bak-armory (copias previas al rediseño)
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.
2026-07-16 21:10:48 +00:00
Inna 4dfe86ef5f Armería: versionar la caché del visor de modelos (wmmv-cache)
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.
2026-07-16 21:09:48 +00:00
Inna 3af7f5e804 Armería: muñeco de papel, talentos, actividad y caché del visor 3D
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.
2026-07-16 21:07:39 +00:00
Inna 65f6776969 Armería: búsqueda insensible a mayúsculas, autocompletar y proxy del visor 3D
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>
2026-07-16 14:41:06 +00:00
Inna 297257b753 Armería/Armory: buscador, ficha de personaje con equipo y visor 3D
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>
2026-07-16 14:29:02 +00:00
Inna 5bcf71a37b Foro: enlaces de wowhead en el idioma del lector
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>
2026-07-16 14:08:08 +00:00
Inna e65e102244 Foro: interfaz de TinyMCE en el idioma de la web
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>
2026-07-16 13:58:01 +00:00
Inna 65a97aab2d Foro: vista previa bajo el editor (se ve como el post publicado)
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>
2026-07-16 13:47:47 +00:00
Inna 6856a3736d Foro: no ejecutar wowhead dentro del editor (ensuciaba el post)
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>
2026-07-16 13:41:03 +00:00
Inna 957a9f57b1 Foro: hornear también el icono del ítem de wowhead
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>
2026-07-16 13:31:06 +00:00
Inna 799341a1a4 Foro: ítems de wowhead sin parpadeo y con color de calidad correcto
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>
2026-07-16 13:26:09 +00:00
Inna c4961879df Foro: color de calidad en los ítems y vista previa en el editor
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>
2026-07-16 13:18:13 +00:00
Inna b04a8f897d Foro: reprocesar los enlaces de wowhead tras montar los posts
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>
2026-07-16 13:10:58 +00:00
Inna a8d329ad79 Foro: enlaces de wowhead con nombre/calidad/icono automáticos
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>
2026-07-16 13:00:22 +00:00
Inna 6f0c39025f Foro: botón «Wowhead» en la barra de TinyMCE
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>
2026-07-16 12:57:16 +00:00
Inna 56f39ef0b4 Foro: permitir enlaces de wowhead en los posts (tooltip, color, icono)
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>
2026-07-16 12:52:40 +00:00
Inna 3a0b9dd5ee Foro: nombres de categorías y foros según el locale (ES/EN)
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>
2026-07-16 12:42:51 +00:00
Inna 18648c6a7e Foro: avatares por clase del personaje principal
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>
2026-07-16 12:38:21 +00:00
Inna 9b44af9675 Foro: botones con el estilo del tema y sin descuadres
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>
2026-07-16 12:31:47 +00:00
Inna af707ad98c Foro: reemplazar el foro anterior por el port del foro estilo FusionCMS
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>
2026-07-16 12:22:08 +00:00
Inna 0f98b754d4 Añadir ads.txt con el ID de editor de AdSense
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>
2026-07-16 11:17:21 +00:00
Inna 67656fa32d Anuncios de Monetag, apagados por defecto y detrás del consentimiento
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>
2026-07-16 11:15:26 +00:00
Inna 972159229a Guía de Mac: CrossOver oficial en vez de una web de software pirateado
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>
2026-07-16 07:11:52 +00:00
Inna 3361f2e9e9 Etiqueta de verificación de AdSense, con interruptor y sin anuncios para quien paga
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>
2026-07-16 06:57:28 +00:00
Inna 94f9731b2c Google Analytics 4, con interruptor y respetando el consentimiento
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>
2026-07-15 19:49:00 +00:00
Inna fbc17f2cf8 Avisar al correo antiguo cuando el cambio de correo se completa
`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>
2026-07-15 19:32:10 +00:00
Inna 4303e9fc88 Los correos de cambio de correo saludan con la cuenta, no con «15#1»
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>
2026-07-15 19:26:46 +00:00
Inna 4a7a970206 Quitar la restricción de Gmail de toda la web
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>
2026-07-15 19:20:42 +00:00
Inna 4ee7d78994 El enlace de restablecer deja de servir una vez usado
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>
2026-07-15 19:13:06 +00:00
Inna fea1316f3c Recuperar cuenta: siempre por correo, y sin exigir Gmail
- 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>
2026-07-15 19:05:26 +00:00
Inna 79520a57ff El login exige un correo, no un nombre de cuenta
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>
2026-07-15 18:57:25 +00:00
Inna c255724ebf Aceptar el correo en MAYÚSCULAS y mostrar las cuentas como WOW1, no 17#1
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>
2026-07-15 18:53:09 +00:00
Inna edf1feedf6 El correo sale en MAYÚSCULAS en los correos de token y activación
- 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>
2026-07-15 18:40:33 +00:00
Inna 653f7f0a07 El botón de solicitar token deja de quedarse bloqueado en «Token enviado»
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>
2026-07-15 18:29:24 +00:00
Inna 61ca63a55f Arreglar cierre de sesión solo: <Link> prefetcheaba /log-out
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>
2026-07-15 18:27:02 +00:00
Inna c46cea2807 Log-in y crear cuenta: avisar si ya hay sesión en vez de ofrecer el formulario
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>
2026-07-15 18:18:23 +00:00
Inna d46cea9631 Textos del token de seguridad: los del original
- É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>
2026-07-15 18:10:08 +00:00
Inna 4f8f101ba8 Impedir cuentas Battle.net duplicadas al activar; citar el límite real de 8
`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>
2026-07-15 18:02:54 +00:00
Inna 1285d3398b Correo de activación: saludar con la cuenta y ajustar los textos
- 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>
2026-07-15 17:57:56 +00:00
Inna a8cb2f0306 Arreglar el aviso de cuenta creada: la comilla simple escapaba el placeholder
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>
2026-07-15 17:53:50 +00:00
Inna a78417e954 Activación de cuenta: recuperar el diseño del tema y decir QUÉ cuenta se activó
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>
2026-07-15 17:43:46 +00:00