package controllers import ( "crypto/rand" "encoding/hex" "encoding/json" "fmt" "time" "github.com/gofiber/fiber/v2" "github.com/sujit-baniya/fiber-boilerplate/app" "github.com/sujit-baniya/fiber-boilerplate/pkg/models" "github.com/sujit-baniya/fiber-boilerplate/pkg/services" ) // AgentHeartbeat recibe métricas del agente instalado en un servidor. // Ruta pública: POST /agent/heartbeat — autenticada por token en JSON body. func AgentHeartbeat(c *fiber.Ctx) error { type HeartbeatRequest struct { Token string `json:"token"` Metricas string `json:"metricas"` ReniceAction string `json:"renice_action"` } var req HeartbeatRequest if err := c.BodyParser(&req); err != nil { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"ok": false, "error": "body inválido"}) } if req.Token == "" { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"ok": false, "error": "token requerido"}) } db := app.Http.Database.DB var servidor models.Servidor if err := db.Where("agent_token = ?", req.Token).First(&servidor).Error; err != nil { return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"ok": false, "error": "token inválido"}) } 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"` } var rawMap map[string]any if jsonErr := json.Unmarshal([]byte(req.Metricas), &rawMap); jsonErr == nil { if os, _ := rawMap["os"].(string); os != "" { updates["so"] = os } if ram, _ := rawMap["ram"].(map[string]any); ram != nil { if total, _ := ram["total_gb"].(float64); total > 0 { updates["ram"] = fmt.Sprintf("%.0f GB", total) } } if cpu, _ := rawMap["cpu"].(map[string]any); cpu != nil { if n, _ := cpu["nucleos"].(float64); n > 0 { updates["nucleos"] = fmt.Sprintf("%d", int(n)) } } // Compatibilidad: nuevo formato (discos array) y viejo (disco objeto) discos, hasArray := rawMap["discos"].([]any) if hasArray { var maxTotal float64 for _, item := range discos { if d, _ := item.(map[string]any); d != nil { if t, _ := d["total_gb"].(float64); t > maxTotal { maxTotal = t } } } if maxTotal > 0 { updates["disco"] = fmt.Sprintf("%.0f GB", maxTotal) } } else if disco, _ := rawMap["disco"].(map[string]any); disco != nil { if total, _ := disco["total_gb"].(float64); total > 0 { updates["disco"] = fmt.Sprintf("%.0f GB", total) } } } } if err := db.Model(&servidor).Updates(updates).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"ok": false, "error": "no se pudo guardar"}) } // Insertar punto en el historial de métricas if req.Metricas != "" { var raw map[string]any if json.Unmarshal([]byte(req.Metricas), &raw) == nil { getFloat := func(obj map[string]any, key string) float64 { if sub, ok := obj[key].(map[string]any); ok { if v, ok := sub["porcentaje"].(float64); ok { return v } } return 0 } discoPct := 0.0 if discos, ok := raw["discos"].([]any); ok && len(discos) > 0 { if d, ok := discos[0].(map[string]any); ok { if v, ok := d["porcentaje"].(float64); ok { discoPct = v } } } tcpConns := 0 if v, ok := raw["tcp_conns"].(float64); ok { tcpConns = int(v) } load1 := 0.0 if la, ok := raw["load_avg"].(map[string]any); ok { if v, ok := la["load1"].(float64); ok { load1 = v } } swapPct := 0.0 if sw, ok := raw["swap"].(map[string]any); ok { if v, ok := sw["porcentaje"].(float64); ok { swapPct = v } } models.InsertMetricasHistory(servidor.ID, getFloat(raw, "cpu"), getFloat(raw, "ram"), swapPct, discoPct, tcpConns, load1, ) } } // Notificar por Telegram si el agente aplicó un renice if req.ReniceAction != "" { go services.NotificarReniceAction(servidor.Nombre, req.ReniceAction) } return c.JSON(fiber.Map{"ok": true, "servidor_id": servidor.ID}) } // GetMetricasHistory devuelve el historial de métricas de un servidor. // GET /app/servidor/:id/metricas-history?horas=24 func GetMetricasHistory(c *fiber.Ctx) error { id := c.Params("id") horas := c.QueryInt("horas", 24) if horas <= 0 || horas > 168 { horas = 24 } var srv models.Servidor if err := app.Http.Database.DB.First(&srv, id).Error; err != nil { return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "servidor no encontrado"}) } items, err := models.GetMetricasHistory(srv.ID, horas) if err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) } return c.JSON(items) } // GenerateAgentToken genera o regenera el token de agente para un servidor. // Ruta protegida: POST /app/servidor/:id/agent-token func GenerateAgentToken(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"}) } // Generar token aleatorio de 32 bytes (64 hex chars) tokenBytes := make([]byte, 32) if _, err := rand.Read(tokenBytes); err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo generar token"}) } token := hex.EncodeToString(tokenBytes) if err := db.Model(&servidor).Update("agent_token", token).Error; err != nil { return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "no se pudo guardar token"}) } return c.JSON(fiber.Map{ "ok": true, "agent_token": token, "servidor_id": servidor.ID, }) }