diff --git a/main.go b/main.go index abefdd1..54a4b09 100755 --- a/main.go +++ b/main.go @@ -157,6 +157,7 @@ func main() { &models.UmindAviso{}, &models.UmindArchivo{}, &models.UmindAccionPendiente{}, + &models.UmindVigilancia{}, &models.UmindConexion{}, &models.UmindEventoLog{}, &models.UmindPlan{}, diff --git a/migrations/migrate.go b/migrations/migrate.go index d8705ad..8f2050a 100755 --- a/migrations/migrate.go +++ b/migrations/migrate.go @@ -129,6 +129,7 @@ func Migrate() { &models.UmindAviso{}, &models.UmindArchivo{}, &models.UmindAccionPendiente{}, + &models.UmindVigilancia{}, &models.UmindConexion{}, &models.UmindEventoLog{}, &models.UmindPlan{}, diff --git a/pkg/models/umind_vigilancia.go b/pkg/models/umind_vigilancia.go new file mode 100644 index 0000000..a2405d1 --- /dev/null +++ b/pkg/models/umind_vigilancia.go @@ -0,0 +1,106 @@ +package models + +import ( + "time" + + "github.com/sujit-baniya/fiber-boilerplate/app" + "gorm.io/gorm" +) + +// UmindVigilancia mira una API cada tanto y avisa cuando pasa algo: +// "avisame por correo cuando esta API devuelva stock en cero". +// +// Apunta a una HERRAMIENTA ya configurada del agente y no a una URL libre. +// Así hereda entero el blindaje de LlamarHerramientaWebhook —resolución de DNS +// en el connect, IPs internas rechazadas, sin redirects, respuesta acotada— +// sin escribir una línea de eso de nuevo. +type UmindVigilancia struct { + gorm.Model + AgenteID uint `json:"agente_id" gorm:"column:agente_id;index;not null"` + TenantID uint `json:"tenant_id" gorm:"column:tenant_id;index"` + Nombre string `json:"nombre" gorm:"column:nombre;size:150;not null"` + Herramienta string `json:"herramienta" gorm:"column:herramienta;size:60;not null"` + // Condicion en lenguaje natural: "el stock está en cero", "hay pedidos sin + // despachar hace más de dos días". + Condicion string `json:"condicion" gorm:"column:condicion;type:text;not null"` + IntervaloMin int `json:"intervalo_min" gorm:"column:intervalo_min;default:30"` + Destino string `json:"destino" gorm:"column:destino;size:120"` + Activa bool `json:"activa" gorm:"column:activa;default:true"` + // HashUltimo es lo que evita quemar plata: si la respuesta no cambió, no + // hay nada que evaluar y no se gasta un token. + HashUltimo string `json:"-" gorm:"column:hash_ultimo;size:64"` + // EnCondicion recuerda si la última evaluación dio que sí. Se avisa al + // ENTRAR en condición, no en cada chequeo que la siga cumpliendo — es la + // lección que el monitor de sitios ya aprendió con las transiciones. + EnCondicion bool `json:"en_condicion" gorm:"column:en_condicion;default:false"` + UltimoCheckAt *time.Time `json:"ultimo_check_at" gorm:"column:ultimo_check_at"` + UltimoError string `json:"ultimo_error" gorm:"column:ultimo_error;type:text"` + FallosNoOk int `json:"-" gorm:"column:fallos_no_ok;default:0"` +} + +func (UmindVigilancia) TableName() string { return "umind_vigilancias" } + +// UmindVigilanciaMax acota cuántas puede tener un agente: cada una es una +// llamada saliente y, cuando cambia, una consulta a la IA. +const UmindVigilanciaMax = 5 + +// UmindVigilanciaIntervaloMin es el piso. Menos que esto es martillarle la API +// a un tercero para ganar segundos que un aviso no necesita. +const UmindVigilanciaIntervaloMin = 5 + +func CreateUmindVigilancia(v *UmindVigilancia) error { + var n int64 + app.Http.Database.DB.Model(&UmindVigilancia{}). + Where("agente_id = ? AND activa = ? AND deleted_at IS NULL", v.AgenteID, true).Count(&n) + if n >= UmindVigilanciaMax { + return gorm.ErrInvalidData + } + if v.IntervaloMin < UmindVigilanciaIntervaloMin { + v.IntervaloMin = 30 + } + return app.Http.Database.DB.Create(v).Error +} + +func GetUmindVigilanciasByAgente(agenteID uint) ([]UmindVigilancia, error) { + var items []UmindVigilancia + err := app.Http.Database.DB.Where("agente_id = ?", agenteID). + Order("created_at DESC").Find(&items).Error + return items, err +} + +func GetUmindVigilanciaByID(id uint) (*UmindVigilancia, error) { + var v UmindVigilancia + if err := app.Http.Database.DB.First(&v, id).Error; err != nil { + return nil, err + } + return &v, nil +} + +func DeleteUmindVigilancia(id uint) error { + return app.Http.Database.DB.Delete(&UmindVigilancia{}, id).Error +} + +// GetUmindVigilanciasPendientes devuelve las que ya les toca según su propio +// intervalo, igual que los canales de correo. +func GetUmindVigilanciasPendientes() ([]UmindVigilancia, error) { + var items []UmindVigilancia + err := app.Http.Database.DB. + Where("activa = ? AND deleted_at IS NULL", true). + Where("ultimo_check_at IS NULL OR ultimo_check_at < NOW() - (intervalo_min || ' minutes')::interval"). + Limit(200).Find(&items).Error + return items, err +} + +func ActualizarVigilancia(id uint, updates map[string]interface{}) { + ahora := time.Now() + updates["ultimo_check_at"] = &ahora + app.Http.Database.DB.Model(&UmindVigilancia{}).Where("id = ?", id).Updates(updates) +} + +// DesactivarVigilancia la apaga tras demasiados fallos seguidos. Una API que +// dejó de existir no puede seguir consultándose para siempre — ni gastando el +// plan de quien la configuró. +func DesactivarVigilancia(id uint, motivo string) { + app.Http.Database.DB.Model(&UmindVigilancia{}).Where("id = ?", id). + Updates(map[string]interface{}{"activa": false, "ultimo_error": motivo}) +} diff --git a/pkg/services/cron_service.go b/pkg/services/cron_service.go index 3d28fa7..d5727ed 100644 --- a/pkg/services/cron_service.go +++ b/pkg/services/cron_service.go @@ -85,6 +85,12 @@ func IniciarCron() { return } + // Vigilancias de APIs — cada una según su propio intervalo. + if _, err := cronScheduler.AddFunc("* * * * *", RevisarVigilancias); err != nil { + log.Printf("[CRON] Error registrando tarea vigilancias_umind: %v", err) + return + } + // Los pendientes que nadie miró vencen solos. if _, err := cronScheduler.AddFunc("0 5 * * *", func() { models.VencerAccionesViejas() }); err != nil { log.Printf("[CRON] Error registrando tarea vencer_acciones: %v", err) diff --git a/pkg/services/umind_agent_service.go b/pkg/services/umind_agent_service.go index d6ed03d..7283ee0 100644 --- a/pkg/services/umind_agent_service.go +++ b/pkg/services/umind_agent_service.go @@ -68,6 +68,7 @@ func umindTools(agenteID uint, sesionInterna bool) []agentTool { // cualquiera, y programar recordatorios gasta el plan de otro. if sesionInterna { tools = append(tools, umindAvisoTools()...) + tools = append(tools, umindVigilanciaTools(agenteID)...) } herramientas, err := models.GetUmindHerramientasActivas(agenteID) @@ -190,6 +191,13 @@ func ejecutarHerramienta(agenteID uint, sessionID string, sesionInterna bool, na return executeUmindDocumentoTool(agenteID, args) } + if strings.HasSuffix(name, "_vigilancia") || name == "listar_vigilancias" { + if !sesionInterna { + return `{"error": "las vigilancias solo puede crearlas el dueño desde su canal privado"}` + } + return executeUmindVigilanciaTool(agenteID, sessionID, name, args) + } + if strings.HasSuffix(name, "_aviso") || name == "listar_avisos" { if !sesionInterna { return `{"error": "los recordatorios solo puede programarlos el dueño desde su canal privado"}` diff --git a/pkg/services/umind_vigilancia_service.go b/pkg/services/umind_vigilancia_service.go new file mode 100644 index 0000000..285641d --- /dev/null +++ b/pkg/services/umind_vigilancia_service.go @@ -0,0 +1,161 @@ +package services + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "log" + "strings" + "sync" + + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" +) + +// La vigilancia de APIs: "avisame cuando esta API devuelva stock en cero". + +var vigilanciasEnCurso sync.Mutex + +// vigilanciaFallosMax es cuántos errores seguidos aguanta antes de apagarse. +// Una API que dejó de existir no puede consultarse para siempre. +const vigilanciaFallosMax = 10 + +// RevisarVigilancias corre cada minuto y atiende solo las que ya les toca. +func RevisarVigilancias() { + if !vigilanciasEnCurso.TryLock() { + return + } + defer vigilanciasEnCurso.Unlock() + + vigilancias, err := models.GetUmindVigilanciasPendientes() + if err != nil { + log.Printf("[UMIND_VIGILA] no se pudieron listar: %v", err) + return + } + if len(vigilancias) == 0 { + return + } + + const trabajadores = 4 + cola := make(chan models.UmindVigilancia) + var wg sync.WaitGroup + for w := 0; w < trabajadores; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for v := range cola { + revisarUnaVigilancia(v) + } + }() + } + for _, v := range vigilancias { + cola <- v + } + close(cola) + wg.Wait() +} + +func revisarUnaVigilancia(v models.UmindVigilancia) { + herramienta, err := models.GetUmindHerramientaByNombre(v.AgenteID, v.Herramienta) + if err != nil { + models.DesactivarVigilancia(v.ID, "la herramienta que vigilaba ya no existe") + models.RegistrarEventoUmind(v.AgenteID, "warn", "vigilancia", + "Se apagó la vigilancia "+v.Nombre+": la herramienta que usaba ya no existe", "") + return + } + authValor := "" + if herramienta.AuthHeaderValorEnc != "" { + if authValor, err = DescifrarSecretoUmind(herramienta.AuthHeaderValorEnc); err != nil { + models.ActualizarVigilancia(v.ID, map[string]interface{}{"ultimo_error": "credencial ilegible"}) + return + } + } + + respuesta, err := LlamarHerramientaWebhook(herramienta.URL, herramienta.AuthHeaderNombre, authValor, map[string]interface{}{}) + if err != nil { + fallos := v.FallosNoOk + 1 + if fallos >= vigilanciaFallosMax { + models.DesactivarVigilancia(v.ID, err.Error()) + models.RegistrarEventoUmind(v.AgenteID, "warn", "vigilancia", + "Se apagó la vigilancia "+v.Nombre+" tras fallar muchas veces seguidas", err.Error()) + return + } + models.ActualizarVigilancia(v.ID, map[string]interface{}{"ultimo_error": err.Error(), "fallos_no_ok": fallos}) + return + } + + // Acá está el ahorro: si la respuesta es idéntica a la anterior, no hay + // nada que evaluar. Preguntarle a la IA en cada chequeo costaría una + // fortuna por una respuesta que ya sabemos. + suma := sha256.Sum256([]byte(respuesta)) + hash := hex.EncodeToString(suma[:]) + if hash == v.HashUltimo { + models.ActualizarVigilancia(v.ID, map[string]interface{}{"ultimo_error": "", "fallos_no_ok": 0}) + return + } + + cumple, err := EvaluarCondicionVigilancia(v.Condicion, respuesta) + if err != nil { + models.ActualizarVigilancia(v.ID, map[string]interface{}{ + "hash_ultimo": hash, "ultimo_error": "no se pudo evaluar la condición: " + err.Error(), + }) + return + } + models.RegistrarUsoUmind(v.AgenteID, models.UsoTipoIA, 400, "tokens") + + updates := map[string]interface{}{"hash_ultimo": hash, "ultimo_error": "", "fallos_no_ok": 0, "en_condicion": cumple} + // Se avisa al ENTRAR en condición. Si ya estaba y sigue, no se repite: + // una alerta que llega cada media hora deja de leerse a la segunda. + if cumple && !v.EnCondicion { + if err := avisarVigilancia(v, respuesta); err != nil { + log.Printf("[UMIND_VIGILA] no se pudo avisar de %q: %v", v.Nombre, err) + models.RegistrarEventoUmind(v.AgenteID, "error", "vigilancia", "No se pudo avisar de la vigilancia "+v.Nombre, err.Error()) + // Sin marcar en_condicion: se reintenta el aviso al próximo ciclo. + delete(updates, "en_condicion") + } + } + models.ActualizarVigilancia(v.ID, updates) +} + +// EvaluarCondicionVigilancia le pregunta a la IA si lo que devolvió la API +// cumple la condición. Se le exige SI o NO para que la respuesta sea usable: +// un modelo que explica su razonamiento acá no sirve de nada. +func EvaluarCondicionVigilancia(condicion, respuesta string) (bool, error) { + if len(respuesta) > 6000 { + respuesta = respuesta[:6000] + } + sistema := "Sos un evaluador. Te dan la respuesta de una API y una condición. " + + "Respondé EXACTAMENTE una palabra: SI si la condición se cumple, NO si no se cumple. " + + "Ante la duda respondé NO." + usuario := fmt.Sprintf("Condición: %s\n\nRespuesta de la API:\n%s", condicion, respuesta) + out, err := CompletarTextoIA("ia", sistema, usuario) + if err != nil { + return false, err + } + return interpretarVeredicto(out), nil +} + +// interpretarVeredicto acepta solo un SI explícito. Cualquier otra cosa —una +// explicación, un error, un modelo confundido— se toma como que no: es +// preferible un aviso que no llega a uno que llega sin motivo a las 3 AM. +func interpretarVeredicto(out string) bool { + limpio := strings.ToUpper(strings.TrimSpace(out)) + limpio = strings.Trim(limpio, ".!\"' \n") + return limpio == "SI" || limpio == "SÍ" || + strings.HasPrefix(limpio, "SI ") || strings.HasPrefix(limpio, "SI,") || + strings.HasPrefix(limpio, "SÍ ") || strings.HasPrefix(limpio, "SÍ,") +} + +// avisarVigilancia reusa la entrega de los recordatorios: mismo destino, mismo +// camino, una sola implementación que mantener. +func avisarVigilancia(v models.UmindVigilancia, respuesta string) error { + detalle := v.Condicion + if len(respuesta) < 800 { + detalle += "\n\nLo que devolvió la API:\n" + respuesta + } + return entregarAviso(models.UmindAviso{ + AgenteID: v.AgenteID, + Titulo: "Se cumplió lo que estabas vigilando: " + v.Nombre, + Detalle: detalle, + Destino: v.Destino, + }) +} diff --git a/pkg/services/umind_vigilancia_test.go b/pkg/services/umind_vigilancia_test.go new file mode 100644 index 0000000..05a200c --- /dev/null +++ b/pkg/services/umind_vigilancia_test.go @@ -0,0 +1,72 @@ +package services + +import ( + "os" + "strings" + "testing" +) + +// Solo un SI explícito dispara el aviso. Un modelo que devuelve una +// explicación, un error o cualquier otra cosa no puede despertar a nadie a las +// 3 de la mañana: un aviso que no llega molesta menos que uno sin motivo. +func TestEvaluarCondicionSoloAceptaSiExplicito(t *testing.T) { + casos := map[string]bool{ + "SI": true, "si": true, "SÍ": true, "Sí.": true, + "SI, el stock está en cero": true, + "NO": false, "no": false, + "Sin duda no puedo determinarlo": false, + "La condición parece cumplirse en algunos casos": false, + "": false, "ERROR": false, "true": false, + } + for entrada, quiero := range casos { + if got := interpretarVeredicto(entrada); got != quiero { + t.Errorf("veredicto %q = %v, quiero %v", entrada, got, quiero) + } + } +} + +// El ahorro que hace viable la feature: sin cambio en la respuesta, no se +// llama a la IA. Sin esto, cinco vigilancias cada 5 minutos son 1440 consultas +// diarias al modelo por cliente. +func TestVigilanciaNoLlamaALaIASiNoCambio(t *testing.T) { + b, err := os.ReadFile("umind_vigilancia_service.go") + if err != nil { + t.Fatal(err) + } + s := string(b) + i := strings.Index(s, "func revisarUnaVigilancia") + cuerpo := s[i:] + cuerpo = cuerpo[:strings.Index(cuerpo, "\n// EvaluarCondicion")] + + hash := strings.Index(cuerpo, "hash == v.HashUltimo") + eval := strings.Index(cuerpo, "EvaluarCondicionVigilancia") + if hash < 0 || eval < 0 || hash > eval { + t.Error("el corte por hash tiene que ir ANTES de evaluar con la IA") + } + corte := cuerpo[hash:eval] + if !strings.Contains(corte, "return") { + t.Error("si el hash no cambió tiene que cortar, no seguir a la evaluación") + } + + // Se avisa al entrar en condición, no en cada chequeo que la siga + // cumpliendo: una alerta cada media hora deja de leerse a la segunda. + if !strings.Contains(cuerpo, "cumple && !v.EnCondicion") { + t.Error("falta el flanco: sin él la misma alerta se repite en cada ciclo") + } +} + +// La vigilancia apunta a una herramienta del agente y nunca a una URL libre: +// es lo que le da el blindaje anti-SSRF sin reimplementarlo. +func TestVigilanciaNoAceptaURLLibre(t *testing.T) { + b, err := os.ReadFile("umind_vigilancia_tools.go") + if err != nil { + t.Fatal(err) + } + s := string(b) + if strings.Contains(s, `"url"`) { + t.Error("la tool no puede aceptar una URL: tiene que apuntar a una herramienta ya configurada") + } + if !strings.Contains(s, "GetUmindHerramientaByNombre(agenteID, herramienta)") { + t.Error("falta validar que la herramienta exista y sea de este agente") + } +} diff --git a/pkg/services/umind_vigilancia_tools.go b/pkg/services/umind_vigilancia_tools.go new file mode 100644 index 0000000..9770b83 --- /dev/null +++ b/pkg/services/umind_vigilancia_tools.go @@ -0,0 +1,123 @@ +package services + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" +) + +// Las tools con las que el dueño arma vigilancias conversando. Solo canal +// interno: cada vigilancia es una llamada saliente recurrente y consultas a la +// IA, o sea plan del dueño gastado por quien la pida. + +func umindVigilanciaTools(agenteID uint) []agentTool { + herramientas, err := models.GetUmindHerramientasActivas(agenteID) + if err != nil || len(herramientas) == 0 { + return nil // sin herramientas no hay nada que vigilar + } + nombres := make([]string, len(herramientas)) + for i, h := range herramientas { + nombres[i] = h.Nombre + } + return []agentTool{ + {Type: "function", Function: agentToolFunc{ + Name: "crear_vigilancia", + Description: "Programa una vigilancia sobre una API: consulta cada cierto tiempo y avisa cuando se cumple una condición. " + + "Herramientas disponibles para vigilar: " + strings.Join(nombres, ", "), + Parameters: agentToolParam{ + Type: "object", + Properties: map[string]agentToolParam{ + "nombre": {Type: "string", Description: "Cómo llamarla, en pocas palabras"}, + "herramienta": {Type: "string", Description: "Nombre de la herramienta a consultar"}, + "condicion": {Type: "string", Description: "Qué tiene que pasar para avisar, en lenguaje natural"}, + "cada_minutos": {Type: "number", Description: "Cada cuántos minutos revisar (mínimo 5, por defecto 30)"}, + }, + Required: []string{"nombre", "herramienta", "condicion"}, + }, + }}, + {Type: "function", Function: agentToolFunc{ + Name: "listar_vigilancias", + Description: "Lista las vigilancias activas sobre APIs.", + Parameters: agentToolParam{Type: "object", Properties: map[string]agentToolParam{}, Required: []string{}}, + }}, + {Type: "function", Function: agentToolFunc{ + Name: "cancelar_vigilancia", + Description: "Apaga una vigilancia. Usá listar_vigilancias para saber el id.", + Parameters: agentToolParam{ + Type: "object", + Properties: map[string]agentToolParam{"id": {Type: "number", Description: "El id de la vigilancia"}}, + Required: []string{"id"}, + }, + }}, + } +} + +func executeUmindVigilanciaTool(agenteID uint, sessionID, name string, args map[string]interface{}) string { + switch name { + case "crear_vigilancia": + nombre, _ := args["nombre"].(string) + herramienta, _ := args["herramienta"].(string) + condicion, _ := args["condicion"].(string) + if strings.TrimSpace(nombre) == "" || strings.TrimSpace(condicion) == "" { + return `{"error": "falta el nombre o la condición"}` + } + // La herramienta tiene que existir y ser de este agente: es lo que + // hace que la vigilancia no pueda apuntar a una URL arbitraria. + if _, err := models.GetUmindHerramientaByNombre(agenteID, herramienta); err != nil { + return fmt.Sprintf(`{"error": "no existe una herramienta llamada %q en este agente"}`, herramienta) + } + cada := 30 + if n, ok := args["cada_minutos"].(float64); ok && int(n) >= models.UmindVigilanciaIntervaloMin { + cada = int(n) + } + agente, err := models.GetUmindAgenteByID(agenteID) + if err != nil { + return `{"error": "no se pudo crear la vigilancia"}` + } + v := &models.UmindVigilancia{ + AgenteID: agenteID, TenantID: agente.TenantID, Nombre: nombre, + Herramienta: herramienta, Condicion: condicion, + IntervaloMin: cada, Destino: sessionID, Activa: true, + } + if err := models.CreateUmindVigilancia(v); err != nil { + return fmt.Sprintf(`{"error": "no se pudo crear: ya tenés el máximo de %d vigilancias activas"}`, models.UmindVigilanciaMax) + } + b, _ := json.Marshal(map[string]interface{}{"ok": true, "id": v.ID, "cada_minutos": cada}) + return string(b) + + case "listar_vigilancias": + items, err := models.GetUmindVigilanciasByAgente(agenteID) + if err != nil { + return `{"error": "no se pudieron listar"}` + } + lista := []map[string]interface{}{} + for _, v := range items { + if !v.Activa { + continue + } + lista = append(lista, map[string]interface{}{ + "id": v.ID, "nombre": v.Nombre, "condicion": v.Condicion, + "cada_minutos": v.IntervaloMin, "en_condicion": v.EnCondicion, + }) + } + b, _ := json.Marshal(map[string]interface{}{"vigilancias": lista}) + return string(b) + + case "cancelar_vigilancia": + id, ok := args["id"].(float64) + if !ok || id <= 0 { + return `{"error": "falta el id"}` + } + v, err := models.GetUmindVigilanciaByID(uint(id)) + if err != nil || v.AgenteID != agenteID { + return `{"error": "esa vigilancia no existe"}` + } + if err := models.DeleteUmindVigilancia(uint(id)); err != nil { + return `{"error": "no se pudo cancelar"}` + } + return `{"ok": true}` + } + return `{"error": "herramienta desconocida"}` +} diff --git a/rest/controllers/umind_vigilancia_controller.go b/rest/controllers/umind_vigilancia_controller.go new file mode 100644 index 0000000..6129d7b --- /dev/null +++ b/rest/controllers/umind_vigilancia_controller.go @@ -0,0 +1,40 @@ +package controllers + +import ( + "strconv" + + "github.com/gofiber/fiber/v2" + "github.com/sujit-baniya/fiber-boilerplate/pkg/models" +) + +// GetUmindVigilanciasHandler — GET /umind/vigilancias?agente_id=N +func GetUmindVigilanciasHandler(c *fiber.Ctx) error { + agenteID, _ := strconv.ParseUint(c.Query("agente_id"), 10, 64) + if _, err := accesoAgente(c, uint(agenteID)); err != nil { + return err + } + items, err := models.GetUmindVigilanciasByAgente(uint(agenteID)) + if err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"items": items}) +} + +// DeleteUmindVigilanciaHandler — DELETE /umind/vigilancias/:id +func DeleteUmindVigilanciaHandler(c *fiber.Ctx) error { + id, err := strconv.ParseUint(c.Params("id"), 10, 64) + if err != nil { + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "id inválido"}) + } + v, err := models.GetUmindVigilanciaByID(uint(id)) + if err != nil { + return errSinAcceso(c) + } + if _, err := accesoAgente(c, v.AgenteID); err != nil { + return err + } + if err := models.DeleteUmindVigilancia(uint(id)); err != nil { + return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()}) + } + return c.JSON(fiber.Map{"ok": true}) +} diff --git a/rest/routes/umind.go b/rest/routes/umind.go index 944c20c..de90fff 100644 --- a/rest/routes/umind.go +++ b/rest/routes/umind.go @@ -65,6 +65,8 @@ func RegistrarRutasUmind(g fiber.Router, scope fiber.Handler, escritura fiber.Ha g.Get("/umind/conexiones", r(controllers.GetUmindConexionesHandler)...) g.Get("/umind/conexiones/conectar", w(controllers.UmindConectarHandler)...) + g.Get("/umind/vigilancias", w(controllers.GetUmindVigilanciasHandler)...) + g.Delete("/umind/vigilancias/:id", w(controllers.DeleteUmindVigilanciaHandler)...) g.Get("/umind/plantillas", w(controllers.GetUmindPlantillasHandler)...) g.Post("/umind/plantillas", w(controllers.CreateUmindPlantillaHandler)...) g.Post("/umind/plantillas/importar", w(controllers.ImportarUmindPlantillaHandler)...)