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:
@@ -1,16 +1,13 @@
|
||||
import { apiGet, type Noticia, type ServerStatus } from '@/lib/api'
|
||||
import { getNoticias, getServerStatus, type Noticia, type ServerStatus } from '@/lib/home'
|
||||
|
||||
// SSR por petición: los datos se obtienen en el servidor Next desde la API Django.
|
||||
// (En Next 16 fetch no se cachea por defecto; force-dynamic lo deja explícito.)
|
||||
// SSR por petición: los datos se leen DIRECTAMENTE de MySQL desde el servidor Next
|
||||
// (sin Django). force-dynamic para renderizar por petición.
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
async function getData(): Promise<{ noticias: Noticia[]; status: ServerStatus | null }> {
|
||||
const [news, status] = await Promise.allSettled([
|
||||
apiGet<{ noticias: Noticia[] }>('/api/home/news/'),
|
||||
apiGet<ServerStatus>('/api/home/status/'),
|
||||
])
|
||||
const [news, status] = await Promise.allSettled([getNoticias(), getServerStatus()])
|
||||
return {
|
||||
noticias: news.status === 'fulfilled' ? news.value.noticias : [],
|
||||
noticias: news.status === 'fulfilled' ? news.value : [],
|
||||
status: status.status === 'fulfilled' ? status.value : null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
@@ -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',
|
||||
}
|
||||
}
|
||||
Generated
+116
-3
@@ -8,6 +8,7 @@
|
||||
"name": "web-next",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"mysql2": "^3.22.6",
|
||||
"next": "16.2.10",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
@@ -1496,7 +1497,6 @@
|
||||
"version": "20.19.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
|
||||
"integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
@@ -2348,6 +2348,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/aws-ssl-profiles": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
|
||||
"integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
|
||||
"engines": {
|
||||
"node": ">= 6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/axe-core": {
|
||||
"version": "4.12.1",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.12.1.tgz",
|
||||
@@ -2698,6 +2706,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/denque": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
|
||||
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
|
||||
"engines": {
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
@@ -3525,6 +3541,14 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/generate-function": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
|
||||
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
|
||||
"dependencies": {
|
||||
"is-property": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/generator-function": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
|
||||
@@ -3769,6 +3793,21 @@
|
||||
"hermes-estree": "0.25.1"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4084,6 +4123,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-property": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
|
||||
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
|
||||
},
|
||||
"node_modules/is-regex": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
|
||||
@@ -4653,6 +4697,11 @@
|
||||
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -4674,6 +4723,20 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/lru.min": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
|
||||
"integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=1.30.0",
|
||||
"node": ">=8.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wellwelwel"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.21",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
|
||||
@@ -4741,6 +4804,38 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/mysql2": {
|
||||
"version": "3.22.6",
|
||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.6.tgz",
|
||||
"integrity": "sha512-fPKmeDGUzvFP7bMD5SASlJ5zIgvCC4hbanTmhbUlEmhyrY1hR4Hi3xLOWgTd3luYjLVifx6uGvMNJ2m/LlqEpg==",
|
||||
"dependencies": {
|
||||
"aws-ssl-profiles": "^1.1.2",
|
||||
"denque": "^2.1.0",
|
||||
"generate-function": "^2.3.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"long": "^5.3.2",
|
||||
"lru.min": "^1.1.4",
|
||||
"named-placeholders": "^1.1.6",
|
||||
"sql-escaper": "^1.4.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/named-placeholders": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
|
||||
"integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
|
||||
"dependencies": {
|
||||
"lru.min": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.16",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
|
||||
@@ -5368,6 +5463,11 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
@@ -5585,6 +5685,20 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sql-escaper": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.5.1.tgz",
|
||||
"integrity": "sha512-4toX5E1fQbBrpfXidaHnF0669nkAdETeIPTs2SUjxxD7RRIs9ICG4gtpmfc68JCEKehsdwLFqBu9VlQqZ1P1gg==",
|
||||
"engines": {
|
||||
"bun": ">=1.0.0",
|
||||
"deno": ">=2.0.0",
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/stable-hash": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz",
|
||||
@@ -6039,8 +6153,7 @@
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
"version": "1.12.2",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"mysql2": "^3.22.6",
|
||||
"next": "16.2.10",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
|
||||
Reference in New Issue
Block a user