feat: Document new service monitoring and quote API, and implement quote API endpoint with enhanced health checks.

This commit is contained in:
Marco Gallegos
2025-12-17 19:57:14 -06:00
parent e8d8a58c46
commit 419b8a283b
2 changed files with 181 additions and 38 deletions

129
README.md
View File

@@ -1,22 +1,133 @@
# Soul:23 coming soon page # Landing Page y Monitor de Servicios "Soul:23"
A responsive landing page built with Bootstrap 4 that displays a countdown and a notification form. Este repositorio contiene el código para una landing page de "próximamente" junto con un sistema de monitoreo de servicios integrado. La aplicación está construida con Node.js y Express, y es capaz de servir contenido estático y exponer una API con varios endpoints funcionales.
## Local Installation ## Características Principales
* **Landing Page Responsiva**: Una página de "próximamente" con un contador regresivo, construida con Bootstrap 4.
* **API de Frases**: Un endpoint que entrega una frase aleatoria en cada solicitud.
* **Monitor de Salud de Servicios**: Un endpoint avanzado que ejecuta un script de Python para verificar el estado de múltiples sitios y servicios web, categorizados en internos, de empresa y externos.
* **Servidor Flexible**: Configurado para servir archivos estáticos, HTML dinámico y endpoints JSON.
* **Contenerización**: Listo para desplegarse con Docker.
## Estructura del Proyecto
```
/
├─── data/
│ ├─── quotes.json
│ └─── sites.json
├─── htmls/
│ └─── telegram.html
├─── scripts/
│ └─── health_checker.py
├─── css/
├─── js/
├─── img/
├─── .gitignore
├─── docker-compose.yml
├─── Dockerfile
├─── index.html
├─── package.json
├─── server.js
└─── README.md
```
## Instalación y Ejecución Local
Para ejecutar el proyecto en un entorno de desarrollo local, sigue estos pasos:
1. **Clona el repositorio**:
```bash
git clone <url-del-repositorio>
cd soul23_placeholder
```
2. **Instala las dependencias**:
Asegúrate de tener Node.js (v18 o superior) y npm instalados.
```bash ```bash
npm install npm install
```
3. **Inicia el servidor**:
```bash
npm start npm start
``` ```
El servidor se iniciará en `http://localhost:3001` por defecto.
The Express server serves all assets from the root and exposes a `/healthchecker` endpoint with the health script as `text/plain`, ready for operators to download with `curl`. ## Documentación Detallada de Componentes
## Countdown and Form ### `server.js`
The time component reads the `data-date` attribute in `#countdown-timer`. Change it to any valid date: Es el núcleo de la aplicación. Configura un servidor Express que gestiona todas las rutas y la lógica principal.
```html #### Endpoints de la API
<div id="countdown-timer" data-date="January 17, 2025 03:24:00">
* **`GET /day-quote`**
* **Descripción**: Devuelve una frase motivacional aleatoria.
* **Lógica**: Lee el arreglo de frases de `data/quotes.json`, selecciona una al azar y la sirve.
* **Respuesta de Ejemplo**:
```json
{
"phrase": "El universo trabaja mientras tú sigues avanzando."
}
``` ```
If you prefer to program it with JavaScript, reassign the `countDownDate` variable inside `js/countdown.js` before the interval starts. * **`GET /healthchecker`**
* **Descripción**: Ejecuta un script de monitoreo en Python (`scripts/health_checker.py`) y devuelve un reporte detallado del estado de los servicios definidos en `data/sites.json`.
* **Lógica**: Utiliza `child_process.exec` de Node.js para invocar el script de Python. Captura la salida JSON del script y la sirve como respuesta. Es ideal para tableros de monitoreo o webhooks.
* **Respuesta de Ejemplo (truncada)**:
```json
{
"timestamp": "2025-12-18T01:34:53.516Z",
"internos": {
"vps_soul23_status": "🟢 OK (VPS Reachable)",
"coolify_status": 200
},
"empresa": {
"vanity_web_status": 200
},
"externos": {
"openai_status": "🟡 Advertencia (Partial System Outage)"
},
"execution_time_seconds": 17.83
}
```
* **`GET /telegram`**
* **Descripción**: Sirve una página HTML (`htmls/telegram.html`) diseñada para gestionar redirecciones a la aplicación de Telegram, adaptándose a la plataforma del usuario.
* **`GET /health`**
* **Descripción**: Un endpoint de salud básico que realiza una prueba de ping a una IP predefinida para una verificación rápida de conectividad.
* **`GET /time-server`**
* **Descripción**: Proporciona la fecha y hora del servidor en múltiples formatos (ISO, Unix, legible).
### Directorio `data/`
Este directorio centraliza todos los datos que la aplicación necesita para funcionar.
* **`quotes.json`**: Un archivo JSON que contiene un único arreglo de strings llamado `phrases`. Cada string es una frase que puede ser servida por el endpoint `/day-quote`.
* **`sites.json`**: El archivo de configuración para el monitor de salud. Contiene tres objetos principales: `internos`, `sitios_empresa` y `externos`. Cada objeto es un diccionario donde la clave es el nombre del servicio y el valor es su URL.
### Directorio `scripts/`
Contiene la lógica de negocio más compleja en forma de scripts.
* **`health_checker.py`**:
* **Lenguaje**: Python 3.
* **Dependencias**: `requests`.
* **Lógica**:
1. Carga la lista de sitios desde `../data/sites.json`.
2. Itera sobre cada servicio y realiza diferentes tipos de verificaciones:
* **Verificación simple**: Para la mayoría de los sitios, comprueba si la URL devuelve un código de estado HTTP 200.
* **Endpoints de Salud Específicos**: Para servicios como `vps_soul23` y `formbricks`, realiza peticiones a sus endpoints `/health` y analiza la respuesta JSON para un estado más detallado.
* **APIs de StatusPage**: Para servicios como OpenAI y Cloudflare, consulta su API de `statuspage.io` para obtener el estado oficial del servicio.
3. Consolida todos los resultados en un único objeto JSON.
4. Si la variable de entorno `WEBHOOK_URLS` está definida (con una o más URLs separadas por comas), envía el resultado JSON a cada webhook.
5. Imprime el resultado JSON en la salida estándar para que `server.js` pueda capturarlo.
### Archivos de Contenerización
* **`Dockerfile`**: Contiene las instrucciones para construir una imagen de Docker de la aplicación. Utiliza una imagen base de Node.js, copia los archivos del proyecto, instala las dependencias de `npm` y define el comando para iniciar el servidor.
* **`docker-compose.yml`**: Facilita la ejecución de la aplicación en un entorno local de Docker, gestionando la construcción de la imagen y la configuración de red.

View File

@@ -1,44 +1,63 @@
const express = require("express"); const express = require("express");
const path = require("path"); const path = require("path");
const fs = require("fs"); const fs = require("fs");
const { exec } = require("child_process"); const { exec } = require("child_process");
const app = express(); const app = express();
const port = process.env.PORT || 3001; const port = process.env.PORT || 3001;
const rootDir = path.join(__dirname); const rootDir = path.join(__dirname);
// Serve static assets from the project root // --- Middleware ---
// Sirve los archivos estáticos (HTML, CSS, JS) desde el directorio raíz del proyecto.
// 'index.html' se sirve como el archivo por defecto.
app.use(express.static(rootDir, { index: "index.html" })); app.use(express.static(rootDir, { index: "index.html" }));
// Health checker should always return the raw script with text/plain
// --- Rutas de la API ---
/**
* @route GET /healthchecker
* @description Ejecuta un script de Python para un chequeo de salud avanzado.
* Invoca el script `scripts/health_checker.py`, que monitorea múltiples servicios
* y devuelve un reporte detallado en formato JSON.
* @returns {object} Un objeto JSON con el estado de los servicios monitoreados.
* @throws 500 - Si el script de Python falla o su salida no es un JSON válido.
*/
app.get("/healthchecker", (req, res) => { app.get("/healthchecker", (req, res) => {
const pythonScriptPath = path.join(rootDir, "scripts", "health_checker.py"); const pythonScriptPath = path.join(rootDir, "scripts", "health_checker.py");
exec(`python3 ${pythonScriptPath}`, (error, stdout, stderr) => { exec(`python3 ${pythonScriptPath}`, (error, stdout, stderr) => {
if (error) { if (error) {
console.error(`Error executing health_checker.py: ${stderr}`); console.error(`Error al ejecutar health_checker.py: ${stderr}`);
return res.status(500).json({ error: "Failed to execute health checker" }); return res.status(500).json({ error: "No se pudo ejecutar el chequeo de salud" });
} }
try { try {
const healthData = JSON.parse(stdout); const healthData = JSON.parse(stdout);
res.status(200).json(healthData); res.status(200).json(healthData);
} catch (parseErr) { } catch (parseErr) {
console.error(`Error parsing health checker output: ${parseErr}`); console.error(`Error al parsear la salida del chequeo de salud: ${parseErr}`);
res.status(500).json({ error: "Failed to parse health checker output" }); res.status(500).json({ error: "La respuesta del chequeo de salud no es un JSON válido" });
} }
}); });
}); });
// Magic link para redirigir a la app de Telegram según plataforma /**
* @route GET /telegram
* @description Sirve una página HTML para la redirección a Telegram.
* @returns {file} El archivo `htmls/telegram.html`.
*/
app.get("/telegram", (req, res) => { app.get("/telegram", (req, res) => {
res.sendFile(path.join(rootDir, "htmls", "telegram.html")); res.sendFile(path.join(rootDir, "htmls", "telegram.html"));
}); });
// Standard health check endpoint for monitoring with VPS ping /**
* @route GET /health
* @description Realiza un chequeo de salud simple haciendo ping a una IP.
* @returns {object} Un objeto JSON con el estado de la conectividad.
*/
app.get("/health", (req, res) => { app.get("/health", (req, res) => {
const vpsIp = "31.97.41.188"; const vpsIp = "31.97.41.188"; // IP a verificar
// Ping with count 1 and timeout 1 second // Ejecuta un ping con un solo paquete y un timeout de 1 segundo.
exec(`ping -c 1 -W 1 ${vpsIp}`, (error, stdout, stderr) => { exec(`ping -c 1 -W 1 ${vpsIp}`, (error, stdout, stderr) => {
const isAlive = !error; const isAlive = !error;
res.status(200).json({ res.status(200).json({
@@ -48,57 +67,70 @@ app.get("/health", (req, res) => {
vps_ping: { vps_ping: {
target: vpsIp, target: vpsIp,
alive: isAlive, alive: isAlive,
output: isAlive ? "VPS Reachable" : "VPS Unreachable", output: isAlive ? "VPS Alcanzable" : "VPS Inalcanzable",
}, },
}, },
}); });
}); });
}); });
// Endpoint to get a random phrase /**
* @route GET /day-quote
* @description Devuelve una frase aleatoria del archivo de citas.
* @returns {object} Un objeto JSON con una única clave "phrase".
* @throws 500 - Si no se puede leer o parsear el archivo `data/quotes.json`.
*/
app.get("/day-quote", (req, res) => { app.get("/day-quote", (req, res) => {
fs.readFile(path.join(rootDir, "data", "quotes.json"), "utf8", (err, data) => { fs.readFile(path.join(rootDir, "data", "quotes.json"), "utf8", (err, data) => {
if (err) { if (err) {
console.error("Error reading quotes file:", err); console.error("Error al leer el archivo de frases:", err);
return res.status(500).json({ error: "Could not read quotes file" }); return res.status(500).json({ error: "No se pudo leer el archivo de frases" });
} }
try { try {
const quotes = JSON.parse(data); const quotes = JSON.parse(data);
const phrases = quotes.phrases; const phrases = quotes.phrases;
if (!phrases || phrases.length === 0) { if (!phrases || phrases.length === 0) {
return res.status(500).json({ error: "No phrases found" }); return res.status(500).json({ error: "No se encontraron frases en el archivo" });
} }
const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)]; const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)];
res.status(200).json({ phrase: randomPhrase }); res.status(200).json({ phrase: randomPhrase });
} catch (parseErr) { } catch (parseErr) {
console.error("Error parsing quotes file:", parseErr); console.error("Error al parsear el archivo de frases:", parseErr);
return res.status(500).json({ error: "Could not parse quotes file" }); return res.status(500).json({ error: "No se pudo parsear el archivo de frases" });
} }
}); });
}); });
// Endpoint to get the current time in multiple formats /**
* @route GET /time-server
* @description Proporciona la hora actual del servidor en diferentes formatos.
* @returns {object} Un objeto JSON con la hora en formato ISO, Unix y legible.
*/
app.get("/time-server", (req, res) => { app.get("/time-server", (req, res) => {
const now = new Date(); const now = new Date();
const timezone = "America/Monterrey"; const timezone = "America/Monterrey";
res.status(200).json({ res.status(200).json({
// Full UTC ISO 8601 string for machines
utc_iso: now.toISOString(), utc_iso: now.toISOString(),
// Unix timestamp in seconds for machines
unixtime: Math.floor(now.getTime() / 1000), unixtime: Math.floor(now.getTime() / 1000),
// Human-readable local time for debugging
datetime_human: now.toLocaleString("en-US", { timeZone: timezone }), datetime_human: now.toLocaleString("en-US", { timeZone: timezone }),
// Timezone identifier
timezone: timezone, timezone: timezone,
}); });
}); });
// Fallback to index.html for other routes (optional) /**
* @route GET *
* @description Ruta "catch-all" que sirve la página principal.
* Útil para Single Page Applications (SPAs) donde el enrutamiento se maneja
* en el lado del cliente.
* @returns {file} El archivo `index.html`.
*/
app.get("*", (req, res) => { app.get("*", (req, res) => {
res.sendFile(path.join(rootDir, "index.html")); res.sendFile(path.join(rootDir, "index.html"));
}); });
// --- Inicio del Servidor ---
app.listen(port, () => { app.listen(port, () => {
console.log(`Soul:23 server listening on port ${port}`); console.log(`Servidor Soul:23 escuchando en el puerto ${port}`);
}); });