Full-stack Next.js: la home lee MySQL directamente (sin Django)

Pivote a Next.js full-stack (Django saldrá del todo en el cutover). Next accede a
las mismas BD MySQL que Django:

- lib/db.ts: pools mysql2 (django_wow + acore_*), singleton en globalThis, creds
  en web-next/.env.local (gitignored, copiadas del .env de Django).
- lib/home.ts: getNoticias (home_noticia), getServerStatus (home_serverselection +
  online desde acore_characters + TCP check + expansión por gamebuild, incl. Classic).
- app/page.tsx: home SSR que llama a lib/home (ya NO a la API Django). Se elimina
  lib/api.ts.

Verificado: con el servicio Django PARADO, la home de Next sigue sirviendo 200 con
datos reales -> lee MySQL directo, Django fuera del circuito.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 22:23:12 +00:00
parent 478a3bb203
commit c36b543184
6 changed files with 238 additions and 46 deletions
-35
View File
@@ -1,35 +0,0 @@
// Cliente de la API JSON de Django.
// - En SSR (servidor Next) llamamos directo a gunicorn por 127.0.0.1:8001.
// - En el navegador usamos rutas relativas (/api/...) que Caddy enruta a Django.
const SERVER_BASE = process.env.DJANGO_API_BASE || 'http://127.0.0.1:8001'
function base(): string {
return typeof window === 'undefined' ? SERVER_BASE : ''
}
export async function apiGet<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${base()}${path}`, {
// Datos siempre frescos (SSR por petición); ajústese por endpoint si procede.
cache: 'no-store',
headers: { Accept: 'application/json' },
...init,
})
if (!res.ok) throw new Error(`API ${path} -> HTTP ${res.status}`)
return res.json() as Promise<T>
}
export interface Noticia {
id: number
titulo: string
fecha: string | null
contenido: string
enlace: string | null
}
export interface ServerStatus {
server_name: string | null
address: string | null
expansion: string
online_characters: number
status: 'Online' | 'Offline'
}
+30
View File
@@ -0,0 +1,30 @@
import mysql from 'mysql2/promise'
// Pools de conexión a las mismas BD MySQL que usaba Django (django_wow + acore_*).
// Se guardan en globalThis para no crear pools nuevos en cada hot-reload de dev.
const g = globalThis as unknown as { _nwPools?: Record<string, mysql.Pool> }
g._nwPools ??= {}
export function db(database: string): mysql.Pool {
if (!g._nwPools![database]) {
g._nwPools![database] = mysql.createPool({
host: process.env.DB_HOST || '127.0.0.1',
port: Number(process.env.DB_PORT || 3306),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database,
waitForConnections: true,
connectionLimit: 5,
namedPlaceholders: true,
})
}
return g._nwPools![database]
}
export const DB = {
default: process.env.DB_NAME_DEFAULT || 'django_wow',
auth: process.env.DB_NAME_AUTH || 'acore_auth',
characters: process.env.DB_NAME_CHARACTERS || 'acore_characters',
world: process.env.DB_NAME_WORLD || 'acore_world',
web: process.env.DB_NAME_WEB || 'acore_web',
}
+86
View File
@@ -0,0 +1,86 @@
import net from 'node:net'
import type { RowDataPacket } from 'mysql2'
import { db, DB } from './db'
export interface Noticia {
id: number
titulo: string
fecha: string | null
contenido: string
enlace: string | null
}
export interface ServerStatus {
server_name: string | null
address: string | null
expansion: string
online_characters: number
status: 'Online' | 'Offline'
}
/** Etiqueta de expansión por build del cliente (incluye WoW Classic; Nova WoW = 3.4.3 = 54261). */
export function expansionFromGamebuild(gamebuild: number): string {
if (gamebuild === 12340) return 'WotLK'
if (gamebuild >= 44000 && gamebuild <= 54261) return 'WotLK Classic'
if (gamebuild === 15595) return 'Cataclysm'
if (gamebuild >= 54262 && gamebuild <= 60000) return 'Cataclysm Classic'
return 'Unknown'
}
export async function getNoticias(limit = 50): Promise<Noticia[]> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, titulo, contenido, fecha_publicacion, enlace FROM home_noticia ORDER BY fecha_publicacion DESC LIMIT ?',
[limit],
)
return rows.map((r) => ({
id: r.id,
titulo: r.titulo,
fecha: r.fecha_publicacion ? new Date(r.fecha_publicacion).toISOString() : null,
contenido: r.contenido,
enlace: r.enlace || null,
}))
}
async function getOnlineCount(): Promise<number> {
try {
const [rows] = await db(DB.characters).query<RowDataPacket[]>(
'SELECT COUNT(*) AS n FROM characters WHERE online = 1',
)
return Number(rows[0]?.n ?? 0)
} catch {
// La BD de personajes (AzerothCore) puede no estar poblada todavía.
return 0
}
}
function checkServerStatus(host: string, port: number, timeout = 1000): Promise<boolean> {
return new Promise((resolve) => {
if (!host || host === 'N/A') return resolve(false)
const socket = new net.Socket()
const done = (ok: boolean) => {
socket.destroy()
resolve(ok)
}
socket.setTimeout(timeout)
socket.once('connect', () => done(true))
socket.once('timeout', () => done(false))
socket.once('error', () => done(false))
socket.connect(port, host)
})
}
export async function getServerStatus(): Promise<ServerStatus | null> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT name, address, port, gamebuild FROM home_serverselection LIMIT 1',
)
const s = rows[0]
if (!s) return null
const [online, up] = await Promise.all([getOnlineCount(), checkServerStatus(s.address, s.port)])
return {
server_name: s.name,
address: s.address,
expansion: expansionFromGamebuild(s.gamebuild),
online_characters: online,
status: up ? 'Online' : 'Offline',
}
}