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>
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>
Componente genérico CharacterPurchaseForm (selector de personaje + botón con precio)
+ entry character_purchase, configurable por data-* en la plantilla. Al enviar, la
vista devuelve {session_id, stripe_public_key} y se redirige al Checkout de Stripe
(helper stripe.ts que carga Stripe.js). Reutilizable por rename/customize/race/
faction/level/gold/transfer... cambiando solo los data-* y la lista de personajes
(json_script). Primer uso: rename_character.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
change_email_view ya devolvía JSON en POST (sesión requerida). ChangeEmailForm.tsx
replica el form (contraseña actual con mostrar-ocultar, correo actual/nuevo/confirmar,
token) y hace fetch form-urlencoded + CSRF a change-email; mensaje HTML. Sustituye el
<form> + change_email_response.js por #change-email-app.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ambas vistas ya devolvían JSON en POST y exigen sesión, así que sin cambio de backend:
- ChangePasswordForm.tsx: 4 campos (actual/nueva/confirmar/token) con mostrar-ocultar;
POST form-urlencoded + CSRF a change_password; mensaje HTML; si el backend responde
redirect:true (logout por seguridad), redirige a login.
- SecurityTokenForm.tsx: muestra la fecha del token y un botón que solicita uno nuevo;
POST -> actualiza la fecha con token_date de la respuesta.
- Plantillas: los <form> + scripts jQuery se sustituyen por las islas
(#change-password-app / #security-token-app) con data-csrf/data-url. El texto
informativo y los enlaces se quedan en Django.
Verificado: check OK; ambas vistas siguen redirigiendo a login/index 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>
register_view ya devolvía JSON en POST, así que sin cambio de backend:
RegisterForm.tsx replica register.js (password/conf con mostrar-ocultar, email/conf,
reclutador opcional, checkbox de términos que habilita el botón) y hace fetch
form-urlencoded + CSRF a {% url 'register' %}. El mensaje de éxito llega como HTML
(dangerouslySetInnerHTML); los de error como texto en rojo.
- partials/register.html: el <form> + register.js se sustituyen por #register-app
(data-csrf/data-register-url) + {% vite_asset %}. Info-box y rama "ya conectado"
siguen en Django.
- Nueva entry 'register' en vite.config.ts.
Verificado: check OK, la página monta la isla, POST con datos inválidos devuelve
el JSON de validación correcto.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
El login se migra a una isla React. login_view ya devolvía JSON en POST, así que
el backend no cambia: LoginForm.tsx hace fetch (form-urlencoded + CSRF) al mismo
{% url 'login' %} y replica el comportamiento del antiguo login_response.js
(mostrar/ocultar contraseña, estados del botón, éxito -> redirige a my-account,
alert/locked, errores transitorios).
- partials/login.html: el <form> + el script jQuery se sustituyen por
#login-app (con data-csrf/data-login-url/data-success-url) + {% vite_asset %}.
La rama "ya conectado" y los enlaces (recover, create-account) siguen en Django.
- CSRF vía data-csrf="{{ csrf_token }}" (fija la cookie y da el token); el fetch
lo manda como X-CSRFToken y csrfmiddlewaretoken. El navegador adjunta Referer.
- Nueva entry 'login' en vite.config.ts.
Verificado: check OK, la página monta la isla, POST devuelve JSON correcto.
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>