Commit Graph

319 Commits

Author SHA1 Message Date
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
Inna 8c85cf2dbe El enlace de activación vuelve a ser alfanumérico; avisar a qué correo se envió
Dos cosas que el port de Django cambió sin querer:

- El hash de activación se generaba con `randomBytes(16).toString('hex')`: 32
  caracteres, sí, pero solo `0-9a-f`. El original era `get_random_string(32)`,
  alfanumérico con mayúsculas y minúsculas (`?act=P4JHQDJey2jJDnEBnqUePI5O2qEFB1`).
- El aviso de cuenta creada era un genérico «Revisa tu correo», mientras que el
  original decía qué cuenta se creó y a qué dirección fue el enlace. Se recupera
  el formato de dos líneas (con id `create-response`, como el original).

El muestreo por rechazo del token de seguridad se sube a `lib/random-token.ts` y
lo comparten los dos generadores, que solo se diferencian en el alfabeto: letras
para el token que se teclea a mano, alfanumérico para el hash de la URL.

Verificado con 100.000 hashes: todos casan /^[A-Za-z0-9]{32}$/, salen los 62
caracteres y la desviación por carácter se queda en 1,3% (ruido).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:34:53 +00:00
Inna 6e4a123dde El token de seguridad son 6 letras al azar, no base64url sesgado
El token se generaba con `randomBytes(4).toString('base64url').slice(0, 6)`,
portado tal cual del Django original (`secrets.token_urlsafe(4)[:6]`), y tenía
dos taras para un código que se lee y se teclea desde un correo:

- Metía `-` y `_`.
- 4 bytes son 32 bits, pero 6 caracteres base64 codifican 36: al último solo
  le llegaban 2 bits reales, así que SIEMPRE terminaba en A, Q, g o w
  (comprobado sobre 20.000 tokens: 4 valores distintos en esa posición).

Ahora son 6 letras A-Z/a-z uniformes, con muestreo por rechazo porque 256 no es
múltiplo de 52 y `byte % 52` favorecería a las primeras letras. Verificado con
300.000 tokens: todos casan /^[A-Za-z]{6}$/, las 52 letras aparecen en la última
posición y la desviación por letra se queda en el 1,4% (ruido).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:11:22 +00:00
Inna 75e4ceed4c El token de seguridad caducado ya no vale; traducir 2 errores que faltaban
checkSecurityToken guardaba expires_at pero NO lo comprobaba: un token
caducado seguía siendo válido para regalos y servicios. Se comprueba en SQL
(`expires_at > NOW()`) y no en JS, para que los dos lados de la comparación
sean hora del servidor: expires_at se inserta como Date de JS y compararlo
contra Date.now() dependería de que Node y MySQL tuvieran la misma zona.

Además faltaban `errors.notAuthenticated` e `invalidRequest` en el namespace
Store (ES y EN): si se caducaba la sesión, send-gift decía «Ha ocurrido un
error» en vez de pedir que vuelvas a iniciar sesión — justo el mensaje que
hace pensar que el botón sigue roto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:03:39 +00:00
Inna 47b5a2a145 Mostrar los errores de send-gift: el tema los dejaba invisibles
El botón «Mostrar regalos» parecía no hacer nada: setFormError se llamaba
bien, pero el tema deja `.alert-message` con display:none esperando que la
rellene y la revele el JS antiguo (jQuery), que ya no existe. La caja se
pintaba y no se veía. Se fuerza el display desde React, como el resto de
formularios.

De paso, `.modal-content` estaba definida en globals.css Y en theme.css.
Como theme.css se importa el último (a propósito, para ganarle al preflight
de Tailwind), ganaba él a igual especificidad y el bloque de globals.css era
código muerto: el modal de la tienda salía como hoja pegada al fondo con
borde gris en vez de tarjeta centrada dorada. Se acota a
`.modal-div .modal-content` para ganar por especificidad y no por orden.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:03:30 +00:00
Inna 97ccb8a179 El CSS del tema entra por el build en vez de un <link> desde public/
Antes: <link href="/ns-themes/.../nightspire-style.css?v=2"> en el <head>. Una
peticion aparte, sin minificar y con cache invalidada a mano con ?v=2.

Ahora: app/theme.css, importado al FINAL de globals.css. Next lo compila,
minifica y le pone hash de contenido; se sirve con cache immutable y hay una
hoja de estilo menos (4 -> 3).

⚠ El @import va el ULTIMO y no es cosmético: el tema se diseñó contra los
defaults del navegador y el preflight de Tailwind los pisa (box-sizing,
alturas). Antes ganaba la cascada por cargarse el último en el <head>; ahora
gana por ser el último import. Si sube de sitio, el diseño se rompe.

Las url() pasan a ABSOLUTAS (/ns-themes/ns-ryu/...). Los assets siguen en
public/, y así el compilador no intenta resolverlos: el @font-face declara
.eot/.otf/.ttf que NO existen (solo hay .woff) y con rutas relativas el build
fallaría.

Verificado: los 6 páginas dan exactamente el mismo alto que antes (8159, 1356,
2645, 1356, 2221, 2349 px) con la misma fuente y colores, y el input del login
sigue en box-sizing:content-box — que es justo donde el preflight rompía. Ojo:
el md5 de las capturas NO sirve para comparar, porque la cabecera lleva un vídeo
y cada captura pilla un fotograma distinto.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:20:06 +00:00
Inna 9a240fe69e Borrar el JS muerto del tema (10 ficheros, 144 KB)
De los 11 css/js del tema solo se usa uno: ns-css/nightspire-style.css, que
carga layout.tsx. Los 10 .js no los pide nadie. Medido en un navegador sobre 10
rutas de producción, no deducido con grep: la web no solicita ni un solo JS del
tema.

Son los manejadores jQuery del portal Django, que se borró hoy; los sustituyeron
los componentes React.

Dos detalles que el grep no habría resuelto:
- ns-scripts.js parecía referenciado desde SiteHeader.tsx, pero la coincidencia
  era un COMENTARIO que documenta de dónde salió el toggle móvil, no una carga.
  Se ajusta el comentario, que si no citaría un fichero inexistente.
- power.js era la copia que UltimoWoW tenía de los tooltips de Wowhead, con
  g_host = 'https://wotlk.novawow.com' (dominio muerto) escrito dentro. La web
  usa el oficial de zamimg, que carga layout.tsx.

Verificado tras borrar: build limpio, 7 rutas sin errores 4xx ni de JS, el tema
se sigue aplicando y el menú móvil (lo que replicaba nwNavBar) despliega bien.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:09:33 +00:00
Inna 3bc583048b Renombrar rutas /root/NovaWoW -> /root/NightSpire en los scripts DB2
El directorio del proyecto se renombró, así que las rutas absolutas de
sql/db2/*.py y la cabecera de sql/item_data.sql apuntaban a un sitio que ya no
existe.

Fuera del repo (no versionado, se anota aquí): el directorio pasó de
/root/NovaWoW a /root/NightSpire y las unidades systemd de novawow-next /
novawow-dpoints-reconcile a nightspire-*. De paso, nightspire-next.service ya no
declara After/Wants de novawow.service, que era el Django borrado y ya no
existía.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:55:41 +00:00
Inna fa7897c195 Prefijos del tema nw- y uw- -> ns- (carpetas, ficheros, clases y rutas)
Renombrado completo de los prefijos heredados: 19 carpetas, 11 ficheros y 318
referencias en 35 ficheros de código, más las clases y las rutas url() de dentro
del CSS del tema. Se usa git mv para conservar el historial.

También fuera del código, que un sed no ve:
- BD: votesite.image_url (4 filas) apuntaba a /nw-themes/...
- Ficheros con la marca vieja en el NOMBRE: novawow-maintenance.webp ->
  nightspire-maintenance.webp (lo usa la página de mantenimiento) y
  store_novawow_response.js.

⚠ Alias en Caddy /nw-themes/* -> /ns-themes/*: los correos ENVIADOS antes del
rebranding llevan esas rutas escritas y están en las bandejas de los usuarios.
Sin el alias, sus imágenes se romperían. Verificado que las rutas viejas siguen
sirviendo 200.

Nota: ns-js/ y ns-js-handlers/ son CÓDIGO MUERTO (manejadores jQuery del portal
Django, que ya se borró). Se midió en el navegador: la web no pide ni un solo JS
del tema. Se renombran igualmente por consistencia, pero son candidatos a
borrarse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:50:43 +00:00
Inna 5362787a34 Cookies: declarar las reales de Next.js, quitar las inventadas
La página venía de ultimowow y declaraba 26 cookies que este sitio NO pone.
Empezando por PHPSESSID, que es de PHP: aquí no hay PHP. Además todo el bloque
de Cloudflare (cf_clearance, __cf_bm, _cfuvid, CF_VERIFIED_DEVICE_*,
_cf_logged_in, sparrow_id), Google Analytics (_ga, _gid, _gat), Facebook
(_fbp, _fbc, fr), Adobe (AMCV_*), Zaraz (zaraz-consent, cfz_*) y las de
preferencias del foro/visor 3D de ultimowow (comments_sort, temp_default_3dmodel).

Se comprobó en un navegador contra producción, no de memoria: las únicas cookies
son NEXT_LOCALE (idioma), ns_cookie_consent (consentimiento, 1 año) y
nightspire_session (sesión, httpOnly). Turnstile NO pone ninguna en nuestro
dominio, y el sitio NO está detrás de un proxy de Cloudflare (la IP es del
servidor y no llega cabecera cf-ray), así que ninguna cookie de Cloudflare
aplicaba. Tampoco hay Google Analytics ni píxeles: no aparecen en el código.

Terceros medidos en el navegador: challenges.cloudflare.com (Turnstile, solo en
formularios), wow.zamimg.com (iconos de Wowhead), cdnjs.cloudflare.com
(tipografías/iconos) y YouTube (solo si un creador tiene vídeo configurado).

40 claves de traducción eliminadas en ES y EN; 43 usadas = 43 definidas, sin
huérfanas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:40:28 +00:00
Inna 6d974f7773 Legal: quitar 'Error 404' del bloque de contacto
Estaba escrito a mano en el JSX de privacy-policy, refund-policy y
terms-and-conditions, no en las traducciones, por eso no cayó con el
resto. Era el nombre de la empresa heredado de ultimowow, figurando
como responsable del servicio.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:29:51 +00:00
Inna 6f52d325a6 Rebranding: sitios de voto, nota legal, prefijo uw->ns y repo renombrado
Sitios de voto: los 4 apuntaban a las fichas de UltimoWoW en los rankings con
SUS ids (Gtop100 94649, pingUsername 91402, etc.). No era cosmética: cada voto
de nuestros jugadores subía a UltimoWoW en el ranking. Se cambian nombre e ids
por un 236588 de relleno, así el enlace deja de acreditar a otro servidor. Las
fichas reales de NightSpire están por crear; entonces habrá que poner sus ids.

Nota legal: decía "Error 404 es la empresa que representa el servicio", heredado
de ultimowow. Al renombrar la marca pasaba a afirmar que una empresa ajena
representa a NightSpire, lo cual es falso. Se quita la empresa en ES y EN.

Prefijo uw- (de UltimoWoW) -> ns-: 100 identificadores en 11 ficheros (clases
CSS, ids de formulario, eventos y la cookie de consentimiento, que pasa a
ns_cookie_consent; a los usuarios les reaparecerá el aviso una vez). Se comprobó
antes que el CSS del tema no define ninguna clase uw-, así que no rompe estilos.
btn-ns-form y ns-cookie-btn-customize se usan sin estar definidas, pero ya era
así antes del cambio: eran clases muertas.

Repo de Gitea renombrado Inna/NovaWoW -> Inna/NightSpire (era la marca vieja más
visible de /changelogs, que forma la URL de cada commit). Gitea redirige la URL
antigua con 301. Se actualizan el remoto y lib/changelog.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:28:29 +00:00
Inna 579396c34a Rebranding a NightSpire en toda la web
Marca, dominios y correos: UltimoWoW/Nova WoW -> NightSpire, cualquier URL de
esos dominios -> https://www.nightspire.gg y los correos -> consultas@nightspire.gg.
98 sustituciones en 16 ficheros de textos de usuario (messages/, app/, components/).

Assets: logo largo de cabecera y vídeo sustituidos por los de NightSpire (el logo
nuevo es 214x28, igual que el viejo, así que no hace falta tocar el CSS).

Redes: apuntan a las cuentas de NightSpire, tanto en el pie de la web como en el
de los correos (que seguían enlazando a las de NovaWoW desde el porte del diseño).

Cookie de sesión novawow_session -> nightspire_session. Cierra la sesión de todos
los conectados una vez; se asume ahora, recién hecho el cutover.

Ficheros del tema renombrados: novawow-style.css -> nightspire-style.css y
novawow-main-logo-transparent.webp -> nightspire-main-logo-transparent.webp, para
que la marca vieja no quede ni en las rutas.

El foro externo (foro.ultimowow.com) pasa a ser el nuestro: <Link> a /forum, que
respeta el idioma y no abre pestaña nueva.

NO se tocan y es a propósito:
- Los comentarios que nombran AzerothCore: describen el comportamiento REAL del
  core (límite de 12 ítems por correo, convención de baneos, SOAP...). Cambiarlos
  a NightSpire los volvería falsos. Además AzerothCore no aparece en la web: las
  14 menciones son todas comentarios de código.
- El regex de brandify() sigue buscando los nombres VIEJOS: son los que hay
  escritos en las noticias de la BD.
- lib/changelog.ts REPO='Inna/NovaWoW': es la ruta real del repo en Gitea.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:15:57 +00:00
Inna cccc70338d Quitar el prefijo home_ de las 41 tablas del portal
El prefijo venía del nombre de la app Django (`home`), jubilada y borrada hoy,
así que ya no significaba nada: home_stripelog -> stripelog. La BD se sigue
llamando django_wow por historia (renombrarla es otra operación).

Comprobado antes de tocar nada: 0 colisiones con nombres existentes, 0 palabras
reservadas (contrastado contra las 260 de MySQL), sin vistas ni triggers que
dependieran de ellas. El RENAME va en una sola sentencia porque así es atómico,
y MySQL reapunta solo las 4 claves ajenas entre estas tablas.

170 referencias actualizadas en 37 ficheros. Se dejó a propósito
sql/drop_home_securitytoken_authuser_fk.sql sin tocar: es una migración ya
aplicada y reescribirla sería falsear el historial.

De paso salió un fallo previo: prices.ts consultaba `home_restoreitemprice`,
una tabla que NO existe. El try/catch de priceFrom se comía el error y devolvía
siempre el precio por defecto, así que el precio de restaurar objetos nunca fue
configurable. Se deja documentado en el código; crear la tabla es otra decisión.

Verificado tras aplicar: 0 tablas home_, las 51 siguen ahí, y el conteo exacto
de filas cuadra con el volcado previo (store_item 3068, item_data 44873,
stripelog 35, store_order 26, noticia 50). La web responde 200 en todas las
rutas y la portada carga las noticias sin errores.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:49:19 +00:00
Inna 5fd006aca9 Jubilar Django: borrar el portal antiguo, la web ya la sirve Next
El cutover se hizo hoy: Caddy manda www.nightspire.gg a :3001 (Next), así que
Django se quedó sin dominio y sin uso. Se comprobó antes de borrar que nada
dependía de él: ninguna referencia a :8001 en Caddy, el timer de reconciliación
llama a Next, DJANGO_API_BASE no lo leía ni una línea del código (se quita
también de la unidad de systemd), web-next/public es autónomo (736 ficheros
reales, 0 symlinks) y no hay ni una referencia a /static/. Con Django parado la
web siguió dando 200 en todas las rutas.

Se va: home/, novawow/, forum/, wotlk_db/, frontend/ (las islas React que Next
sustituyó), static/, staticfiles/, manage.py, requirements.txt, db.sqlite3 y el
Docker de Django.

Se quedan docs/ y sql/: no son código Django sino documentación y esquemas, y
sql/forum_schema.sql describe la BD acore_web que Next usa hoy.

La BASE DE DATOS no se toca. django_wow y sus 41 tablas home_* son ahora de
Next, que las consulta con SQL directo. El nombre se queda por historia.

README reescrito: describía cómo montar un Django que ya no existe (venv,
manage.py migrate, gunicorn, Docker).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 14:39:19 +00:00