restore_items_view ahora responde JSON cuando la petición es AJAX (X-Requested-With):
GET con character_name -> {items}; POST recover_id -> {success, message}. Se conserva
el flujo legacy (redirect+messages) para no-AJAX. RestoreItemsForm.tsx: paso 1 elegir
personaje y buscar ítems borrados (SOAP), paso 2 elegir ítem y recuperarlo; refresca la
lista tras recuperar. La vista pasa character_names por json_script.
Verificado: check OK, render de la isla, redirige a login sin sesión.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
restore_character_view pasa de redirect+messages a JSON en POST: guid -> SOAP
.char del restore -> {success, message, redirect}. RestoreCharacterForm.tsx
(select de personajes borrados + botón) hace fetch y en éxito redirige a my-account.
La vista pasa character_data por json_script; se conserva la rama sin personajes.
Verificado: check OK, ambas ramas renderizan, redirige a login sin sesión.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ban_history_view construye ban_rows (baneos de cuenta + personaje combinados, con
estado Activo/Inactivo y origen Cuenta/Personaje) y los pasa por json_script a la
tabla genérica HistoryTable. Sustituye la tabla Django con doble bucle.
Verificado: check OK, render de la isla con datos, redirige a login sin sesión.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tabla genérica reutilizable HistoryTable (columnas por data-columns, filas por
json_script) + entry history_table:
- security_history: intentos de conexión (username/status/ip/fecha). Se elimina la
caja "actividad" duplicada. Las vistas pasan login_attempts_data serializable.
- trans_history: historial de transacciones (StripeLog) con transactions_data.
Datos embebidos con json_script (sin endpoint extra). Verificado: check OK, ambas
plantillas renderizan la isla con datos.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
vote_points_view ya devolvía JSON en POST. VotePanel.tsx renderiza los sitios de
votación (nombre, icono, PV) y cada botón abre el sitio en otra pestaña y hace POST
del voto -> acredita PV o muestra el cooldown que devuelve el backend. La vista pasa
vote_sites_data (serializable) por json_script. Se elimina el JS jQuery y el panel
duplicado; la sección informativa se queda en Django.
Verificado: check OK, render de la isla correcto, redirige a login sin sesión.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rename_guild_view ya devolvía JSON (session_id de Stripe). GuildRenameForm.tsx:
select de hermandades (donde el usuario es GM) + nuevo nombre/confirmar + contraseña
+ token -> Stripe Checkout. La vista pasa guild_names por json_script; se conserva
la rama {% if no_guild_leader_message %} en Django. Se elimina el JS jQuery muerto.
Verificado: check OK, render de ambas ramas correcto, redirige a login sin sesión.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- revive/unstuck (acciones SOAP gratuitas): reutilizan CharacterPurchaseForm; se
hace el precio opcional (botón sin "por X €" cuando no hay precio). El componente
ya maneja la respuesta sin session_id mostrando el mensaje.
- gold: variante GoldForm (personaje + cantidad de oro -> Stripe). La vista pasa
ahora character_names y gold_options (serializables) por json_script.
- transfer: variante TransferForm (personaje + cuenta destino + token -> Stripe).
La vista pasa character_names por json_script.
Verificado: check OK, plantillas renderizan las islas, las 4 páginas redirigen a
login sin sesión.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
De las ~508 líneas de partials/my-account.html, solo la parte superior es dinámica
(datos de cuenta, estado, puntos, token, personajes); el resto son paneles de
navegación estáticos (enlaces), que se quedan en Django.
- API: account_me_api -> /api/account/me/ (protegido por sesión, 401 si no hay):
user_info, account_status, dp/vp, créditos Battlepay, token de seguridad y
personajes. Reutiliza get_account_characters/get_account_status/formatear_fecha.
Resiliente si acore no está disponible.
- React: AccountDashboard.tsx (fieldsets de datos + estado con contador de baneo en
vivo + rejilla de personajes). Entry my_account.
- partials/my-account.html: los 2 fieldsets + la rejilla de personajes + el script
del contador se sustituyen por #account-dashboard-app; los paneles de utilidades
e historiales siguen en Django.
Verificado: check OK, /api/account/me/ 401 sin sesión, my-account redirige a login.
(El render con datos reales requiere sesión + BD AzerothCore.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Backend: helper verify_turnstile en _base (verifica el token contra
challenges.cloudflare.com/siteverify; fail-closed ante error de red; si no hay
secreto configurado, no bloquea). Se exige en el POST de login_view,
register_view y recover_account_view -> rechazo con mensaje si falla.
- settings: TURNSTILE_SITE_KEY / TURNSTILE_SECRET_KEY desde .env (el secreto NO
se commitea). La clave de sitio se expone a las plantillas por el context
processor (TURNSTILE_SITE_KEY).
- Frontend: hook useTurnstile (carga el script de Cloudflare una vez y renderiza
el widget, devuelve el token). LoginForm/RegisterForm/RecoverForm incluyen el
widget, mandan cf-turnstile-response y deshabilitan el submit hasta tener token.
El sitekey llega por data-sitekey en el div de montaje.
Verificado: sin token los 3 POST se rechazan; siteverify acepta la clave secreta
(error invalid-input-response con token falso, no invalid-input-secret).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
recover_account_view era un stub que solo renderizaba (los formularios no tenían
backend y el HTML arrastraba un reCAPTCHA de otro dominio). Ahora es funcional:
Backend:
- Modelo PasswordReset (token+email+expiración 1h) + migración 0014.
- recover_account_view maneja POST JSON con 3 tipos, todo por email (bnet), con
respuestas genéricas anti-enumeración:
* password: crea token y envía enlace de reset (emails/password_reset.html).
* accountname: envía las cuentas de juego ligadas (emails/account_names.html).
* activation: reenvía el enlace de activación pendiente (emails/activation.html).
- reset_password_view: GET valida el token (isla), POST re-deriva el verifier
SRP6 v2 de la cuenta bnet (bnet.bnet_make_registration) y lo actualiza; marca el
token usado. Ruta 'reset-password'.
- Resiliente si la BD de cuentas (AzerothCore) no está disponible: mensaje limpio,
nunca 500.
Frontend (islas Vite): RecoverForm.tsx (selector de tipo + email) y
ResetPassword.tsx (nueva contraseña con token). Se elimina el reCAPTCHA roto; se
puede añadir uno propio si se aportan claves.
Verificado en producción: los 3 flujos devuelven éxito genérico; reset valida
token y contraseñas; token válido no provoca 500.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El selector de cuenta de juego (bnet con varias) pasa a isla React y la selección
se hace vía GET (a petición), no POST:
- Nuevo endpoint GET /api/account/select/?account_id=<id> (account_select_api):
idempotente y solo permite elegir cuentas ligadas a la bnet en sesión; fija la
cuenta en sesión y devuelve {success, redirect}. 401 si no hay sesión bnet.
- select_account_view: se elimina el manejo de POST (queda solo la lógica GET:
redirect si no hay bnet, autoseleccionar si hay una, render si hay varias).
- SelectAccount.tsx lee las cuentas embebidas con json_script y, al pulsar, hace
fetch GET y redirige. Nueva entry 'select_account'.
- select_account.html: el <form> POST se sustituye por la isla + json_script.
Verificado: check OK, endpoint 401 sin sesión, /select-account/ redirige a login.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Plantillas: los textos hardcodeados "WotLK 3.3.5a" pasan a "3.4.3" (title,
description, keywords, og:* en head.html; encabezado de la portada).
- Expansión: se centraliza en ServerSelection (helper expansion_from_gamebuild
+ propiedad .expansion) y se reconoce WoW Classic: WotLK Classic 3.4.x
(~44832–51536, p.ej. 3.4.3=51505) además de 3.3.5a (12340), y Cataclysm
Classic 4.4.x además de 4.3.4 (15595). Se elimina la lógica duplicada de los
4 sitios (api, context_processor, pages x2) que solo mapeaban 12340->WotLK.
El ServerSelection de Nova WoW queda en build 51505 (3.4.3) -> etiqueta WotLK,
puerto 1119 (bnetserver).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Segunda isla de la home: la tabla de estado (nombre, online, login, hora,
dirección) pasa de plantilla Django a componente React.
- API: home_status_api -> /api/home/status/ (server_name, address, expansion,
online_characters, status). Reutiliza check_server_status y
get_online_characters_count de views/pages.
- React: ServerStatus.tsx (fetch + reloj en vivo con setInterval, mismas clases
CSS). El entry home.tsx ahora monta dos islas: #home-news-app y #home-status-app.
- partials/noticias.html: la <table> de estado se sustituye por el contenedor.
Verificado: check OK, ambos endpoints 200, la home monta las dos islas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Arquitectura elegida: islas React/TSX montadas sobre las plantillas Django
existentes (django-vite), alimentadas por endpoints JSON bajo /api/. El backend,
las URLs y la auth por sesión se mantienen; se migra sección a sección.
Fase 0 (tooling):
- frontend/ con Vite + React 18 + TypeScript. Build a static/dist con manifest.
- django-vite 3.1 en INSTALLED_APPS + DJANGO_VITE (dev_mode=DEBUG,
static_url_prefix='dist'). STATIC_URL pasa a '/static/' (absoluto) para que los
assets no choquen con el <base href> de la plantilla. WhiteNoise sirve el bundle.
- .gitignore: frontend/node_modules y static/dist (artefactos de build).
Fase 1 (piloto: lista de noticias de la portada):
- API: home/views/api.py -> home_news_api (JsonResponse); ruta /api/home/news/
montada en home/api_urls.py fuera de i18n_patterns.
- React: NewsList.tsx hace fetch de la API y renderiza las noticias (mismas clases
CSS que el HTML original). Entry src/entries/home.tsx monta en #home-news-app.
- partials/noticias.html: el bucle {% for noticia %} se reemplaza por el contenedor
de montaje + {% vite_asset %}.
Verificado en producción: manage.py check OK, /api/home/news/ 200, la home incluye
el mount y el <script> del bundle, y el bundle se sirve (200).
Build en deploy: cd frontend && npm run build && collectstatic (ver frontend/README.md).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- settings: ALLOWED_HOSTS y CSRF_TRUSTED_ORIGINS a nightspire.gg /
www.nightspire.gg (+ IP del servidor). Se retira el antiguo novawow.com.
- WhiteNoise para servir estáticos desde gunicorn (el reverse proxy Caddy
corre como usuario 'caddy' y no puede leer /root): middleware +
STORAGES con CompressedStaticFilesStorage. Añadido a requirements.txt.
- home: get_online_characters_count() ahora tolera que la BD de personajes
(AzerothCore) no exista/esté vacía y devuelve 0 en vez de provocar un 500
en la portada (mismo criterio que el context_processor get_server_info).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El módulo monolítico (~3200 líneas) se convierte en un paquete cuyo
__init__.py re-exporta todas las vistas, de modo que `views.X`,
`from home.views import X` y la cadena 'home.views.not_found' (handler404)
siguen funcionando sin tocar urls.py.
Módulos por sección: _base (imports comunes + require_paid_stripe), auth,
pages, account, recruit, characters, store, points, guild, history,
players y payments. Cada submódulo hereda los imports con
`from ._base import *`; las dependencias entre secciones se resuelven con
imports explícitos (guild->auth, recruit/history->account).
Se eliminan dos definiciones duplicadas que quedaban muertas al cargar:
- get_character_image (primera copia, idéntica a la efectiva)
- store_novawow_success_view (stub sin SOAP, pisado por la versión real)
Validado con `manage.py check` (0 issues), resolución de las 100 rutas y
un chequeo estático de nombres no definidos por módulo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>