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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
Envía ítems de la tienda por correo al personaje de un amigo, pagado por
SumUp. lib/gift.ts (catálogo por categoría, priceCart con precios reales de
BD, sendGiftByMail vía SOAP .send items troceado a 12/correo + anti-inyección);
servicio send-gift en PAID_SERVICES; ruta dedicada /api/gift/checkout (origen
propio, destino existe, token de seguridad, carrito); SendGiftForm + página con
el diseño; i18n Paid[send-gift]; seed_giftitems.sql (4 cat, 8 ítems).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /change-race-character (3€), /change-faction-character (5€, valida condiciones:
sin hermandad/correo/subastas/capitán-arena y oro <= cap por nivel),
/level-up-character (10€), /gold-character (oro por correo SOAP). Cada uno borra
su ruta antigua y actualiza el link del panel.
- SumUp: hook precheck en PaidServiceConfig (condiciones cambio de facción);
columna home_stripelog.metadata para campos extra (gold_amount).
- /quest-character: rastreador de estado de misiones/cadenas (clase, Hijos de
Hodir, misión por ID) en personaje offline; cooldowns por categoría, Turnstile,
IDs de misión verificados en wotlkdb 3.3.5a. Tabla home_questsearchhistory.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /rename-character y /customize-character: renombrar/personalizar pagados por SumUp
(getRenamePrice/getCustomizePrice). SumUp ahora es service-aware: columna
home_stripelog.service; createSumUpCheckout guarda service+character;
fulfillSumUpCheckout despacha a la acción del servicio (.char rename/customi) o,
sin service, acredita PD. [service]/checkout acepta provider:'sumup';
PaidServiceForm acepta provider+confirmText; service-success maneja el return SumUp.
Se eliminan las antiguas /rename y /customize (Stripe).
- /revive-character (comparte ReviveServiceContent); se elimina /revive.
- Renombres de ruta + todos sus enlaces:
/account -> /my-account, /recruit -> /recruit-a-friend, /unstuck -> /unstuck-character.
(/select-account y /api/account/* intactos.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CharacterSelect: componente reutilizable que colorea las opciones por clase
(class="priest big-font"…) y el <select> con la clase seleccionada. Usado en
unstuck, revive, gold, transfer, trade-points, servicios de pago y recruit.
getGameCharacters ahora devuelve classCss.
- /unstuck (+revive): diseño del original (info + NOTA, prompt, opciones coloreadas,
botón "Desbloqueado"/"Revivido" con refresh).
- /vote-points: seed de los 4 sitios con logos y URLs (sql/seed_votesites.sql);
botón voted-button para sitios en cooldown; etiqueta "Nota:" separada.
- account-info.png (imagen nueva) y CSS de los paneles de cuenta: se añade
background-blend-mode: saturation y background-size en #account-settings y
.account-fieldset para que la imagen salga como el original.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /security-token: diseño completo del original (info + advertencia + NOTA 7 días),
fecha en formato HH:MM:SS DD-MM-YYYY, botón "Token enviado", enlace a /recover.
- /recover: 4 opciones (contraseña por usuario, nombre de cuenta, token de seguridad
y enlace de activación por correo); recuperación de token nueva; Turnstile por envío.
- /vote-points: caja de info con las 7 secciones Q&A + panel "Sitios de votación" con
tarjetas inline-div (imagen, PV, último voto), botón refrescar; abre el sitio y acredita
PV al volver. lib/vote.ts + getVoteSitesForAccount.
- /rename-guild: renombra una hermandad de la que la cuenta es Maestro por 1000 PD +
token de seguridad; SOAP .guild rename con comillas; reembolsa los PD si SOAP falla.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- /transfer-d-points: transferir PD a la cuenta dueña de un personaje
(requiere token de seguridad; movimiento atómico con FOR UPDATE).
- /trade-points: comercio PD<->oro mediante códigos de 1 solo uso
(contraseña + token + Turnstile; oro al creador por SOAP; expira 1h;
lock de 5s tras canjear; rollback total si falla la entrega).
- /promo-code: canje de códigos por PD/PV (sensibles a mayúsculas/minúsculas,
usos limitados, 1 canje por cuenta).
- /admin/promo: panel para crear / editar / activar / desactivar / borrar
códigos de promoción.
- Tablas nuevas (sql/): home_tradecode, home_promocode, home_promoredemption.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Junto al «Nombre de la cuenta» hay un botón «+» que pide la contraseña y crea
una nueva cuenta de juego bajo la Battle.net. Ruta /api/account/add-game-account:
verifica la contraseña (authenticate contra battlenet_accounts), calcula el
siguiente battlenet_index (máx 8) y crea la fila `account` con SRP6 Grunt
derivado de la contraseña (gameMakeRegistration) — igual que la activación, así
la cuenta sirve para entrar al juego. Recarga completa al crear/cambiar.
GameAccountSwitcher siempre presente (select si >1, texto si 1) + «+».
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Si la Battle.net tiene más de una cuenta de juego, /account muestra un
desplegable «Cargar datos de la cuenta» (WOW1, WOW2…). Al cambiar, fija la
cuenta activa en la sesión vía /api/auth/select-account y refresca → toda la
página (datos, personajes, servicios, compras) opera sobre la cuenta elegida
(la sesión.accountId manda). Con una sola cuenta no aparece (como hasta ahora).
components/GameAccountSwitcher.tsx (client) + getGameAccounts en la page.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Los fieldsets Datos básicos/Estado de la cuenta ahora replican el markup nuevo:
iconos Font Awesome por campo (account-ficon), .separate account-info,
«Nombre de la cuenta», correos enmascarados (maskEmail), PD | PV en una línea
(dp-color/vp-color), Token, «2FA: Desactivado» (fa-lock-open red-info), Cuenta
baneada con enlace «Consultar historial» (/ban-history), reclutada y amigos.
Se quita Créditos Battlepay. Clases account-ficon/account-info al tema; claves
i18n accountName/twofaLabel/twofaOff/banHistoryLink.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
accountTools en el orden del markup: cambiar contraseña, 2FA (email-icon,
label purple-info), transferir PD, comercio de PD, código promoción, token,
recluta, renombrar hermandad, votar, adquirir PD. «Cambiar correo» (que
tenemos y no está en la lista) al final; panel admin el último (solo admin).
Tool.labelClass para el morado del 2FA. Iconos nuevos del sprite: trade-pd-icon
(-299 -928) y color .purple-info (#b172ff) añadidos al tema. Claves i18n nuevas
(svc2FA/TransferDP/TradePD/Promo/RenameGuild/DPoints). Rutas nuevas → 404 aún.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- characterTools reordenado y completado según el markup del usuario (14 items):
...gold, Rastreador de misiones (/quest), transfer, Recuperar personaje
(/restore-character), Recuperar ítems (/restore-items), Tienda (/store),
Enviar regalo (/send-gift). Se retira battlepay del panel (queda en cabecera).
Claves i18n nuevas (svcQuest/RestoreChar/RestoreItems/StoreItems/SendGift).
- Sprite de iconos actualizado a account-icons.png (de uw-ryu) que incluye el
icono de regalo; CSS: .acc-icon → .png, send-gift-icon (-424 -553) y promo-icon
reubicado (-174 -928), verificado que las 25 posiciones restantes coinciden.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tercer panel plegable «HISTORIALES» (id account-history) con 4 herramientas:
Historial de PD y PV, transacciones, sanciones y seguridad (points-history/
trans-history/ban-history/security-history; iconos pointsh/dnth/banh/securityh).
Arranca CERRADO como los otros dos (el usuario lo abre al click). Prop
defaultOpen añadido a Panel (no usado aquí). Claves i18n en Account (es/en).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Añade una 404 con el markup del tema (main-page > middle-content >
body-content > title-content + body-box-content justified), dentro del
layout de [locale] (cabecera/vídeo/pie). Como el layout raíz es un segmento
dinámico ([locale]), se usa el patrón de next-intl: `[locale]/not-found.tsx`
+ catch-all `[locale]/[...rest]/page.tsx` que dispara notFound(). Bilingüe
(claves NotFound.title/message en es/en). Devuelve HTTP 404; las rutas
conocidas siguen 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La página de cuenta no se parecía a la de Django (oro descuadrado, info como
tabla, herramientas en grupos abiertos). Reescrita para replicar el
partials/my-account.html + isla AccountDashboard de Django:
- Información en DOS fieldsets (.account-fieldset): «Datos básicos» (cuentas,
correos, fecha de registro, IPs) y «Estado de la cuenta» (insignia de rango
nw-rank + PD/PV/créditos/token + baneo + reclutamiento). lib/account.ts
ampliado para traer reg_mail/email/joindate/last_ip/last_attempt_ip, estado
de baneo (account_banned) y nº de reclutados.
- Personajes en char-box con el markup exacto de Django: retrato flotante +
oro EN LÍNEA (monedas width=10). Antes las monedas salían a tamaño nativo
(descuadre) y apiladas.
- Herramientas en paneles plegables (acordeón) «OPCIONES DE CUENTA / OPCIONES
DE PERSONAJE» como Django (componente cliente AccountTools), con los
servicios ya implementados en Next y las etiquetas/descripciones exactas.
- globals.css: revertir otro reset del preflight de Tailwind — img{display:
block} apilaba verticalmente los iconos en línea (monedas); el tema los usa
inline como la web Django. Volvemos a img{display:inline}.
Verificado con Playwright (login real con cuenta de prueba + captura): info,
personajes con oro en línea y paneles que expanden con sus iconos; home sin
regresión por el cambio de img.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reescritura completa del frontend Next.js del sistema visual Tailwind
"simulado" al tema Django original (nw-ryu), para paridad pixel con la
web actual antes del cutover.
- Tema real: copia de static/nw-themes/nw-ryu + favicons a public/, el
layout carga el novavow-style.css real de ultimo (gana la cascada sobre
Tailwind) + Font Awesome.
- Shell replicando los partials Django: SiteHeader, Video, Social, Footer,
ServerClock; home con estructura real (main-page/middle-content/...).
- Helpers reutilizables: PageShell (main-page > middle-content > body-content
> title-content) y ServiceBox (title-box-content + back-to-account).
- Paginas migradas a clases reales del tema (fieldset/tool-button/char-box/
item-box/info-box-light/max-center-table/alert-message/botones reales),
eliminando el markup Tailwind (.nw-btn/.nw-card/.nw-input):
auth (login/register/recover/reset/select-account/activate),
cuenta + servicios de personaje (revive/unstuck/rename/customize/
change-race/change-faction/level-up/gold/transfer + pago Stripe),
ajustes (change-password/change-email/security-token),
comunidad (vote-points/recruit/battlepay), foro completo, y
admin (indice + 7 secciones + los Admin*Manager).
- Se conserva el bilingue (next-intl); claves nuevas en messages/es|en.json.
Verificado: typecheck + build OK; rutas protegidas 307->login; sin
MISSING_MESSAGE; cero Tailwind residual (solo .nw-tool-btn/.nw-page,
clases propias en globals.css).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Este build de AzerothCore usa account_access(AccountID, SecurityLevel), no el
clásico (id, gmlevel). Se corrigen las 4 consultas (admin.ts, forum-perm.ts,
forum.ts, admin-users.ts) -> isAdmin/forumIsModerator/gmlevel ahora funcionan.
- account page: enlace destacado al Panel de administración, visible solo si isAdmin.
- i18n es/en: Account.adminPanel.
Verificado: build OK; cuenta de prueba con SecurityLevel 3 (>= ADMIN_GMLEVEL) es admin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La página de cuenta solo mostraba datos y personajes, sin acceso a los servicios.
Añade un panel con 4 grupos:
- Servicios de personaje: renombrar, personalizar, cambiar raza/facción, subir nivel,
comprar oro, transferir.
- Utilidades: revivir, desatascar, token de seguridad.
- Ajustes de cuenta: cambiar contraseña, cambiar email.
- Comunidad: votar, reclutar, tienda (Battlepay).
i18n es/en en el namespace Account.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reutiliza la infraestructura Stripe (sin depender de SumUp/credenciales externas):
- lib/battlepay.ts: getPendingOrders/getPendingOrder/markOrderPaid sobre
acore_auth.battlepay_orders (PENDING->PAID, el mod-battlepay entrega en juego).
- fulfill.ts: rama battlepay (metadata.battlepay_reference -> markOrderPaid),
idempotente vía el claim atómico compartido con el webhook.
- API POST /api/battlepay/pay (valida orden PENDING de la cuenta, crea checkout).
- UI: /battlepay (lista + pagar) y /battlepay-success (entrega compartida),
enlace "Tienda" en la cabecera para usuarios logueados.
- i18n es/en: namespace Battlepay + Nav.store.
Verificado: build OK, /battlepay 307->login, API 401 sin sesión, success 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/forum.ts: getForumProfile (post_count, topic_count, joindate, gmlevel->status
admin/gm, y últimos 5 temas), portado de forum/views.py profile().
- Página /forum/user/[username] con contadores, antigüedad, rango y temas recientes.
- Los nombres de autor en la vista del tema enlazan al perfil.
- i18n es/en: Forum.memberSince, recentTopics, roleAdmin, roleGm.
Verificado: build OK, /forum/user 200.
Foro COMPLETO: índice, foro/tema, crear, responder, editar/borrar, moderación
(lock/pin/mover/borrar/restaurar), búsqueda, paginación, editor rico y perfiles.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/forum.ts: getTopic/getPosts/countPosts aceptan includeDeleted (mods ven lo
borrado); Post.deleted y TopicFull.deleted.
- API: PUT /api/forum/post restaura post (mod-only); moderate action 'restore'
restaura tema (usa includeDeleted al leer el tema).
- UI: posts borrados atenuados con badge y botón Restaurar (PostActions);
TopicModBar muestra banner + Restaurar tema cuando el tema está borrado.
- i18n es/en: Forum.restore, restoreTopic, deletedMark.
Verificado: build OK, PUT post y moderate restore 401 sin sesión, /forum 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- components/RichTextArea.tsx: barra de formato (negrita, cursiva, subrayado, tachado,
enlace, cita, lista, código) que envuelve la selección del textarea en HTML;
el HTML se sanea en el servidor con cleanPostHtml. Sin dependencias pesadas
(evita CKEditor y no rompe el pipeline de saneado).
- Cableado en ReplyForm, NewTopicForm y PostActions (edición).
- i18n es/en: namespace Editor.
Verificado: build OK, /forum y home 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- app/api/stripe/webhook: verifica firma (constructEventAsync) y en
checkout.session.completed registra la IP de Stripe (como Django) y ENTREGA
el servicio. Idempotente frente a la página de éxito.
- lib/stripe.ts: claimPaidCheckout ahora es ATÓMICO (UPDATE ... WHERE fulfilled=0
+ affectedRows) para evitar doble entrega entre webhook y página de éxito;
nuevo recordStripeIp.
- lib/fulfill.ts: fulfillCheckoutSession compartido (reclama + resuelve servicio
desde metadata.service + ejecuta). checkout guarda `service` en metadata.
- service-success: usa la entrega compartida y distingue entregado/ya-procesado/error.
- i18n: Paid.alreadyProcessed.
Requiere configurar STRIPE_WEBHOOK_SECRET en .env.local (endpoint apuntando a
/api/stripe/webhook, evento checkout.session.completed).
Verificado: build OK, webhook 400 sin firma / firma inválida.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Portado de home/views/recruit.py:
- lib/recruit-claim.ts: getRecruitRewards (con marca de reclamadas),
getRecruitedLevel80Count, y claimRecruitReward con las mismas validaciones que
Django (recompensa existe, no reclamada, amigos reclutados a nivel 80 suficientes,
anti-abuso por IP compartida), entrega por SOAP `.send items` (éxito si "Mail sent to")
y registro en home_claimedreward. Extra de seguridad: valida que el personaje sea de la cuenta.
- API POST /api/recruit/claim (401 sin sesión).
- UI /recruit (pública): rejilla de recompensas con estado reclamada/elegible/bloqueada,
selector de personaje y contador de amigos a nivel 80. Enlace en el footer.
- i18n es/en: namespace RecruitClaim + Nav.recruit.
Verificado: build OK, /recruit 200, claim 401 sin sesión.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/admin-recruit.ts: list/create/delete sobre home_recruitreward
(required_friends, reward_name, item_id, item_quantity, item_link, icon_class).
- API: POST /api/admin/recruit, DELETE /api/admin/recruit/[id] (403 sin admin).
- UI: /admin/recruit con AdminRecruitManager (alta ordenada por amigos requeridos),
enlace desde el índice del admin.
- i18n es/en (Admin): 10 claves nuevas.
Verificado: build OK, /admin/recruit 307→login, APIs 403 sin admin,
columnas confirmadas en la migración 0001_initial.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/admin-users.ts: searchAccounts (acore_auth.account + account_banned + gmlevel),
banAccount/unbanAccount por BD (autoritativo) con kick best-effort por SOAP.
Baneo días<=0 = permanente (unbandate=bandate, convención AzerothCore); desactiva
baneo activo previo antes de insertar el nuevo.
- API: GET /api/admin/users?q=, POST {accountId, action:ban|unban, reason, days} (403 sin admin).
- UI: /admin/users con AdminUsersManager (buscar por usuario/email, badges GM/online/baneado,
banear con motivo+días o desbanear), enlace desde el índice del admin.
- i18n es/en (Admin): 14 claves nuevas.
Verificado: build OK, /admin/users 307→login, APIs 403 sin admin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/admin-gold.ts: list/create/delete sobre home_goldprice (id, gold_amount, price).
- API: POST /api/admin/gold, DELETE /api/admin/gold/[id] (403 sin admin).
- UI: /admin/gold con AdminGoldManager (alta ordenada por cantidad + borrado),
enlace desde el índice del admin.
- i18n es/en (Admin): manageGold, gold, goldAmount, goldPrice, goldUnit, noGold.
Verificado: build OK, /admin/gold 307→login sin sesión, APIs 403 sin admin,
columnas confirmadas en la migración Django.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/forum.ts: countTopics/countPosts + offset/limit en getTopics/getPosts
(mismos límites que Django: FORUM_TOPICS_PER_PAGE=20, FORUM_POSTS_PER_PAGE=15).
- components/Pagination.tsx: paginación reutilizable (ventana ±2, primera/última,
prev/next) con enlaces ?page=N; no renderiza si solo hay una página.
- Foro y tema aceptan ?page=N (clamp al rango válido).
- El formulario de respuesta solo aparece en la última página; enlace a ella si no.
- i18n: namespace Common (prev/next/page) + replyOnLastPage.
Build OK. Sin datos de foro en acore_web (estado vacío correcto).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Portado desde forum/db.py + forum/permissions.py de Django:
- lib/forum-perm.ts: forumIsModerator (gmlevel >= FORUM_MOD_GMLEVEL) y
canEditPost (autor o moderador).
- Edición y borrado lógico de posts (autor o mod), respetando tema bloqueado,
con validación de longitud mínima (plainLength).
- Moderación de temas (solo mod): bloquear/desbloquear, fijar/no fijar, borrar.
- Búsqueda de temas por título/contenido (searchTopics/countSearchTopics)
con página /forum/search y buscador en el índice.
- API: PATCH/DELETE /api/forum/post, POST /api/forum/moderate (401/403 correctos).
- UI: PostActions, TopicModBar, ForumSearchBox; marca "editado" y "[Fijado]".
- i18n: 22 claves nuevas en es/en.
Verificado: build OK, /forum y /forum/search 200, APIs 401 sin sesión.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- globals.css: tokens @theme (nw-bg/panel/border/gold/text/muted), fondo con
degradado + brillo radial, y componentes reutilizables (.nw-btn, .nw-btn-ghost,
.nw-card, .nw-heading con oro degradado).
- Header: sticky con backdrop-blur y marca en oro degradado.
- Home: sección hero (nombre del server + tagline + CTAs Crear cuenta / Foros) y
estado del servidor + noticias en tarjetas (nw-card). Textos hero en catálogos.
Verificado: build OK, home renderiza el hero/tarjetas, los tokens nw compilan en el
CSS. Sistema visual base listo para aplicar al resto de páginas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>