diff --git a/web-next/app/page.tsx b/web-next/app/page.tsx index 90521b2..73fbfb4 100644 --- a/web-next/app/page.tsx +++ b/web-next/app/page.tsx @@ -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('/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, } } diff --git a/web-next/lib/api.ts b/web-next/lib/api.ts deleted file mode 100644 index 6ef12ff..0000000 --- a/web-next/lib/api.ts +++ /dev/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(path: string, init?: RequestInit): Promise { - 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 -} - -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' -} diff --git a/web-next/lib/db.ts b/web-next/lib/db.ts new file mode 100644 index 0000000..9f47ce9 --- /dev/null +++ b/web-next/lib/db.ts @@ -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 } +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', +} diff --git a/web-next/lib/home.ts b/web-next/lib/home.ts new file mode 100644 index 0000000..ee01b4d --- /dev/null +++ b/web-next/lib/home.ts @@ -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 { + const [rows] = await db(DB.default).query( + '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 { + try { + const [rows] = await db(DB.characters).query( + '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 { + 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 { + const [rows] = await db(DB.default).query( + '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', + } +} diff --git a/web-next/package-lock.json b/web-next/package-lock.json index d83704f..f6a249e 100644 --- a/web-next/package-lock.json +++ b/web-next/package-lock.json @@ -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", diff --git a/web-next/package.json b/web-next/package.json index 77d1913..57a3585 100644 --- a/web-next/package.json +++ b/web-next/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "mysql2": "^3.22.6", "next": "16.2.10", "react": "19.2.4", "react-dom": "19.2.4"