From 0cf7c6d279dfd8a713bee852ba241fbf17e8de2e Mon Sep 17 00:00:00 2001
From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com>
Date: Thu, 21 May 2026 22:37:31 -0500
Subject: [PATCH] up
---
pkg/models/servidor.go | 3 +
pkg/services/hostinger_service.go | 14 +++++
resources/views/servidor.html | 50 +++++++++++++++
resources/views/servidor_dashboard.html | 35 ++++++++++-
rest/controllers/agent_controller.go | 34 ++++++++++
rest/controllers/servidor_controller.go | 83 +++++++++++++++++++++++++
rest/routes/user.go | 1 +
7 files changed, 218 insertions(+), 2 deletions(-)
diff --git a/pkg/models/servidor.go b/pkg/models/servidor.go
index 4ed5a73..f2a8581 100755
--- a/pkg/models/servidor.go
+++ b/pkg/models/servidor.go
@@ -26,6 +26,9 @@ type Servidor struct {
AgentToken string `json:"agent_token" gorm:"column:agent_token;uniqueIndex"`
AgentLastSeen *time.Time `json:"agent_last_seen" gorm:"column:agent_last_seen"`
MetricasJson string `json:"metricas_json" gorm:"column:metricas_json;type:text"`
+ // Integración Hostinger
+ HostingerVpsID *int `json:"hostinger_vps_id" gorm:"column:hostinger_vps_id"`
+ HostingerState string `json:"hostinger_state" gorm:"column:hostinger_state"`
}
func (Servidor) TableName() string {
diff --git a/pkg/services/hostinger_service.go b/pkg/services/hostinger_service.go
index f94007c..6b35a77 100644
--- a/pkg/services/hostinger_service.go
+++ b/pkg/services/hostinger_service.go
@@ -191,6 +191,20 @@ type HostingerHosting struct {
// ─── Métodos de la API ───────────────────────────────────────────────────────
+// GetVPSByID obtiene una VM específica por su ID.
+func (c *HostingerClient) GetVPSByID(vmID int) (*HostingerVPS, error) {
+ path := fmt.Sprintf("/vps/v1/virtual-machines/%d", vmID)
+ body, err := c.getRaw(context.Background(), path)
+ if err != nil {
+ return nil, err
+ }
+ var vps HostingerVPS
+ if err := json.Unmarshal(body, &vps); err != nil {
+ return nil, fmt.Errorf("hostinger: no se pudo interpretar respuesta VPS: %w", err)
+ }
+ return &vps, nil
+}
+
// GetVPSList obtiene la lista de VPS/VMs.
func (c *HostingerClient) GetVPSList() ([]HostingerVPS, error) {
body, err := c.getRaw(context.Background(), "/vps/v1/virtual-machines")
diff --git a/resources/views/servidor.html b/resources/views/servidor.html
index fbc6cbf..264cf5f 100755
--- a/resources/views/servidor.html
+++ b/resources/views/servidor.html
@@ -79,6 +79,13 @@
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
+
@@ -262,6 +269,21 @@
+
+
+
+
+
+
+
+
@@ -344,6 +366,14 @@
+
+
+
+
+
+
+
@@ -386,6 +416,7 @@
deleteModal: false,
selectedItem: {},
errors: {},
+ syncCargando: {},
init() {
this.loadData();
this.loadProvServidor();
@@ -421,6 +452,8 @@
ultimo_ping: data.ultimo_ping,
prov_servidor_id: data.prov_servidor_id,
tipo_servidor_id: data.tipo_servidor_id,
+ hostinger_vps_id: data.hostinger_vps_id,
+ hostinger_state: data.hostinger_state,
created_at: data.CreatedAt,
}));
console.log('📊 Servidores procesados:', this.datos.length);
@@ -552,6 +585,22 @@
});
},
+ async syncHostinger(id) {
+ if (!id || this.syncCargando[id]) return;
+ this.syncCargando[id] = true;
+ try {
+ const r = await axios.post(`/app/servidor/${id}/sync-hostinger`);
+ if (r.data?.ok) {
+ await this.loadData();
+ alert('Sincronizado desde Hostinger correctamente.');
+ }
+ } catch (e) {
+ alert('Error al sincronizar: ' + (e.response?.data?.error || e.message));
+ } finally {
+ this.syncCargando[id] = false;
+ }
+ },
+
updateItem() {
if (this.selectedItem.prov_servidor_id) {
this.selectedItem.prov_servidor_id = parseInt(this.selectedItem.prov_servidor_id);
@@ -575,6 +624,7 @@
ultimo_ping: this.selectedItem.ultimo_ping,
prov_servidor_id: this.selectedItem.prov_servidor_id,
tipo_servidor_id: this.selectedItem.tipo_servidor_id,
+ hostinger_vps_id: this.selectedItem.hostinger_vps_id ? parseInt(this.selectedItem.hostinger_vps_id) : null,
};
this.setLoading(true);
diff --git a/resources/views/servidor_dashboard.html b/resources/views/servidor_dashboard.html
index 1dd9e1b..68a5c2f 100644
--- a/resources/views/servidor_dashboard.html
+++ b/resources/views/servidor_dashboard.html
@@ -46,8 +46,11 @@
● En línea
● Fuera
○ Sin agente
-
-
+
+
+
+
@@ -153,6 +156,14 @@
Agente
+
@@ -484,6 +495,7 @@
agentTokenCargando: false,
cargando: true,
cargandoConexiones: false,
+ syncCargando: {},
pingCargando: {},
pingResultado: {},
@@ -560,6 +572,25 @@
}
},
+ async syncHostinger(servidor) {
+ const id = servidor.ID;
+ if (!id || this.syncCargando[id]) return;
+ this.syncCargando[id] = true;
+ try {
+ const r = await fetch(`/app/servidor/${id}/sync-hostinger`, { method: 'POST' });
+ const data = await r.json();
+ if (data.ok) {
+ await this.cargarServidores();
+ } else {
+ alert('Error: ' + (data.error || 'No se pudo sincronizar'));
+ }
+ } catch (e) {
+ alert('Error al sincronizar con Hostinger: ' + e.message);
+ } finally {
+ this.syncCargando[id] = false;
+ }
+ },
+
agentOnline(servidor) {
if (!servidor?.agent_last_seen) return false;
const last = new Date(servidor.agent_last_seen);
diff --git a/rest/controllers/agent_controller.go b/rest/controllers/agent_controller.go
index 87bbcb7..16d1918 100644
--- a/rest/controllers/agent_controller.go
+++ b/rest/controllers/agent_controller.go
@@ -3,6 +3,8 @@ package controllers
import (
"crypto/rand"
"encoding/hex"
+ "encoding/json"
+ "fmt"
"time"
"github.com/gofiber/fiber/v2"
@@ -35,9 +37,41 @@ func AgentHeartbeat(c *fiber.Ctx) error {
now := time.Now()
updates := map[string]interface{}{
"agent_last_seen": &now,
+ "ultimo_ping": now.Format(time.RFC3339),
}
if req.Metricas != "" {
updates["metricas_json"] = req.Metricas
+ // Actualizar campos planos desde las métricas del agente (la verdad viene del agente)
+ type agentRAM struct {
+ TotalGB float64 `json:"total_gb"`
+ }
+ type agentCPU struct {
+ Nucleos int `json:"nucleos"`
+ }
+ type agentDisco struct {
+ TotalGB float64 `json:"total_gb"`
+ }
+ type agentPayload struct {
+ OS string `json:"os"`
+ RAM agentRAM `json:"ram"`
+ CPU agentCPU `json:"cpu"`
+ Disco agentDisco `json:"disco"`
+ }
+ var m agentPayload
+ if jsonErr := json.Unmarshal([]byte(req.Metricas), &m); jsonErr == nil {
+ if m.OS != "" {
+ updates["so"] = m.OS
+ }
+ if m.RAM.TotalGB > 0 {
+ updates["ram"] = fmt.Sprintf("%.0f GB", m.RAM.TotalGB)
+ }
+ if m.CPU.Nucleos > 0 {
+ updates["nucleos"] = fmt.Sprintf("%d", m.CPU.Nucleos)
+ }
+ if m.Disco.TotalGB > 0 {
+ updates["disco"] = fmt.Sprintf("%.0f GB", m.Disco.TotalGB)
+ }
+ }
}
if err := db.Model(&servidor).Updates(updates).Error; err != nil {
diff --git a/rest/controllers/servidor_controller.go b/rest/controllers/servidor_controller.go
index 43383da..bfe4787 100755
--- a/rest/controllers/servidor_controller.go
+++ b/rest/controllers/servidor_controller.go
@@ -5,6 +5,7 @@ import (
"math"
"net"
"strconv"
+ "strings"
"time"
"github.com/gofiber/fiber/v2"
@@ -159,6 +160,11 @@ func UpdateServidor(c *fiber.Ctx) error {
m.ID = uint(uid)
+ // Cargar el registro actual para detectar cambios que deben propagarse a Hostinger
+ var actual models.Servidor
+ app.Http.Database.DB.First(&actual, uid)
+ nombreAnterior := actual.Nombre
+
if err := models.UpdateServidor(m); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": "Error al actualizar el servidor",
@@ -166,9 +172,86 @@ func UpdateServidor(c *fiber.Ctx) error {
})
}
+ // Sincronizar hostname en Hostinger si está vinculado y el nombre cambió
+ if actual.HostingerVpsID != nil && m.Nombre != "" && m.Nombre != nombreAnterior {
+ if client, hErr := hostingerClient(); hErr == nil {
+ _ = client.SetVPSHostname(*actual.HostingerVpsID, m.Nombre)
+ }
+ }
+
return c.Status(fiber.StatusOK).JSON(m)
}
+// SyncServidorFromHostinger sincroniza los datos del VPS de Hostinger hacia el servidor local.
+// Actualiza: ip_servidor, ram, nucleos, disco, hostinger_state, vencimiento (si hay suscripción).
+// La verdad de las specs físicas siempre viene de Hostinger.
+func SyncServidorFromHostinger(c *fiber.Ctx) error {
+ id := c.Params("id")
+ db := app.Http.Database.DB
+
+ var servidor models.Servidor
+ if err := db.First(&servidor, id).Error; err != nil {
+ return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "servidor no encontrado"})
+ }
+ if servidor.HostingerVpsID == nil {
+ return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "el servidor no tiene hostinger_vps_id configurado"})
+ }
+
+ client, err := hostingerClient()
+ if err != nil {
+ return c.Status(fiber.StatusFailedDependency).JSON(fiber.Map{"error": "No se encontró configuración activa de Hostinger"})
+ }
+
+ vps, err := client.GetVPSByID(*servidor.HostingerVpsID)
+ if err != nil {
+ return c.Status(fiber.StatusUnprocessableEntity).JSON(fiber.Map{"error": err.Error()})
+ }
+
+ updates := map[string]interface{}{
+ "hostinger_state": vps.State,
+ }
+
+ // IP principal (primer IPv4)
+ if len(vps.IPV4) > 0 && vps.IPV4[0].Address != "" {
+ updates["ip_servidor"] = vps.IPV4[0].Address
+ }
+
+ // RAM: Hostinger devuelve en MB → convertir a GB
+ if vps.RAMBytes > 0 {
+ gbRam := float64(vps.RAMBytes) / 1024.0
+ updates["ram"] = fmt.Sprintf("%.0f GB", gbRam)
+ }
+
+ // CPU cores
+ if vps.CPU > 0 {
+ updates["nucleos"] = fmt.Sprintf("%d", vps.CPU)
+ }
+
+ // Disco: Hostinger devuelve en GB directamente
+ if vps.DiskBytes > 0 {
+ updates["disco"] = fmt.Sprintf("%d GB", vps.DiskBytes)
+ }
+
+ // Vencimiento: buscar suscripción cuyo nombre contenga el hostname del VPS
+ if subs, sErr := client.GetOrders(); sErr == nil {
+ for _, sub := range subs {
+ if sub.ExpiresAt != "" && len(sub.ExpiresAt) >= 10 &&
+ (strings.Contains(strings.ToLower(sub.Name), strings.ToLower(vps.Hostname)) ||
+ strings.Contains(strings.ToLower(sub.Name), "vps")) {
+ updates["vencimiento"] = sub.ExpiresAt[:10]
+ break
+ }
+ }
+ }
+
+ if err := db.Model(&servidor).Updates(updates).Error; err != nil {
+ return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo guardar"})
+ }
+
+ db.First(&servidor, id)
+ return c.JSON(fiber.Map{"ok": true, "servidor": servidor})
+}
+
func DeleteServidor(c *fiber.Ctx) error {
uid, err := strconv.ParseUint(c.Params("id"), 10, 32)
if err != nil {
diff --git a/rest/routes/user.go b/rest/routes/user.go
index 99bb5d0..46a4fc3 100755
--- a/rest/routes/user.go
+++ b/rest/routes/user.go
@@ -80,6 +80,7 @@ func UserRoutes(app fiber.Router) {
protected.Get("/servidor-dashboard/:id", controllers.GetServidorDashboard)
protected.Get("/conx-ping/:id", controllers.PingConexion)
protected.Post("/servidor/:id/agent-token", controllers.GenerateAgentToken)
+ protected.Post("/servidor/:id/sync-hostinger", controllers.SyncServidorFromHostinger)
// Rutas de proveedores de servidor
protected.Get("/prov_servidor", middlewares.MenuMiddleware, controllers.ProvServidor)