Commit Graph

20 Commits

Author SHA1 Message Date
Inna 476e5dba87 sql: consolidar en web-next/sql y tirar lo del foro viejo
Repaso de la carpeta sql/ heredada. De sus 8 ficheros:

- forum_schema.sql y forum_views.sql: SE BORRAN. Son del foro anterior, que
  af707ad reemplazó por el port de FusionCMS (web-next/sql/forum_fusion.sql).
  El primero crea `forums`, tabla que ya no existe (el código usa forum_forums);
  el segundo añade un contador `views` que ni existe en prod ni usa el código.
  Quedan en el historial por si hicieran falta.
- achievement_points.sql, battlepay_sumup.sql, seed_news.sql y
  seed_recruit_rewards.sql: siguen siendo válidos y se mueven a web-next/sql/,
  que es la carpeta viva. El primero pasa a achievement_points_world.sql para no
  chocar con la copia de django_wow que usa la armería; cada uno avisa del otro.
- rename_home_prefix.sql y drop_home_securitytoken_authuser_fk.sql: se quedan en
  sql/ como registro de migraciones ya aplicadas.

Se añade battlepay_purchases.sql: la tabla la escribe el core y no estaba en
ningún SQL, así que no había forma de recrearla. Sus 61 filas son de pruebas
locales (2 cuentas, 127.0.0.1), sin datos de jugadores.

Verificado importando todo en BD limpias: seed_news deja 50 noticias,
seed_recruit_rewards 6, battlepay_sumup crea sus 2 tablas y
battlepay_purchases reproduce prod fila a fila con el mismo esquema.
2026-07-16 21:29:12 +00:00
Inna 9caa7b6c92 sql: quitar el prefijo home_ de los ficheros que quedaron atrás
El renombrado de las 41 tablas (cccc703) no llegó a los ficheros SQL ni a los
generadores, así que seguían creando y consultando tablas home_* que ya no
existen: aplicarlos hoy no reproducía el portal.

- 12 ficheros pasan a las tablas reales (store_item, stripelog, noticia, ...).
- Los 6 que llevaban el prefijo en el NOMBRE se renombran también; nada los
  referenciaba.
- gen_store_catalog.py leía FROM home_store_item de django_wow (roto) y
  db2_to_sql.py generaba home_item_data; corregidos, con sus comentarios.
- add_stripelog_service.sql usaba ADD COLUMN IF NOT EXISTS, que es de MariaDB:
  en MySQL 9.7 daba error de sintaxis y no se podía aplicar (la columna se había
  puesto a mano en prod). Ahora es un ADD COLUMN normal, como su hermano.

Se dejan intactos sql/rename_home_prefix.sql y
sql/drop_home_securitytoken_authuser_fk.sql: son migraciones ya aplicadas y los
nombres home_* son parte de su historia.

Verificado importando todo en BD limpias: los ALTER dejan stripelog y noticia
con el esquema idéntico a producción, y la consulta de gen_store_catalog.py
devuelve filas.
2026-07-16 21:23:29 +00:00
Inna 011cae27fb Armería: SQL de las tablas que necesita (django_wow)
Las tres tablas propias que consulta la armería (el resto son de AzerothCore):

- achievement_names: no tenía SQL en el repo. Se añade el volcado (1912 filas).
- achievement_points: solo existía sql/achievement_points.sql, que crea la tabla
  en acore_world para el top de /<realm>-players. La armería la lee de django_wow,
  así que se añade el volcado de esa copia.
- item_data: el fichero (y su generador) seguían creando `home_item_data`, nombre
  que dejó de existir al quitar el prefijo home_ en cccc703; se renombra a
  `item_data`, que es lo que consulta el código.

Verificado importando los tres en una BD limpia: coinciden fila a fila con
producción, con las tildes y los apóstrofos correctos.
2026-07-16 21:19:24 +00:00
Inna 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 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 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 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 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 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 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 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 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 fc98ddb624 send-gift: subir precios de 0.50 a 1.00 € (mínimo de cobro de SumUp)
SumUp no permite cobros por debajo de 1 €. Se actualizan las dos pociones
(0.50 -> 1.00) en el seed y en la BD de prod.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:57:23 +00:00
Inna 1a2d40ee9c Añadir /send-gift (Enviar regalo) pagado por SumUp
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>
2026-07-14 13:49:56 +00:00
Inna 03f75e189c web-next: /restore-character y /restore-items con todas las condiciones
- /restore-character: recupera personajes borrados por BD (como .character deleted
  restore). Condiciones: nivel>60, borrado <30 días, DK no si ya hay DK activo,
  nombre libre, cooldown 12h/personaje. Gratis. Tabla home_restorehistory.
- /restore-items: recupera ítems borrados vía SOAP del core (.item restore list /
  .item restore); el core aplica calidad/equipable/BoP/<7días. Cada ítem 100 PD
  (reembolso si SOAP falla), consulta con cooldown 8h/personaje + Turnstile.
  Tabla home_itemsearchhistory.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:09:11 +00:00
Inna eaae16e33a web-next: change-race/faction/level-up/gold por SumUp + rastreador de misiones
- /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>
2026-07-14 12:39:28 +00:00
Inna f7a376bd7c web-next: rename-character/customize-character por SumUp + renombrado de rutas
- /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>
2026-07-14 11:35:56 +00:00
Inna 6ca94c2803 web-next: selector de personaje coloreado + rediseño unstuck + fixes vote/account-info
- 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>
2026-07-14 10:55:43 +00:00
Inna 66d056c04b web-next: herramientas de PD (transferir, comercio, códigos de promoción)
- /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>
2026-07-14 09:27:11 +00:00