Commit Graph

259 Commits

Author SHA1 Message Date
Inna 24248bc603 store: el árbol de categorías, igual que el original (colores y sin fantasmas)
Investigadas las 750 categorías de la BD contra el HTML real del original. Los
ítems estaban perfectos (554 hojas, 3068 ítems, mismo contenido en cada una: no
faltaba ninguno). Todo lo roto era de categorías:

- 45 CATEGORÍAS FANTASMA, todas con nombre de código ("11-1", "23-10"). El seed
  dedujo el árbol de los códigos de las hojas ("11-1-1" => padre "11-1"), pero
  el original NO siempre pinta ese nivel: en 45 hojas el "-1-" del código no
  corresponde a nada y cuelgan directas de su raíz. Ahora se lee el anidamiento
  real (<ul>/<li>): 705 categorías (20 + 131 + 554) en vez de 750.
- El nombre de una categoría NO es texto plano, es HTML, y lo estábamos
  aplanando: 141 nombres perdían su color y 40 cabeceras el suyo. Hay dos
  mecanismos y los dos hacen falta: la clase en la CABECERA, que sustituye a
  first-brown para colorear la categoría entera ("Brujo" en color de brujo), y
  los <span class="no-toggle X"> dentro del nombre (clases y las etiquetas
  [Nivel 232 a 245] / (DPS) en otro tono).
- 21 nombres TRUNCADOS en Tokens de Armadura: guardábamos "Guerrero" donde el
  original dice "Guerrero, Sacerdote, Druida" (solo se quedó el primer span).

El HTML se trocea en el SERVIDOR (`parseCategoryName`) y al cliente le llegan
solo texto + nombre de clase, así que no hay HTML de la BD entrando en el DOM.
El texto sin color va suelto, NO envuelto en <span>: el tema tiene
`span { color: #fff }` y lo pondría blanco en vez de heredar el de la cabecera.

`name` pasa a VARCHAR(512): con el HTML, el más largo mide 198 y no cabía en 191.
El name_en se genera en el propio seed traduciendo solo el TEXTO y respetando
las etiquetas, así que sql/store_category_en.sql sobra y se borra.
gen_store_catalog.py regenera el seed entero (categorías del HTML, ítems de la
BD, que ya estaban verificados) y es reproducible byte a byte.

Verificado en el navegador contra el original: mismo árbol, 0 categorías con
nombre de código, "[Nivel 232 a 245]" en marrón tenue y los colores de clase
oficiales (Guerrero #C79C6E, Sacerdote blanco, Druida #FF7D0A). En inglés
también, conservando los colores.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:24:19 +00:00
Inna fb28e97307 store: devolver por Stripe/SumUp lo no entregado de una compra con tarjeta
Faltaba la otra mitad de 0be3dc5: con saldo ya se reembolsaba lo que no llegaba
a enviarse, pero con tarjeta el dinero lo tiene la pasarela y el cliente se
quedaba pagando de más. Ambas tienen API de reembolso parcial, así que ahora
`fulfill` puede pedir que se devuelva un importe y lo hace quien conoce la
pasarela (lib/fulfill para Stripe, lib/sumup para SumUp).

Para saber CUÁNTO devolver hay que saber qué costó cada entrada, así que el
pedido pasa a guardarse como `itemId:qty:precio:moneda`. Releer el precio del
catálogo al entregar no vale por dos razones: 114 item_id son ambiguos (el mismo
objeto se vende a 50 PV y a 100 PD), y hay que devolver lo que se COBRÓ, no lo
que valga el ítem el día de la entrega. Los pedidos con el formato viejo se
entregan igual, pero no se puede calcular su reembolso: se registra para hacerlo
a mano.

`fulfill` devuelve ahora `boolean | {ok, refundEur}`; los servicios que no
entregan a medias siguen devolviendo boolean y no se tocan.

Sobre SumUp, todo comprobado contra su API en sandbox y nada de esto está en
sitios obvios:
 - El reembolso NO va por referencia de checkout sino por transacción.
 - Hay que usar el endpoint de v1.0; el viejo `/v0.1/me/refund/{txn}` responde
   409 a CUALQUIER reembolso parcial (el total sí funciona).
 - v1.0 quiere el importe en CÉNTIMOS y ENTERO, al revés que el resto de la API
   v0.1, que usa euros. Y mandar 0.10 en vez de 10 no da error: se trunca a 0,
   SumUp lo lee como "sin importe" y DEVUELVE EL PAGO ENTERO. Casi me lo comí.
 - Hay un mínimo por reembolso (20 cts en esta cuenta, la API lo dice en
   `min_refundable_amount`): por debajo responde 400 y se registra para hacerlo
   a mano.
El host sale de SUMUP_API_BASE (por defecto api.sumup.com) en vez de ir a fuego.

Verificado de punta a punta con pagos reales de prueba, parcheando el SOAP para
que solo saliera el primer correo:
 - Stripe (sk_test): pagados 1,30 €, entregadas 12/13 -> reembolso de 0,10 €,
   status succeeded en la API de Stripe.
 - SumUp (sandbox): pagados 1,50 €, entregadas 12/15 -> evento REFUND 0.3
   REFUNDED en la API de SumUp.

De paso: el concepto del cobro decía "1 objeto" al comprar 15 copias (contaba
líneas, no copias).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:57:00 +00:00
Inna 0be3dc5280 store: reembolsar solo lo no entregado si el envío falla a medias
Un carrito de más de 12 entradas se envía en varios correos. Si fallaba el 3.º
de 9, los 2 primeros ya estaban en el buzón del jugador y aun así se le devolvía
el carrito ENTERO: se quedaba los objetos gratis. Con las cantidades por línea
(f232f0f) llegar ahí es fácil: 100 copias son 9 correos.

sendStoreItems devuelve ahora CUÁNTAS entradas entregó en vez de un booleano: un
correo enviado no se puede deshacer, así que quien llama necesita saber dónde se
cortó. Cada `.send items` es un correo, así que el troceo es atómico por correo:
un chunk sale entero o no sale. purchaseStoreCart reembolsa solo las entradas
que quedaron sin enviar (cada una lleva su precio y su moneda) y devuelve
`partialDelivery`, con un mensaje que dice la verdad: parte llegó al correo del
juego y solo se ha devuelto el resto. Si no sale ni un correo, sigue siendo el
`deliveryFailed` de siempre con la devolución completa.

En el pago con tarjeta no se puede hacer lo mismo (el dinero ya lo cobró la
pasarela): fulfillStoreOrder devuelve false si no salió todo. No puede duplicar
porque claimPaidCheckout reclama el pago una sola vez, pero por eso mismo
tampoco hay reintento y un envío a medias hay que rescatarlo a mano desde
home_store_order. Queda anotado en el código.

Verificado de punta a punta, no solo razonado: con un parche temporal del SOAP
que deja salir el 1.er correo y tumba el resto, 13 copias de un ítem de 10 PD
(130 PD, 12+1 entradas) dejan el saldo en 500 -> 380, o sea 500-130+10: se
devuelve SOLO la entrada que no salió y se cobran las 12 entregadas. El parche
se revirtió y producción se reconstruyó limpia.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:19:09 +00:00
Inna f232f0fc5f store: poder cambiar la cantidad de un mismo artículo en el carrito
El carrito era un conjunto: añadir dos veces el mismo ítem no hacía nada y el
botón se quedaba en "Añadido". Ahora cada línea lleva sus copias, editables con
un <input number> en la columna Cant, y volver a pulsar Añadir suma una (mismo
patrón que el carrito de send-gift, que ya lo hacía así).

Ojo con los dos "cantidad" que conviven, que no son lo mismo:
`home_store_item.quantity` es el LOTE que entrega cada copia (Paño de lino = 20)
y `copies` es cuántas veces se compra la línea. Por eso la fila enseña "3 × 20"
y el total de la columna Cant sigue siendo unidades (60), como en el original.
Cada copia se manda al SOAP como una entrada propia (`2589:20 2589:20`), que es
justo lo que ya sabía trocear sendStoreItems (12 por correo) y parsear
fulfillStoreOrder: el formato del pedido de tarjeta no cambia.

El servidor no se fía del cliente: priceStoreCart recibe {id, copies}, valida las
copias (1..MAX_COPIES, como el MAX_QTY de send-gift), rechaza el carrito si algún
id no existe y recalcula los totales como precio*copias contra la BD.

Verificado contra la API con saldo de prueba, no solo en la interfaz: con 500 PD
y un ítem de 200, 2 copias (400) pasan el cobro y 3 (600) dan insufficientPd. Si
el servidor ignorase las copias ambas darían lo mismo, así que el corte exacto
entre 2 y 3 prueba que multiplica. Comprobado también que el reembolso del
deliveryFailed devuelve los 400 PD, y que copias=0 o 101 se rechazan.

El CSS del input va con `#cart-list` por especificidad, no por gusto: el tema
define `input[type=number] { width: 290px }` y se carga el último para ganar la
cascada, así que un `.store-copies` a secas perdía y el input salía de 290px
reventando la tabla (send-gift se libra porque usa estilo en línea).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:10:33 +00:00
Inna e500bd7a84 store: precios en euros en el carrito y mínimo real de las pasarelas
El carrito enseñaba PD/PV aunque eligieras tarjeta, y por defecto ya presuponía
saldo. Ahora se muestran SIEMPRE las dos monedas y se apaga la que no se va a
cobrar con el método elegido, así que no presupone nada ni se contradice.

Los euros por línea van con hasta 3 decimales a propósito: un ítem en PV con
precio impar cuesta medio céntimo (75 PV = 0,375 €), y redondear cada línea a 2
haría que las líneas no sumaran el total (0,38 + 0,38 = 0,76 contra un total de
0,75). El total sí va a céntimos: es el importe que se cobra de verdad. Y con
mínimo 2 decimales, que es dinero: "1,50 €", no "1,5 €". El formateo va por
Intl con el locale de la web, así que en español sale con coma; antes el
selector interpolaba el número crudo y decía "0.75 €" con punto.

Mínimos de las pasarelas: SumUp ya estaba en 1 € y Stripe estaba en 0,50 €,
ahora también 1 €. Estaban sueltos en la ruta; se centralizan en
`lib/store-pricing`, un módulo PURO que importan tanto la ruta como el
componente, para que el carrito enseñe exactamente lo que se valida y se cobra.
`storeEuroTotal` pasa a vivir ahí (una sola implementación) y lib/store lo
envuelve con un guard: si alguien cambia PD_PER_UNIT o VP_PRICE_FACTOR sin tocar
store-pricing, revienta al primer uso en vez de cobrar mal en silencio (esos
módulos tocan BD y no pueden llegar al bundle del cliente, de ahí la
duplicación, igual que en PaymentMethodSelect).

Por debajo del mínimo, la opción de tarjeta se desactiva y dice "Mínimo 1,00 €"
en vez de dejarte pulsar Enviar para que la API la rechace. El método efectivo
se deriva en el render (si el carrito baja del mínimo con tarjeta ya elegida,
cae al saldo) en vez de sincronizarlo con un setState en un efecto, que
provocaría renders en cascada.

Verificado en el navegador: con 0,375 € las dos tarjetas salen desactivadas y
elegido el saldo; con 1,50 € se habilitan. Las dos líneas de 0,375 € suman el
total de 0,75 €.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:55:27 +00:00
Inna 553a9cf993 fix: el tooltip de wowhead salía con huecos enormes entre líneas
El tooltip se inyecta en el <body>, así que le caen encima las reglas globales
del tema. La culpable era `th { height: 40px }` (global, viene tal cual del tema
original): el tooltip usa <th> para la columna de la derecha (fase, tipo de
arma, velocidad...), y cada una de esas filas se estiraba a 40px en vez de ~17.
Cuatro filas de esas = los ~100px que sobraban.

El store original no lo sufre porque su tooltip lo pinta otro script, con otro
markup: es un problema que aparece al usar el tooltips.js de wowhead de verdad.
Afectaba a TODAS las páginas con tooltips, no solo a la tienda.

Medido contra una página desnuda con solo tooltips.js (misma versión, mismo
ítem), que es la referencia de cómo debe verse: el tooltip pasa de 390px de alto
a 289, exactamente el de la referencia. Las tablas del tema no se tocan: la
regla va acotada a .wowhead-tooltip th, y el <th> del carrito sigue midiendo
40px.

De paso, el tooltip no tenía fondo y se leía la página a través de él. No era
cosa nuestra —pasa igual en la página desnuda—: universal.css solo le da layout
y colores de calidad, y el fondo lo pone el CSS de wowhead.com, que aquí no se
carga. Se le da el fondo y el borde de las cajas del tema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:18:57 +00:00
Inna 1752e74581 store: corrige la errata "Sacedorte" e iguala el ancho del cuadro de ítem
Dos arreglos pendientes que quedaron señalados:

1. "Sacedorte" era una errata de "Sacerdote" en tres categorías (3-10, 4-10 y
   19-9; la 20-9 estaba bien). Se veía así en la web en español. Corregido en la
   BD y, sobre todo, en el seed `store_catalog.sql`, que la traía 3 veces: sin
   eso, reconstruir la BD la resucitaba. Regenerado store_category_en.sql (217
   nombres distintos en vez de 218: las dos grafías ya son una).
   Un escaneo por parecido contra los términos oficiales de los DB2 confirma que
   era la única errata; el resto de casi-coincidencias son plurales legítimos
   ("Espadas" vs el "Espada" del juego).

2. El cuadro del ítem medía 160px y el del store original 200. Igualado. Solo
   afecta a /store: send-gift fija las columnas de su rejilla a 160px y hace
   width:auto, y se ha comprobado que sigue midiendo 160.
   Con solo cambiar la anchura se quedaba en 200 en vez de 214: el tema se
   diseñó para el content-box del navegador y el preflight de Tailwind mete
   border-box, así que el padding y el borde dejaban de sumar (el mismo problema
   que ya estaba arreglado para los inputs). Se restaura content-box solo en la
   tienda, porque en la rejilla de send-gift desbordaría las columnas.

Medido en el navegador: el cuadro pasa a 214px de ancho, que es lo que mide el
del original (213 en la captura). send-gift sigue en 160.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:54:09 +00:00
Inna bb0f4f21c1 store: categorías en inglés (términos oficiales sacados de los DB2)
Cierra el idioma de /en/store: los nombres de ítem ya salían en inglés
(2daf920), pero el árbol de categorías seguía en español y la tienda se veía a
medias.

Los términos DEL JUEGO no se traducen a mano, se sacan del cliente, para que
casen con lo que ve un jugador en inglés:

- ChrClasses  -> clases (Brujo = Warlock, Pícaro = Rogue...)
- ItemSet     -> sets (Vestiduras de la Némesis = Nemesis Raiment, Las diez
                 tormentas = The Ten Storms, Atavío viento abisal = Netherwind
                 Regalia). Casando sin distinguir mayúsculas: las categorías
                 capitalizan distinto que los ItemSet.
- ItemSubClass-> tipos de arma (Armas de asta = Polearms, Varitas = Wands)
- home_item_data -> lo que no es ItemSet. Los sets de PvP de WotLK no son
                 ItemSets reales, así que "Gladiador furioso" = "Furious
                 Gladiator" sale de los nombres bilingües de sus propios ítems.

Solo se traduce a mano lo que la tienda se inventó y no existe en el juego
(Items de leveo = Leveling Items, Moneda de transfiguración = Transmog
Currency), más las fórmulas (Nivel de objeto N = Item Level N, Set de banda
tier N = Tier N Raid Set). Los ~45 nombres que son solo un código ("11-1") y
los que ya estaban en inglés ("Tier 7.1 (PvE)") se dejan igual.

wdc_db2.py aprende a leer strings de tablas no sparse: el valor guardado no es
un índice a la string table sino un offset relativo a la posición del propio
campo. Verificado con ChrClasses (las 10 clases en ambos idiomas).

Revisadas las 218 a mano. Corregida una que me había inventado: "Equipo de
Batalla del Iluminado de la Fuente del Sol" no es "Sunwell Radiance"
(eso es un buff): "Iluminado" es Lightbringer en el ItemSet y "Fuente del Sol"
es Sunwell según sus ítems -> "Sunwell Lightbringer Battlegear".

OJO, bug de datos preexistente: "Sacedorte" (errata de "Sacerdote") en las
categorías 3-10, 4-10 y 19-9; el 20-9 sí está bien. Se ve así en español. El
name_en va a "Priest"; la errata en español NO se toca.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:45:24 +00:00
Inna 2daf920c01 store: nombres de ítem en el idioma de la web (desde home_item_data)
El catálogo original solo tiene el nombre en español, así que /en/store venía
mostrando los ítems en español. Ahora salen de `home_item_data` (extraída de
los DB2 del cliente, commit 4ba9c42), que es el único sitio con el nombre en
inglés.

- getStoreCatalog(locale) hace LEFT JOIN con home_item_data y elige name_es o
  name_en. Si falta el nombre en ese idioma se cae al del catálogo, que siempre
  está, así que ningún ítem puede quedarse sin nombre.
- La columna se elige de una lista cerrada ('en' -> name_en, resto -> name_es):
  nunca se interpola el locale crudo, que viene de la URL.
- /api/store/catalog recibe el locale por query: la ruta no cuelga de [locale].

Verificado en los dos idiomas: 45327 sale "Grito de la sirena" en /es y
"Siren's Cry" en /en. El árbol sigue devolviendo las 3068 filas y ninguna sin
nombre (el JOIN no duplica: `entry` es clave primaria).

Pendiente: los nombres de las CATEGORÍAS siguen solo en español
(home_store_category.name), así que /en/store se ve a medias. No son datos del
juego, así que los DB2 no ayudan: hay que traducir 218 nombres distintos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:31:57 +00:00
Inna 4ba9c42829 sql: datos de ítems del cliente 3.4.3 extraídos de los DB2 (es/en)
En 3.4.3 `item_template` ya no existe: los datos del juego viven en ficheros
DB2 del cliente, y aquí acore_world está vacío. Se extraen a `home_item_data`
(44873 ítems): nombre es/en, calidad, nivel de objeto, tipo de inventario y
displayId.

No hay ninguna librería en PyPI que lea WDC4 (wdbx, dbc-extract3, wow-db2 no
existen), así que el lector va incluido en sql/db2/:

- `wdc_db2.py`: WDC3/WDC4 con registros fijos (bitpacked, common y pallet).
- `itemsparse.py`: ItemSparse es una tabla *sparse*, que es otro mundo: los
  locstring van inline y terminados en \0, así que desplazan todo lo que va
  detrás y los offsets de field_storage_info no valen. Hay que recorrer los
  campos en orden, y el fichero no dice cuáles son strings: eso sale de las
  definiciones oficiales de WoWDBDefs (ItemSparse.dbd, incluido), casando el
  layout_hash del propio fichero (D532973D = 3.4.3).
- `db2_to_sql.py`: genera sql/item_data.sql. Regenerable y multi-locale: basta
  con dejar el ItemSparse.db2 de cada idioma en /root/<locale>/.

Verificado: los 3068 ítems de la tienda están cubiertos; 2949 nombres (96%)
coinciden literalmente con el catálogo antiguo y los 119 restantes son
revisiones de traducción de Blizzard, no fallos del parser ("Bordón de Luz" ->
"Batuta de Luz"). El ítem 45327 sale q4 / ilvl 238 / "Siren's Cry", idéntico a
lo que devuelve wowhead. La distribución de calidad es realista (53
legendarios, 23 artefactos).

Ojo: la tabla está cargada pero TODAVÍA no la usa ningún código de la web.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:24:34 +00:00
Inna 14ebd1e1f1 store: previsualización del render del ítem al pasar el ratón
Replica el preview del store original: un icono en el cuadro del ítem que, al
pasar el ratón, muestra el render del objeto. El CSS ya estaba en el tema
(nw-st-tooltip-w / nw-st-img-tooltip / nw-img-display, copiado del original);
solo faltaban el markup y las imágenes.

- Los 545 PNG se sirven desde nw-images/nw-displays/ en vez de enlazar al
  servidor original, para no depender de un tercero que puede cortarlo.
- Solo lo llevan los ítems equipables (610 de 3068): el resto no tiene
  apariencia, igual que en el original. El dato ya estaba en la columna
  `preview` de home_store_item y llegaba hasta el componente sin usarse.
- El icono va con `fas`, no con el `fa` suelto del original: cargamos Font
  Awesome 6, donde solo .fas/.fa-solid activan la fuente, y el `fa` de FA4/5
  no pintaría nada sin el shim de compatibilidad.

Descartado el visor 3D de wowhead: zamimg sirve el meta del modelo con lista
blanca de orígenes (403 desde nuestro dominio, 200 solo desde wowhead.com), así
que solo funcionaría proxeando sus assets para saltarse esa comprobación.

Verificado en el navegador: oculto por defecto, visible en hover, imagen 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:07:08 +00:00
Inna 8e16ba8391 store: iguala el carrito y el cuadro de ítem al diseño original
Comparado contra el store original (ultimowow /es/store-bennu) con una sesión
real: markup, CSS y clases que aplica su power.js. Cinco causas distintas:

- Los nombres de ítem salían azules: el layout declaraba `const whTooltips`,
  que queda en el ámbito léxico del script y NO crea propiedad en `window`,
  que es de donde tooltips.js lee la config -> se ignoraba entera. Ahora
  `window.whTooltips`, con colorLinks+iconizeLinks (el original no define
  whTooltips y usa esos defaults; sus enlaces acaban con clases `icontinyl q4`).
  renameLinks sigue en false: el texto sale de nuestra BD, en español.
- tooltips.js solo recorre el DOM al cargar, así que los enlaces que monta
  React (hoja del árbol, carrito) no se teñían. Se pasan por
  $WowheadPower.refreshLinks(), igual que hace el original tras cada AJAX.
- globals.css redefinía .item-box/.item-name/.item-img-box/.max-left-table2,
  que el tema nw-ryu ya trae del original, y con más especificidad ganaba la
  cascada. El peor: un `border-collapse` que anulaba el `border-spacing: 0 8px`
  del tema, que es lo que separa las filas del carrito.
- Faltaban la caja "Carrito - <REINO>" y "Personaje seleccionado: <nombre>"
  (coloreado por clase), que el original sí tiene.
- El total de la columna "Cant" mostraba el número de líneas en vez de la suma
  de cantidades: un ítem que entrega un mazo de 20 debe contar 20, no 1.

Verificado con navegador contra un build de producción: el nombre del ítem sale
con clases `icontinyl q4` y color rgb(163,53,238), el mismo valor exacto que
devuelve el original.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 07:11:07 +00:00
Inna 742567cfbf Fix: /store no mostraba nada al elegir personaje
El selector funcionaba: el fetch del catálogo respondía y React montaba el
árbol entero, pero el tema lo tenía oculto y no se veía nada en pantalla.

Son reglas del store original (BENNU), que ocultaba estos elementos y los
revelaba con jQuery (.show()/slideToggle()). En el port a React la visibilidad
la decide el montaje del componente, y el tema nw-ryu se carga el último para
ganar la cascada, así que hay que deshacerlas con !important:

- #store-div: ocultaba el árbol completo (el síntoma reportado).
- #store-list li ul: habría ocultado las subcategorías al desplegarlas.

Además, el <ul> anidado comparte clase con el <span> que lo abre (igual que en
el markup original), así que el display:inline-block de los spans deformaba la
lista. Esas reglas pasan a ir atadas a `span`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:34:58 +00:00
Inna f0b006404c store: pagar también con Stripe y SumUp (tarjeta), además del saldo PD/PV
Selector de forma de pago en el carrito (Saldo PD/PV · Stripe · SumUp), como el
resto de servicios. Con tarjeta, el carrito se convierte a euros (100 PD = 1 €,
200 PV = 1 €), se guarda como pedido y se entrega tras confirmarse el pago.

- BD: tabla home_store_order (ref, cuenta, personaje, items, importe) para no
  depender del límite de metadata de la pasarela (sql/home_store_order.sql).
- lib/store: storeEuroTotal, createStoreOrder, fulfillStoreOrder.
- paid-services: servicio 'store' (fulfill = enviar el pedido por order_ref);
  la entrega la dispara service-success/reconciliación/webhook como los demás.
- /api/store/send: rama provider stripe/sumup (crea pedido + checkout, mínimo
  1 €/0,50 €) además del pago con saldo.
- StoreBrowser: selector Saldo/Stripe/SumUp; con tarjeta redirige a la pasarela.
- i18n Store (payMethod/payBalance/payStripe/paySumUp/confirmCard/amountTooLow)
  y Paid.store (title/success). Verificado: euros 200PD+28PV=2,14 €, pedido y
  fulfill por order_ref (SOAP real pendiente, worldserver caído).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:14:01 +00:00
Inna 5a903ef627 store: añade la sección plegable "Novedades" (changelog)
Replica el bloque Novedades del diseño original: fieldset plegable con el
changelog (Noviembre 2024) y sus objetos enlazados a wowhead (icono zamimg tiny
+ color por calidad). components/StoreNews.tsx (client, toggle con flecha
rotate/rotate2 del tema), incluido en la caja de info de /store. i18n
Store.newsTitle/newsIntro/newsDate (es/en).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 00:00:11 +00:00
Inna 026fbcb81c Implementa la tienda /store (catálogo BENNU: 750 categorías, 3068 ítems)
Porta la tienda de ítems/transfiguración del sistema antiguo: elegir personaje →
árbol de categorías (3 niveles) → carrito → enviar por correo. Se paga con el
saldo PD o PV de la cuenta (cada ítem lleva su precio y moneda).

- BD: home_store_category (árbol por code "1-1-1") + home_store_item
  (mismos item_id, nombres, iconos, precios y moneda que el original;
  2947 en PD, 121 en PV). Seed en sql/store_catalog.sql, extraído del HTML real.
- lib/store.ts: getStoreCatalog (árbol anidado), getStoreBalances, priceStoreCart
  (precios REALES de BD), purchaseStoreCart (cobro atómico PD+PV con FOR UPDATE y
  reembolso si el envío SOAP falla; `.send items` troceado a 12/correo).
- API: GET /api/store/catalog (tras elegir personaje) y POST /api/store/send.
- components/StoreBrowser.tsx: selector de personaje, árbol colapsable (ítems
  montados solo al abrir), carrito con totales PD/PV, enviar, modal de resultado.
- página /store con la info y el título "Tienda - REINO". Enlaces de ítems e
  iconos por wowhead/zamimg (con tooltip y locale). El enlace ya existía en
  AccountTools. i18n namespace Store (es/en). CSS del árbol/carrito/modal.

Verificado: catálogo E2E (750 cats/3068 ítems), cobro atómico + reembolso y
detección de saldo insuficiente (cuenta 15). SOAP real sin probar (worldserver
caído en este entorno).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 23:54:57 +00:00
Inna 033ebc6861 Fix: tooltips de wowhead en el idioma de la web (no siempre inglés)
El script de tooltips prioriza el atributo data-wowhead sobre el subdominio del
href, y su parámetro `domain` codifica idioma + versión: el 1.er segmento va al
mapa de locales ({es:6,...}) y el resto a la versión (wotlk=WRATH). Antes se
enviaba `domain=wotlk` (sin idioma) → locale 0 = inglés siempre.

Ahora `wowheadData(type,id,locale)` genera `domain=es.wotlk` en español (locale 6)
y `domain=wotlk` en inglés. Verificado: el endpoint de datos con locale 6 devuelve
los nombres en español.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:41:58 +00:00
Inna 7d1abab9f1 Enlaces de ítems por wowhead (locale + tooltips) en toda la web
Sustituye la base de datos de ítems del sistema antiguo (AoWoW en
wotlk.novawow/wotlk.ultimowow) y los iconos de mirrors por wowhead / zamimg,
con el idioma del tooltip según el locale de la web (es. / www.).

Sistema reutilizable para rutas actuales y futuras:
- lib/wowhead.ts: wowheadUrl/wowheadItemUrl/wowheadWotlkHome (subdominio por
  locale), wowheadIcon (zamimg) y wowheadData (atributo data-wowhead, rama WotLK).
- components/WowheadLink.tsx: enlace con tooltip en el locale actual.
- layout raíz: config whTooltips (solo tooltips) + script tooltips.js global;
  funciona con contenido añadido por React (carrito, búsquedas).

Aplicado en: send-gift (catálogo + carrito), restore-items, recruit
(RecruitPanel, enlace por item_id + icono zamimg), quest-character (misión/npc/
facción + iconos), y el enlace "WOTLK DB" de la cabecera.

Verificado en prod (:3001): /es -> es.wowhead.com, /en -> www.wowhead.com,
data-wowhead presente, iconos por zamimg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:34:26 +00:00
Inna 809eb756c8 send-gift: permitir pagar con PV (puntos de voto), más caro que PD
Solo en /send-gift se añade una 4ª forma de pago: PV (vote points, gratis por
votar). Cuesta más que PD para preservar su valor: coste_PV = coste_PD ×
VP_PRICE_FACTOR (=2, configurable en lib/pay-with-dpoints).

- lib/dpoints: getVPointsBalance, creditVPoints y spendVPoints (débito atómico
  del campo vp, con FOR UPDATE), espejo de las de PD.
- lib/pay-with-dpoints: VP_PRICE_FACTOR, vpointsCost() y payServiceWithVPoints()
  (descuenta PV, ejecuta el envío y reembolsa si falla).
- gift/checkout: rama provider='vp' (además de 'pd').
- service-success: la entrega inmediata cubre 'pd' y 'vp'.
- PaymentMethodSelect: opción VP opcional (solo si el form pasa vpBalance);
  muestra coste en PV, insuficiencia y el saldo VP.
- SendGiftForm + página: pasan el saldo VP.
- i18n Pay: vp, vpCost, insufficientVp, balanceVp, errors.insufficientVp (es/en).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:18:14 +00:00
Inna 0244d0f8ef rename-guild: el texto informativo refleja las 3 formas de pago
El párrafo "Requiere 1000 PD" era estático y contradecía el nuevo selector.
Ahora indica el coste equivalente (1000 PD o 10 €) y que se puede elegir la
forma de pago (PD, tarjeta con Stripe o SumUp), que ya se muestra en el
formulario.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:01:56 +00:00
Inna 20ee5a83c8 rename-guild: permitir pagar con PD, Stripe o SumUp
Renombrar hermandad ya no es solo PD: ahora ofrece el mismo selector de
forma de pago que el resto de servicios (saldo PD / tarjeta Stripe / tarjeta
SumUp). El coste es equivalente: 1000 PD = 10 €.

- lib/guild: se separa la validación (checkGuildRenameEligibility) del
  renombrado por SOAP (renameGuildBySoap); nuevo GUILD_RENAME_EUR (10 €).
- paid-services: entrada 'rename-guild' para que la reconciliación/webhook
  entregue el servicio tras el pago con tarjeta.
- nueva ruta /api/guild/rename/checkout con las 3 pasarelas (valida
  elegibilidad antes de cobrar); se elimina la antigua /api/guild/rename.
- RenameGuildForm usa PaymentMethodSelect y redirige a service-success.
- i18n: Paid.rename-guild (title/success) para la página de éxito.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:56:44 +00:00
Inna 1b0920d11f Permitir pagar los servicios con PD, Stripe o SumUp (elección)
Los 9 servicios con precio en euros (rename, customize, change-race,
change-faction, level-up, gold, transfer, restore-item, send-gift) ahora
ofrecen un selector de forma de pago con las 3 opciones: saldo PD, tarjeta
(Stripe) o tarjeta (SumUp).

- lib/dpoints: spendDPoints() descuenta PD de forma atómica (FOR UPDATE).
- lib/pay-with-dpoints: paga con saldo PD, ejecuta la acción al momento y
  reembolsa si la ejecución falla; 100 PD = 1 €.
- rutas checkout (character/[service] y gift): rama provider='pd' que valida
  saldo, descuenta, ejecuta fulfill y devuelve la URL de éxito (sin pasarela).
- service-success: rama provider='pd' (la entrega ya se hizo en el checkout).
- PaymentMethodSelect: componente compartido con coste por método y saldo;
  desactiva PD si no hay saldo suficiente.
- Formularios (PaidServiceForm, Gold, Transfer, RestoreItems, SendGift) y sus
  páginas pasan el saldo PD y envían el método elegido + locale.
- i18n: namespace Pay (es/en); añadidas claves Paid.restore-item que faltaban.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:44:31 +00:00
Inna 942fe2e397 Fix captcha: resetear Turnstile tras un fallo (token de un solo uso)
Al fallar (p.ej. contraseña incorrecta) el token de Turnstile ya se consumió en el
servidor; el reintento reenviaba el mismo token y daba "captcha fallida" aunque
estuviera resuelto. Se resetea el widget (setCaptcha('') + captchaKey++ con
<Turnstile key={captchaKey}>, mismo patrón que recover/restore/quest) en login,
create-account y trade-points (venta y canje). El check de código no usa captcha,
no se resetea ahí.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:17:37 +00:00
Inna 20e1d5a6b5 Renombrar /login -> /log-in en toda la web (/login da 404)
Mueve la página de login a /log-in y actualiza todas las referencias (enlace
CONECTAR del header y los redirect de auth de ~45 páginas). /login ahora da 404.
La API /api/auth/login no se toca (solo la ruta de página).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:03:31 +00:00
Inna 14125dbd72 Formularios: noValidate en todos para usar red-response del sitio
Desactiva la validación nativa del navegador ("Rellene este campo", formato de
email) en el resto de formularios (recover, reset-password, trade-points,
rename-guild, transfer-dp, gold, promo, transfer, quest, send-gift, 2FA, restore).
Los errores se muestran como red-form-response (por validación en cliente o del
servidor), consistente con login y create-account. Botones ya gateados por campos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:57:39 +00:00
Inna 071b1d5f0e login: noValidate para usar red-response en vez del aviso nativo
El navegador mostraba "Rellene este campo" (required) en lugar del error del sitio.
noValidate en el form deja que la validación en cliente ya existente muestre el
red-form-response (missingFields).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:55:14 +00:00
Inna 8ca3975cac create-account: desactivar validación nativa (noValidate) para usar red-response
El navegador mostraba su tooltip nativo "Rellene este campo" en los inputs required
en vez del mensaje de error del sitio. Con noValidate en el form, la validación en
cliente (clientError) muestra el red-form-response también para campos vacíos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:52:12 +00:00
Inna c1f865dfa5 create-account: validación en cliente con red-response inmediato
Antes los errores (contraseñas/correos que no coinciden, campos vacíos) solo se
mostraban en rojo tras el envío al servidor. Ahora se validan en el cliente y se
muestran al instante como red-form-response, sin llamada al servidor. Nueva clave
emailMismatch. El servidor sigue haciendo la validación final (Gmail, existe, etc.).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:50:30 +00:00
Inna 5d0d2bb089 Renombrar /register -> /create-account + rediseño (Battle.net)
Mueve la página de registro a /create-account (los enlaces del header y del login
apuntan ahí; /register ahora da 404 con el 404 del sitio). Rediseño según el
diseño aportado, adaptado a Battle.net (cuenta por email, sin usuario): caja de
info bnet (tipo bnet, reglas de contraseña/Gmail, activación, login por correo),
ojos para mostrar/ocultar contraseña, checkbox de "no accedo desde EEUU" +
términos (con enlaces), ambos requeridos para habilitar el botón, y bloqueo de
pegar en confirmar contraseña/correo. i18n ES/EN. No incluye FingerprintJS
(antiabuso por huella de dispositivo, requiere backend; la protección es Turnstile
+ Gmail + activación).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:44:15 +00:00
Inna f37bf71dc0 d-points: tabla de niveles especifica también el importe en euros
La tabla de niveles mostraba solo PD ("Hasta 100 PD"); ahora añade el equivalente
en la moneda (100 PD = 1 €): "Hasta 100 PD (1 €)" / "Up to 100 PD (1 €)". Se
sustituyen los 14 mensajes range1..14 por 3 plantillas parametrizadas
(rangeFrom/rangeUpTo/rangeOver) con {pd}/{price}/{sym}; RANK_ROWS lleva el umbral
en PD y el tipo. Adaptado a la moneda (DP_CURRENCY).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:34:45 +00:00
Inna 8a5ce82afa Rango de cuenta por TOTAL DONADO (Stripe + SumUp), no por saldo
El nivel/rango usaba el saldo de PD actual, que baja al gastar. Ahora se basa en
el total donado = suma de pagos confirmados (Stripe mode test/live + SumUp),
convertidos a PD (amount * 100). Cuenta todas las pasarelas y donaciones mixtas y
no baja al gastar PD. Se usa max(donatedPD, saldo) para no rebajar el rango a
nadie (p.ej. PD de promo). getAccountDashboard expone donatedPD. Verificado:
cuenta 15 con €43 en pagos -> Nivel 11 aunque el saldo sea 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:25:54 +00:00
Inna dc3cd411f6 Zonas ES: añadir a mano los 3 ids restantes (cobertura EN/ES completa)
15921 'The Den' -> 'La Guarida'; 16406/16407 '[DNT]' (marcador interno de
Blizzard, se conserva). Ahora todos los ids del mapa EN tienen también ES.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:19:19 +00:00
Inna 01948ba356 Zonas ES: añadir las que faltaban desde FusionCMS (9156 -> 9672)
Fusiona las 516 zonas ausentes del wow_zones.php español de FusionCMS en
lib/data/zone-names.json (solo las que faltaban; se conservan las existentes),
ordenado por id. Ahora todos los ids con nombre EN tienen también ES.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:17:11 +00:00
Inna 2e9106f0bf Ficha de personaje: zona/raza/clase multiidioma (ES/EN)
Añade mapa de zonas en inglés (lib/data/zone-names-en.json, 9675 zonas de
FusionCMS wow_zones.php; cobertura 100% de los ids del mapa ES) y nombres de
raza/clase EN (wow_constants.php). getZoneName/getRaceName/getClassName aceptan
locale (EN cae a ES si falta). getAccountDashboard(session, locale) lo propaga y
my-account pasa el idioma. El dinero (oro/plata/cobre) usa iconos, universal.
Verificado: Innadin -> Human/Paladin/Stormwind City en /en.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:11:30 +00:00
Inna 794021f9a3 changelogs: fecha por idioma (es-ES / en-US)
formatFecha usaba es-ES fijo; ahora recibe el locale y formatea en en-US para
inglés. /en/changelogs muestra "July 14, 2026 at 07:55 PM"; /es/ en español.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:58:31 +00:00
Inna 85c5c995ee Admin del foro: gestionar traducción EN de categorías y foros
Los formularios de crear categoría/foro aceptan nombre/descripción en inglés, y
se añade EDICIÓN en línea de categorías (name, name_en, order) y foros (name,
name_en, description, description_en). lib/admin-forum: createCategory/createForum
con EN + updateCategory/updateForum; rutas PUT en category/[id] y forum/[id].
Claves Admin nuevas (nombre/descripción EN). Verificado: listado con EN + update
round-trip; PUT protegido (403 sin admin).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:55:54 +00:00
Inna cc6c76859c Foro multiidioma: nombres/descripciones de categorías y foros (ES/EN)
forum_categories.name_en y forums.name_en/description_en (sql/add_forum_en.sql,
aplicado en prod y traducidos los existentes: Comunidad/Anuncios/Discusión general).
getForumIndex(locale) y getForum(id, locale) usan EN si existe, si no caen al
español. Las páginas del foro pasan el locale. Labels (Temas/Mensajes) ya estaban
traducidos. Verificado: /en/forum en inglés, /es/forum en español.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:50:20 +00:00
Inna 1d2aae5df2 ban-history: alcance y estado multiidioma
getSanctions devuelve claves (scopeKey account/battlenet/character + scopeName;
statusKey active/activePermanent/expired) en vez de español. La página traduce
alcance ("Character: {name}") y estado; "Permanente" ya usaba clave. Motivo y
autor son datos del GM (no se traducen). Nuevas claves History.ban.scope/status.
Verificado con sanciones de prueba (todos los combos resuelven en es y en).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:44:09 +00:00
Inna 16ae9f0db8 security-history: acción y estado web multiidioma
La lib devolvía la acción ("Token de seguridad solicitado") y el estado del login
web ("Conexión exitosa"/"Contraseña incorrecta") en español. Ahora devuelve claves
(actionKey='tokenRequested'; statusKey='success'/'wrongPassword'/'other' + statusRaw)
y la página las traduce (History.security.action/webStatus). El coloreado rojo pasa
a depender de statusKey==='wrongPassword'. Verificado con datos reales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:39:46 +00:00
Inna 19d3c43a76 trans-history: concepto multiidioma (helper de concepto compartido)
Extrae la lógica de concepto traducible a lib/tx-concept.ts (buildConcept +
purchasedPD + SERVICE_CONCEPT) y la reutilizan points-history y trans-history.
La columna Concepto de trans-history ahora se traduce (reusa History.points.concept.*)
en vez de mostrar el product_name en español. Corrige también la sombra de
variable en PlatformBox (map t -> tx). Verificado con la cuenta 15.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:35:18 +00:00
Inna b2e7ab1b88 points-history: concepto/método/estado multiidioma
Los valores de las columnas Concepto/Método/Estado venían en español desde la lib.
Ahora getPointsHistory devuelve CLAVES (conceptKey+args, method 'vote'/'promo',
status 'delivered'/'pending'/'credited') y la página las traduce. Concepto por
servicio ("Rename character: {char}", "Vote on {site}", "Code {code}", "{n} PD");
los pagos heredados sin servicio caen a conceptRaw. Claves nuevas en History.points.
Verificado con la cuenta 15: todas las combinaciones resuelven en es y en.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:29:20 +00:00
Inna 86c9b01c76 Noticias multiidioma (ES/EN) con fallback
home_noticia gana columnas titulo_en/contenido_en (sql/add_news_en.sql). getNoticias(locale)
usa la versión EN si existe, si no cae al español (título y contenido independientes); la fecha
también se formatea por idioma. El panel de admin permite escribir la traducción al crear y EDITAR
noticias existentes (nuevo PUT /api/admin/news/[id], updateNews). Claves Admin nuevas (título/
contenido EN, editar/guardar). Probado: /en/ muestra EN, /es/ ES; PUT guardado (403 sin sesión).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:14:07 +00:00
Inna 81e1450e6b i18n: títulos de pestaña por idioma (metadata -> generateMetadata)
Los <title> estáticos estaban en español fijo. Se migran 31 páginas a
generateMetadata reutilizando la clave de título ya traducida de cada página
(o del <h1>). Se traducen además los PageShell title hardcodeados de las 6
páginas CharServiceB y los títulos de download-client y changelogs (+ su
mensaje de error). tsc/build OK, paridad es/en, <title> cambia por idioma.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:07:25 +00:00
Inna 651d8deafc i18n: traducir toda la app (ES + EN) — páginas y componentes con texto hardcodeado
Se externaliza el texto español hardcodeado de ~55 archivos a next-intl y se añaden
traducciones al inglés, con paridad de claves es/en. Nuevos namespaces: History,
CharService, CharServiceB, Points, Legal, UI, Misc (+ altas en Common/Admin).

- Historiales (PD/PV, transacciones, sanciones, seguridad), servicios de personaje
  (transfer, send-gift, quest, restore-*, change-*, customize, level-up, gold, rename),
  PD/pagos (d-points, trade, promo, transfer-dp, rename-guild, DPointsTabs), páginas
  legales (cookies, privacidad, términos, reembolsos, aviso legal, contacto), layout
  (cabecera, footer, cookies, 2FA, descargas, jugadores, recluta) y páginas varias
  (home, reino, ayuda, addons, 2falogin).
- Textos con markup inline via t.rich; interpolación con ICU.
- Componente <NoteLegend/> para la leyenda NOTA/NOTE compartida.
- payLabel/confirmText de los servicios de pago traducidos.
- Verificado: tsc OK, next build OK, todas las claves t() resuelven en es y en,
  todos los t.rich casan etiquetas, páginas 200 en /es/ y /en/ sin claves crudas.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:58:54 +00:00
Inna e457a43f05 Header: nombre del reino en mayúsculas (fuente Futura consistente)
El nombre del reino (p.ej. "Trinity") salía en minúsculas y las minúsculas caían
a una fuente de reserva (Futura es de mayúsculas), desentonando con el resto del
nav. Se muestra en mayúsculas como los demás ítems.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:57:21 +00:00
Inna 906a18000e Header: versión en el CSS del tema para forzar recarga de caché
El nav ya mide igual que el original (1112px, items 174px, Futura) tras el fix de
box-sizing; el CSS del tema se servía sin versión y quedaba cacheado. Se añade
?v=2 a novawow-style.css para bustear la caché del navegador.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:50:04 +00:00
Inna 434011624c Barra final solo en la raíz de idioma (/es/), resto de rutas sin barra
trailingSlash true ponía barra en todo (/es/content-creators/). Se pasa a
skipTrailingSlashRedirect + normalización en middleware: raíz de idioma /es → /es/
(con barra), y cualquier sub-ruta con barra final se redirige sin ella
(/es/x/ → /es/x). Sin bucles. Las /api quedan fuera del middleware (sin 308).
El redirect del logout apunta a /es/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:36:13 +00:00
Inna d0490e06fe URLs con barra final (trailingSlash) como el sitio original
next.config: trailingSlash true → /es redirige a /es/ y los enlaces se generan
con barra. Se ajustan a barra final los destinos internos que se enlazan a mano
(logout /log-out/ y su redirect, y los window.location.assign de login/
select-account) para evitar el salto 308 extra.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:18:06 +00:00
Inna c5a90d744a Header: enlaces internos con <Link> (prefijo de idioma)
Los enlaces internos del header eran <a> planos sin prefijo (/download-client…);
ahora usan <Link> de next-intl → /es/download-client, /es/changelogs, reino,
jugadores, videos, ayuda y los logos. Externos (WOTLK DB) y el logout (route
handler) siguen como <a>. Resuelve también los avisos no-html-link-for-pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 15:12:59 +00:00
Inna dd748d64d8 Header: restaurar content-box en el nav (tamaño como el original)
El tema nw-ryu se diseñó para content-box; los items del nav usan min-width:126px
+ padding:24px (ancho total 174px). El preflight de Tailwind fuerza border-box, que
mete el padding dentro del min-width -> items ~48px más estrechos y header apretado
respecto a ultimowow.com. Se revierte a content-box en .nav-bar a / .nav-dropdown-btn
/ .nav-dropdown-content a (mismo patrón que ya se hizo con inputs/tablas/img).

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