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)