diff --git a/main.go b/main.go index e2034de..7d2ca3f 100755 --- a/main.go +++ b/main.go @@ -49,6 +49,10 @@ func main() { migrations.MigratePortal() // Crear tablas de integraciones y pasarelas si no existen app.Http.Database.DB.AutoMigrate( + // Tablas base del sistema (columnas nuevas) + &models.Users{}, + &models.Roles{}, + &models.Submodules{}, &models.QueryHistory{}, &models.HostingerConfig{}, &models.CloudflareConfig{}, @@ -66,6 +70,7 @@ func main() { // Notificaciones &models.NotifEventoConfig{}, &models.SistemaNotificacion{}, + &models.ServidorAlertaUmbral{}, // Shield &models.ShieldConfig{}, // Partner @@ -75,6 +80,12 @@ func main() { &models.PortalPasswordResetToken{}, // Integración VCard API Admin &models.VcardApiConfig{}, + // Integración Coolify + &models.CoolifyConfig{}, + // Servidores: nuevos campos de agente + tabla join de integraciones + &models.Servidor{}, + &models.ConxDb{}, + &models.ProvServidor{}, ) // Seed automático (idempotente) de módulos del sistema migrations.SeedRenovaciones() diff --git a/resources/views/coolify.html b/resources/views/coolify.html index 80726c9..2323040 100644 --- a/resources/views/coolify.html +++ b/resources/views/coolify.html @@ -87,7 +87,11 @@ +======= + x-text="app.status || 'unknown'"> +>>>>>>> parent of 71fcf98 (up) @@ -107,49 +111,42 @@
- - - - - - - @@ -230,22 +227,23 @@
+======= + x-text="svc.status || 'unknown'"> +>>>>>>> parent of 71fcf98 (up)
- - - + + + + class="text-xs px-2.5 py-1 rounded-lg bg-gray-50 text-gray-600 hover:bg-gray-100 transition">Envs
@@ -269,7 +267,11 @@
+======= + x-text="db.status || 'unknown'"> +>>>>>>> parent of 71fcf98 (up)
@@ -277,15 +279,12 @@
DB:
- - - + + +
@@ -455,6 +454,17 @@
+ +
+

URL Webhook (notificaciones)

+

Copia esta URL en Coolify → Settings → Notifications → Webhook para recibir eventos de deployment/restart.

+
+ + +
+
@@ -785,53 +795,18 @@ document.addEventListener('alpine:init', () => { catch { return dt; } }, - // Coolify puede devolver "running:healthy", "exited:0", etc. — extrae la parte base. - normalizeStatus(status) { - if (!status) return ''; - return status.toLowerCase().split(':')[0].trim(); - }, - - isRunning(status) { - const s = this.normalizeStatus(status); - return s === 'running'; - }, - - isStopped(status) { - const s = this.normalizeStatus(status); - return s === 'stopped' || s === 'exited' || s === 'idle'; - }, - - isTransitioning(status) { - const s = this.normalizeStatus(status); - return s === 'starting' || s === 'stopping' || s === 'restarting'; - }, - statusClass(status) { - const s = this.normalizeStatus(status); - if (!s) return 'bg-gray-100 text-gray-500'; - if (s === 'running') return 'bg-green-100 text-green-700'; - if (s === 'stopped') return 'bg-red-100 text-red-600'; - if (s === 'exited') return 'bg-red-100 text-red-600'; - if (s === 'idle') return 'bg-gray-100 text-gray-500'; - if (s === 'starting') return 'bg-yellow-100 text-yellow-700'; - if (s === 'stopping') return 'bg-yellow-100 text-yellow-700'; + if (!status) return 'bg-gray-100 text-gray-500'; + const s = status.toLowerCase(); + if (s === 'running') return 'bg-green-100 text-green-700'; + if (s === 'stopped') return 'bg-red-100 text-red-600'; + if (s === 'exited') return 'bg-red-100 text-red-600'; + if (s === 'starting') return 'bg-yellow-100 text-yellow-700'; if (s === 'restarting') return 'bg-yellow-100 text-yellow-700'; - if (s === 'degraded') return 'bg-orange-100 text-orange-700'; - if (s === 'unhealthy') return 'bg-orange-100 text-orange-700'; + if (s === 'degraded') return 'bg-orange-100 text-orange-700'; return 'bg-gray-100 text-gray-500'; }, - statusLabel(status) { - if (!status) return 'desconocido'; - // Muestra el status original sin el sufijo de health check - const [base, extra] = status.toLowerCase().split(':'); - const labels = { running:'corriendo', stopped:'detenido', exited:'detenido', idle:'inactivo', - starting:'iniciando', stopping:'deteniendo', restarting:'reiniciando', - degraded:'degradado', unhealthy:'no saludable' }; - const baseLabel = labels[base] || base; - return extra ? `${baseLabel} (${extra})` : baseLabel; - }, - deployStatusClass(status) { if (!status) return 'bg-gray-100 text-gray-500'; const s = status.toLowerCase(); @@ -938,8 +913,6 @@ document.addEventListener('alpine:init', () => { const r = await fetch('/app/coolify/apps' + qs); const j = await r.json(); this.apps = Array.isArray(j) ? j : (j.data || []); - // Pre-init actionLoading para reactividad garantizada en Alpine.js - const al = {}; this.apps.forEach(a => { al[a.uuid] = false; }); this.actionLoading = al; this.lastFetch = this.fmtDate(new Date().toISOString()); } catch(e) { this.flash('Error cargando apps: ' + e.message, false); } finally { this.loading = false; } @@ -962,7 +935,6 @@ document.addEventListener('alpine:init', () => { const r = await fetch('/app/coolify/services'); const j = await r.json(); this.services = Array.isArray(j) ? j : (j.data || []); - const sl = { ...this.actionLoading }; this.services.forEach(s => { if (!(s.uuid in sl)) sl[s.uuid] = false; }); this.actionLoading = sl; this.lastFetch = this.fmtDate(new Date().toISOString()); } catch(e) { this.flash('Error cargando servicios: ' + e.message, false); } finally { this.loading = false; } @@ -974,7 +946,6 @@ document.addEventListener('alpine:init', () => { const r = await fetch('/app/coolify/databases'); const j = await r.json(); this.databases = Array.isArray(j) ? j : (j.data || []); - const dl = { ...this.actionLoading }; this.databases.forEach(d => { if (!(d.uuid in dl)) dl[d.uuid] = false; }); this.actionLoading = dl; this.lastFetch = this.fmtDate(new Date().toISOString()); } catch(e) { this.flash('Error cargando bases de datos: ' + e.message, false); } finally { this.loading = false; } diff --git a/resources/views/vcard_api.html b/resources/views/vcard_api.html index 1cdfbed..0c760e7 100644 --- a/resources/views/vcard_api.html +++ b/resources/views/vcard_api.html @@ -446,12 +446,31 @@ diff --git a/rest/controllers/coolify_controller.go b/rest/controllers/coolify_controller.go index e8f9130..f61aaeb 100644 --- a/rest/controllers/coolify_controller.go +++ b/rest/controllers/coolify_controller.go @@ -12,13 +12,9 @@ import ( "github.com/sujit-baniya/fiber-boilerplate/pkg/models" ) -// ─── helpers internos ──────────────────────────────────────────────────────── - -// coolifyDo ejecuta una petición a la API de Coolify y devuelve el body crudo. func coolifyDo(method, endpoint string, reqBody io.Reader, contentType string, cfg *models.CoolifyConfig) ([]byte, int, error) { base := strings.TrimRight(cfg.BaseURL, "/") url := fmt.Sprintf("%s/api/v1%s", base, endpoint) - client := &http.Client{Timeout: 30 * time.Second} req, err := http.NewRequest(method, url, reqBody) if err != nil { @@ -29,7 +25,6 @@ func coolifyDo(method, endpoint string, reqBody io.Reader, contentType string, c if contentType != "" { req.Header.Set("Content-Type", contentType) } - resp, err := client.Do(req) if err != nil { return nil, 0, err @@ -39,7 +34,6 @@ func coolifyDo(method, endpoint string, reqBody io.Reader, contentType string, c return body, resp.StatusCode, nil } -// coolifyProxy resuelve config, aplica qs de la request y responde al frontend. func coolifyProxy(c *fiber.Ctx, method, endpoint string) error { cfg, err := models.GetCoolifyConfig() if err != nil { @@ -48,34 +42,33 @@ func coolifyProxy(c *fiber.Ctx, method, endpoint string) error { if !cfg.Activo { return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "La integración Coolify está inactiva"}) } - ep := endpoint if qs := string(c.Request().URI().QueryString()); qs != "" { ep = endpoint + "?" + qs } - var bodyReader io.Reader ct := "" if raw := c.Body(); len(raw) > 0 { bodyReader = strings.NewReader(string(raw)) ct = c.Get("Content-Type", "application/json") } - body, status, err := coolifyDo(method, ep, bodyReader, ct, cfg) if err != nil { - return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + // No devolver 502: Cloudflare lo intercepta y oculta el error al frontend + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No se pudo conectar a Coolify: " + err.Error()}) } - var result json.RawMessage if err := json.Unmarshal(body, &result); err != nil { result = json.RawMessage(fmt.Sprintf(`{"raw":%q}`, string(body))) } + // Si Coolify devuelve 5xx, bajar a 400 para que Cloudflare no lo intercepte + if status >= 500 { + status = fiber.StatusBadRequest + } c.Status(status) return c.JSON(result) } -// ─── Página principal ───────────────────────────────────────────────────────── - func CoolifyIndex(c *fiber.Ctx) error { cfg, _ := models.GetCoolifyConfig() return c.Render("coolify", fiber.Map{ @@ -85,8 +78,6 @@ func CoolifyIndex(c *fiber.Ctx) error { }, "layouts/main") } -// ─── Configuración ──────────────────────────────────────────────────────────── - func CoolifyGetConfig(c *fiber.Ctx) error { cfg, err := models.GetCoolifyConfig() if err != nil { @@ -127,7 +118,6 @@ func CoolifySaveConfig(c *fiber.Ctx) error { return c.JSON(fiber.Map{"ok": true, "config": cfg}) } -// CoolifyTestConnection verifica conectividad con /health (no requiere auth). func CoolifyTestConnection(c *fiber.Ctx) error { cfg, err := models.GetCoolifyConfig() if err != nil { @@ -137,7 +127,7 @@ func CoolifyTestConnection(c *fiber.Ctx) error { client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Get(base + "/api/health") if err != nil { - return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) + return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "No se pudo conectar a Coolify: " + err.Error()}) } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) @@ -149,171 +139,142 @@ func CoolifyTestConnection(c *fiber.Ctx) error { return c.JSON(result) } -// ─── Applications ───────────────────────────────────────────────────────────── - func CoolifyListApplications(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/applications") } - func CoolifyGetApplication(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")) } - func CoolifyApplicationLogs(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/logs") } - func CoolifyApplicationEnvs(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/envs") } - func CoolifyApplicationEnvCreate(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodPost, "/applications/"+c.Params("uuid")+"/envs") } - func CoolifyApplicationEnvUpdateBulk(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodPatch, "/applications/"+c.Params("uuid")+"/envs") } - func CoolifyApplicationEnvDelete(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodDelete, "/applications/"+c.Params("uuid")+"/envs/"+c.Params("env_id")) } - func CoolifyApplicationStart(c *fiber.Ctx) error { - return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/start") + return coolifyProxy(c, http.MethodPost, "/applications/"+c.Params("uuid")+"/start") } - func CoolifyApplicationStop(c *fiber.Ctx) error { - return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/stop") + return coolifyProxy(c, http.MethodPost, "/applications/"+c.Params("uuid")+"/stop") } - func CoolifyApplicationRestart(c *fiber.Ctx) error { - return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/restart") + return coolifyProxy(c, http.MethodPost, "/applications/"+c.Params("uuid")+"/restart") } - func CoolifyApplicationDeployments(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/deployments") } - func CoolifyApplicationDeploy(c *fiber.Ctx) error { - // POST /api/v1/deploy?uuid={uuid}&force=false return coolifyProxy(c, http.MethodPost, "/deploy") } -// ─── Servers ────────────────────────────────────────────────────────────────── - func CoolifyListServers(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/servers") } - func CoolifyGetServer(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/servers/"+c.Params("uuid")) } - func CoolifyServerResources(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/servers/"+c.Params("uuid")+"/resources") } - func CoolifyServerDomains(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/servers/"+c.Params("uuid")+"/domains") } - func CoolifyServerValidate(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/servers/"+c.Params("uuid")+"/validate") } -// ─── Services ───────────────────────────────────────────────────────────────── - func CoolifyListServices(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/services") } - func CoolifyGetService(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid")) } - func CoolifyServiceStart(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid")+"/start") } - func CoolifyServiceStop(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid")+"/stop") } - func CoolifyServiceRestart(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid")+"/restart") } - func CoolifyServiceEnvs(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/services/"+c.Params("uuid")+"/envs") } - func CoolifyServiceEnvUpdateBulk(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodPatch, "/services/"+c.Params("uuid")+"/envs") } -// ─── Databases ──────────────────────────────────────────────────────────────── - func CoolifyListDatabases(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/databases") } - func CoolifyGetDatabase(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/databases/"+c.Params("uuid")) } - func CoolifyDatabaseStart(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/databases/"+c.Params("uuid")+"/start") } - func CoolifyDatabaseStop(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/databases/"+c.Params("uuid")+"/stop") } - func CoolifyDatabaseRestart(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/databases/"+c.Params("uuid")+"/restart") } -// ─── Projects ───────────────────────────────────────────────────────────────── - func CoolifyListProjects(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/projects") } - func CoolifyGetProject(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/projects/"+c.Params("uuid")) } - func CoolifyProjectEnvironments(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/projects/"+c.Params("uuid")+"/environments") } -// ─── Deployments ────────────────────────────────────────────────────────────── - func CoolifyListDeployments(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/deployments") } - func CoolifyGetDeployment(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/deployments/"+c.Params("uuid")) } -// ─── Teams ──────────────────────────────────────────────────────────────────── - func CoolifyListTeams(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/teams") } - func CoolifyCurrentTeam(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/teams/current") } - func CoolifyTeamMembers(c *fiber.Ctx) error { return coolifyProxy(c, http.MethodGet, "/teams/current/members") } -// CoolifyWebhook recibe notificaciones push de Coolify (deployments, etc.) -// Acepta el payload y devuelve 200; el procesamiento puede extenderse aquí. +// CoolifyWebhook recibe notificaciones push de Coolify (deployment events, etc.) +// Configura en Coolify UI → Settings → Notifications → Webhook → URL: https://admin.u-site.app/app/coolify/webhook func CoolifyWebhook(c *fiber.Ctx) error { - return c.SendStatus(fiber.StatusOK) + var payload map[string]interface{} + if err := c.BodyParser(&payload); err != nil { + // acepta también body vacío o texto plano + payload = map[string]interface{}{"raw": string(c.Body())} + } + + // Log del evento para debug + eventType := "" + if t, ok := payload["type"].(string); ok { + eventType = t + } else if t, ok := payload["event"].(string); ok { + eventType = t + } + + _ = eventType // en el futuro: guardar en DB, enviar push, etc. + + return c.JSON(fiber.Map{"ok": true}) }