Files
NightSpire/web-next/lib/home.ts
T
Inna 86c9b01c76 Noticias multiidioma (ES/EN) con fallback
home_noticia gana columnas titulo_en/contenido_en (sql/add_news_en.sql). getNoticias(locale)
usa la versión EN si existe, si no cae al español (título y contenido independientes); la fecha
también se formatea por idioma. El panel de admin permite escribir la traducción al crear y EDITAR
noticias existentes (nuevo PUT /api/admin/news/[id], updateNews). Claves Admin nuevas (título/
contenido EN, editar/guardar). Probado: /en/ muestra EN, /es/ ES; PUT guardado (403 sin sesión).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 19:14:07 +00:00

102 lines
3.3 KiB
TypeScript

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'
world_status: 'Online' | 'Offline'
}
// Puerto del worldserver (AzerothCore) para el estado del reino.
const WORLD_PORT = 8085
/** 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'
}
/**
* Noticias localizadas. Con `locale='en'` usa `titulo_en`/`contenido_en` si
* existen; si están vacías, cae a la versión en español (título y contenido de
* forma independiente). El texto original (titulo/contenido) siempre es ES.
*/
export async function getNoticias(locale = 'es', limit = 50): Promise<Noticia[]> {
const [rows] = await db(DB.default).query<RowDataPacket[]>(
'SELECT id, titulo, titulo_en, contenido, contenido_en, fecha_publicacion, enlace FROM home_noticia ORDER BY fecha_publicacion DESC LIMIT ?',
[limit],
)
const en = locale === 'en'
return rows.map((r) => ({
id: r.id,
titulo: en && r.titulo_en ? r.titulo_en : r.titulo,
fecha: r.fecha_publicacion ? new Date(r.fecha_publicacion).toISOString() : null,
contenido: en && r.contenido_en ? r.contenido_en : 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, worldUp] = await Promise.all([
getOnlineCount(),
checkServerStatus(s.address, s.port),
checkServerStatus(s.address, WORLD_PORT),
])
return {
server_name: s.name,
address: s.address,
expansion: expansionFromGamebuild(s.gamebuild),
online_characters: online,
status: up ? 'Online' : 'Offline',
world_status: worldUp ? 'Online' : 'Offline',
}
}