579396c34a
Marca, dominios y correos: UltimoWoW/Nova WoW -> NightSpire, cualquier URL de esos dominios -> https://www.nightspire.gg y los correos -> consultas@nightspire.gg. 98 sustituciones en 16 ficheros de textos de usuario (messages/, app/, components/). Assets: logo largo de cabecera y vídeo sustituidos por los de NightSpire (el logo nuevo es 214x28, igual que el viejo, así que no hace falta tocar el CSS). Redes: apuntan a las cuentas de NightSpire, tanto en el pie de la web como en el de los correos (que seguían enlazando a las de NovaWoW desde el porte del diseño). Cookie de sesión novawow_session -> nightspire_session. Cierra la sesión de todos los conectados una vez; se asume ahora, recién hecho el cutover. Ficheros del tema renombrados: novawow-style.css -> nightspire-style.css y novawow-main-logo-transparent.webp -> nightspire-main-logo-transparent.webp, para que la marca vieja no quede ni en las rutas. El foro externo (foro.ultimowow.com) pasa a ser el nuestro: <Link> a /forum, que respeta el idioma y no abre pestaña nueva. NO se tocan y es a propósito: - Los comentarios que nombran AzerothCore: describen el comportamiento REAL del core (límite de 12 ítems por correo, convención de baneos, SOAP...). Cambiarlos a NightSpire los volvería falsos. Además AzerothCore no aparece en la web: las 14 menciones son todas comentarios de código. - El regex de brandify() sigue buscando los nombres VIEJOS: son los que hay escritos en las noticias de la BD. - lib/changelog.ts REPO='Inna/NovaWoW': es la ruta real del repo en Gitea. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
102 lines
3.2 KiB
TypeScript
102 lines
3.2 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; el nuestro = 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 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 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',
|
|
}
|
|
}
|