- «Top de logros» ahora suma los PUNTOS de logros (no el nº): JOIN a
acore_world.achievement_points (id→points), poblada desde el DB2 Achievement
del build 3.4.3.54261 (wago.tools). Columna «Puntos de logros». Seed en
sql/achievement_points.sql (1912 logros).
- Fix: el logro «Nivel 80» es el id 13 (logros de nivel WotLK van de +10:
6=Nv10,7=Nv20…12=Nv70,13=Nv80), no el 20. «Primeros del reino» ahora se
puebla correctamente.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extiende la ruta dinámica [realm] para servir también /<slug>-players (slug
del reino en realmlist). PlayersBoard (server) consulta acore_characters:
- Personajes creados por clase (conteo por clase y facción) + totales.
- Top de muertes con honor (characters.totalKills/todayKills, LIMIT 10).
- Top de logros (nº de logros por personaje vía character_achievement;
los «puntos» no existen en la BD del servidor, vienen del DBC del cliente).
- Primeros del reino - Nivel 80 (primer personaje por fecha del logro 20 =
Nivel 80, overall/por clase/por raza; vacío si aún no hay nivel 80).
Imágenes reales del tema (nw-classes/nw-races/nw-icons). lib/players.ts con
los mapas de clase/raza/facción. RealmInfo extraído a su componente. Enlace
JUGADORES de la cabecera ahora dinámico (/<slug>-players), como REINO.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El menú de la cabecera enlazaba a /novawow-realm fijo. Ahora Header (server)
calcula el slug desde realmlist (getRealmName + realmSlug) y lo pasa a
SiteHeader como realmHref → el enlace REINO apunta a /trinity-realm (o al
reino que haya). Acompaña a la ruta dinámica [realm].
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La página del reino pasa de /novawow-realm (fija) a una ruta dinámica
app/[locale]/[realm] cuyo slug es <nombre-del-reino>-realm, con el nombre
tomado de acore_auth.realmlist (LIMIT 1). Ej.: realm «Trinity» → /trinity-realm.
realmSlug() normaliza el nombre (minúsculas, sin espacios). Si el slug no
coincide con el reino actual → notFound(). Convive con el catch-all [...rest]
y las rutas estáticas mantienen prioridad (verificado: /login, /account OK;
/foo y /novawow-realm → 404).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ruta con el markup del tema: título «Reino - <NOMBRE>» (nombre tomado de
acore_auth.realmlist con LIMIT 1, por si hay varios reinos; lib/realm.ts
getRealmName) y dos columnas realm-information-half con Rates y Horarios.
force-dynamic + generateMetadata con el nombre del reino.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El override box-sizing:content-box incluía `select`, agrandándolo a 60px de
alto y 320px de ancho. El default del navegador para <select> es border-box
(40px), igual que la web original sin Tailwind. Se quita `select` del override
(los inputs de texto siguen en content-box, que sí es su default). Corrige el
campo de /download-client y todos los selects del sitio (recover, admin,
servicios de personaje).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ruta con el markup del tema + lógica interactiva (componente cliente
ClientDownload): selector de idioma (esES/esMX/enUS/enGB) que muestra el
selector de SO y la descripción del idioma; al elegir SO «Mac OS Catalina»
aparece la guía de CrossOver; el botón DESCARGAR abre el magnet link
correspondiente (btih por idioma + trackers comunes) o muestra error si
falta elegir. Clases del tema (game-download-select/g-download-button).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Guarda el script de siembra usado para poblar django_wow.home_noticia con las
50 noticias reales, para reimportar en el futuro. Cabecera con instrucciones
y aviso sobre el DELETE (vacía la tabla antes de insertar).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
La home ya lee las noticias de home_noticia; se sembraron las 50 noticias
reales del sitio en la BD. Ajustes de render: fecha en formato español largo
(«21 de junio de 2026») y contenido en <p> (con <br> y enlaces), como el
markup original.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El botón Personalizar del banner ahora despliega un panel con checkboxes por
categoría (Necesarias fija+desactivada, Analíticas, Marketing) y «Guardar
preferencias», tal como el markup del usuario. Al guardar, escribe la cookie
uw_cookie_consent con la selección granular y sincroniza con Zaraz. Categorías
alineadas a las del panel (Necesarias/Analíticas/Marketing); ConsentStatus y
acceptAll/rejectAll usan las mismas. Estilos del panel en globals.css.
Verificado: Personalizar abre el panel, marcar Analíticas + Guardar deja la
cookie {necessary,analytics:true,marketing:false} y /cookies lo refleja.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Banner real `uw-cookie-content` (Rechazar/Personalizar/Aceptar todas) fijo
abajo y site-wide (montado en el layout, componente CookieBanner). Aparece
en la primera visita, guarda la decisión en la cookie uw_cookie_consent (1
año) y se sincroniza con Cloudflare Zaraz si está presente
(zaraz.consent.setAll / .modal). Estilos en globals.css.
- «Personalizar» abre el modal granular de Zaraz (o /cookies si no hay Zaraz).
- window.UWCookies {showBanner, revokeConsent, acceptAll, rejectAll}.
- Página /cookies: panel «Tu estado de consentimiento» (ConsentStatus) que
muestra los datos EN VIVO de la API de Zaraz (zaraz.consent.getAll()) por
propósito; si Zaraz no está, cae a la preferencia guardada por categoría.
Verificado con navegador: el banner aparece, «Aceptar todas» lo oculta y
escribe la cookie, y el panel de /cookies refleja el estado.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Crea la ruta de Declaración de Cookies con el markup del tema y las 5 tablas
de cookies (uw-cookie-table, estilizadas en globals.css) + estilos de listas
y botones (btn-uw). Los enlaces internos .php se cablean a las rutas Next
(/privacy-policy, /contact-us).
Sistema de consentimiento = Cloudflare Zaraz (elección del usuario):
components/CookieConsent.tsx expone window.UWCookies.showBanner()
(zaraz.consent.modal=true) y revokeConsent() (zaraz.consent.setAll(false)),
que Cloudflare activa cuando el dominio pasa por su proxy con Zaraz Consent;
degrada con aviso si Zaraz no está presente. El footer ya enlazaba aquí.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Crea la ruta Contáctanos con el markup del tema y el texto completo.
El footer ya enlazaba aquí. Título por metadata; devuelve 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Crea la ruta de Aviso legal con el markup del tema y el texto completo.
El footer ya enlazaba aquí. Título por metadata; devuelve 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Crea la ruta de Política de Reembolso con el markup del tema y el texto
completo. El footer ya enlazaba aquí. Título por metadata; devuelve 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Crea la ruta de Política de Privacidad con el markup del tema y el texto
completo. El footer ya enlazaba aquí. Título por metadata; devuelve 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Crea la ruta de Términos y Condiciones con el markup del tema
(main-page > ... > body-box-content) y el texto legal completo. El footer
ya enlazaba aquí. Título por metadata; devuelve 200.
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>
En el recuadro de estado de la home, la fila del reino mostraba el conteo de
personajes online (0). Ahora muestra Online/Offline según un TCP check al
worldserver (puerto 8085), con color verde/rojo como la fila Login.
lib/home.ts añade world_status (checkServerStatus address:8085).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Antes era un tool-button suelto a ancho completo (podía desbordarse). Ahora
es una celda más de la rejilla de 2 columnas del panel «Opciones de cuenta»
(solo admins), con el mismo contorno/ancho que el resto (Token de seguridad,
etc.). Utilidades y Herramientas se mantiene igual.
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>
El tema nw-ryu se diseño para los defaults del navegador (la web Django no
usa Tailwind). El preflight de Tailwind rompia el layout de los formularios:
- box-sizing:border-box hacia que la height fija de los inputs (24px)
incluyera el padding → campos finos/aplastados. Se vuelve a content-box
en los inputs/select del tema (.admin-form conserva border-box).
- table{border-collapse:collapse} anulaba el border-spacing de las tablas
del tema (middle-center-table, max-center-table...) → filas pegadas sin
separacion. Se vuelve a border-collapse:separate.
Reglas sin @layer → ganan al preflight (que va en @layer base). Verificado
por captura: los campos de /login ahora coinciden pixel a pixel con Django.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Turnstile: quitar clases Tailwind del wrapper (my-3/flex/justify-center)
→ estilo inline neutral centrado, como la web Django.
- login/register/recover: el boton ya NO arranca deshabilitado (gris) por
falta de captcha; se ve activo como en Django. El captcha lo sigue
validando el servidor (devuelve captchaFailed). Se quita el SITE_KEY
cliente ya innecesario.
- reset-password: pagina que habia quedado sin migrar (usaba nw-btn/
nw-input/mx-auto/text-amber) → PageShell + middle-center-table + inputs
planos + recover-button + alert-message, igual que el resto del flujo.
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>
Se veían como enlaces azules sin estilo. Ahora:
- /admin: rejilla de tarjetas (icono + etiqueta + flecha) con hover dorado.
- /account (servicios): chips con borde, fondo de panel y hover dorado en vez de
enlaces subrayados azules.
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>
- lib/data/zone-names.json: ZONE_NAMES (9156 zonas) exportado de zone_definitions.py.
- lib/character-info.ts: getZoneName, getClassCss/getClassName, getRaceName y
getCharacterImage (raza/clase/género -> /races/big-*.webp, con validación de
compatibilidad clase-raza y fallback unknown), portado de account.py.
- lib/account.ts: Character enriquecido (race, class, zone, imageUrl, class/raceName).
- account page: tarjetas con imagen de raza, color de clase WoW, nivel, raza/clase,
📍 zona y oro. globals.css: colores de clase (.class-*).
- public/races: 20 imágenes de raza del tema + unknown.webp.
Verificado: build OK, imágenes 200, /account 307→login. El JSON de zonas se usa
solo en servidor (no infla el bundle cliente).
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>
- Copia los logos del tema original (nw-logo -> public/brand/logo.webp,
nw-long-logo -> logo-long.webp).
- Header: logo + nombre. Home hero: logo largo con glow dorado.
- Footer: logo + copyright + enlaces (inicio/foros/votar/registro), i18n.
Verificado: logos se sirven (200), home 200.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reemplazo masivo de los estilos sueltos repetidos por las clases del sistema visual
en ~20 componentes/páginas (auth, cuenta, servicios de personaje, foro, admin, voto):
- inputs -> .nw-input, botones primarios -> .nw-btn, tarjetas -> .nw-card.
Cohesión visual completa con la home y la cabecera.
Verificado: build OK, páginas clave 200/redirect correctos, clases aplicadas.
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>
- lib/vote.ts: getVoteSites (home_votesite), registerVote (cooldown 12h30 vía
home_votelog, acredita PV en home_api_points, registra el voto).
- Route /api/vote (POST {url}, guard sesión). Página /vote-points (VotePanel:
abre el sitio + registra el voto, muestra PV/cooldown). Enlace "Votar" en cabecera.
- Cierra el ciclo con el admin de sitios de voto.
Verificado: /vote-points 200, POST 401 sin sesión; lógica probada: voto -> +PV,
segundo voto -> cooldown 12h29m.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/admin.ts: isAdmin(session) por allowlist ADMIN_EMAILS (normalizada) o gmlevel
de AzerothCore (account_access, resiliente si no existe). ADMIN_EMAILS/ADMIN_GMLEVEL
en .env.local.
- lib/admin-news.ts: listNews / createNews / deleteNews (home_noticia).
- Routes /api/admin/news (POST, guard isAdmin) y /api/admin/news/[id] (DELETE).
- Páginas /admin (dashboard) y /admin/news (protegidas: no-admin -> redirect).
AdminNewsManager (cliente: crear + listar + borrar).
Verificado: /admin sin permiso redirige, APIs 403; el pipeline crear noticia -> se
renderiza en la home (SSR) funciona. Turnstile bloquea el login por curl (correcto).
Pendiente admin: precios de servicios, foros, sitios de voto, usuarios.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- lib/forum-sanitize.ts: cleanPostHtml con sanitize-html (misma allowlist que
forum/sanitize.py de nh3: formato básico + a/img/tablas, rel nofollow, esquemas
http/https/mailto). Verificado XSS-safe (script/onclick/javascript: eliminados).
- lib/forum-write.ts: createTopic (tema + primer post), createPost (respuesta +
updated_at), forumIsPostable / topicIsReplyable.
- Routes /api/forum/topic y /api/forum/reply (guard de sesión; identidad = cuenta de
juego: poster=username, poster_id=accountId). Componentes NewTopicForm y ReplyForm
(clientes). Se muestran solo si hay sesión; el tema cerrado no admite respuesta.
Verificado: escritura 401 sin sesión, saneado correcto. Pendiente: editar/borrar,
moderación (fijar/cerrar/mover), búsqueda, editor enriquecido.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>