From 553252508f0633fb027170d04285963732b706b0 Mon Sep 17 00:00:00 2001 From: Lizandro Guarnizo <77708265+lizandrogd@users.noreply.github.com> Date: Tue, 26 May 2026 14:06:38 -0500 Subject: [PATCH] up --- resources/views/coolify.html | 137 +++++++++++++++---------- resources/views/vcard_api.html | 29 +----- rest/controllers/coolify_controller.go | 97 +++++++++++------ 3 files changed, 156 insertions(+), 107 deletions(-) diff --git a/resources/views/coolify.html b/resources/views/coolify.html index 2323040..80726c9 100644 --- a/resources/views/coolify.html +++ b/resources/views/coolify.html @@ -87,11 +87,7 @@ -======= - x-text="app.status || 'unknown'"> ->>>>>>> parent of 71fcf98 (up) @@ -111,42 +107,49 @@
- - - + @@ -227,23 +230,22 @@
-======= - x-text="svc.status || 'unknown'"> ->>>>>>> parent of 71fcf98 (up)
- - - + + + + class="text-xs px-2.5 py-1 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 transition">Envs
@@ -267,11 +269,7 @@
-======= - x-text="db.status || 'unknown'"> ->>>>>>> parent of 71fcf98 (up)
@@ -279,12 +277,15 @@
DB:
- - - + + +
@@ -454,17 +455,6 @@
- -
-

URL Webhook (notificaciones)

-

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

-
- - -
-
@@ -795,18 +785,53 @@ 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) { - 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'; + 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 (s === 'restarting') return 'bg-yellow-100 text-yellow-700'; - if (s === 'degraded') return 'bg-orange-100 text-orange-700'; + if (s === 'degraded') return 'bg-orange-100 text-orange-700'; + if (s === 'unhealthy') 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(); @@ -913,6 +938,8 @@ 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; } @@ -935,6 +962,7 @@ 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; } @@ -946,6 +974,7 @@ 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 0c760e7..1cdfbed 100644 --- a/resources/views/vcard_api.html +++ b/resources/views/vcard_api.html @@ -446,31 +446,12 @@ diff --git a/rest/controllers/coolify_controller.go b/rest/controllers/coolify_controller.go index f61aaeb..e8f9130 100644 --- a/rest/controllers/coolify_controller.go +++ b/rest/controllers/coolify_controller.go @@ -12,9 +12,13 @@ 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 { @@ -25,6 +29,7 @@ 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 @@ -34,6 +39,7 @@ 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 { @@ -42,33 +48,34 @@ 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 { - // 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()}) + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": 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{ @@ -78,6 +85,8 @@ func CoolifyIndex(c *fiber.Ctx) error { }, "layouts/main") } +// ─── Configuración ──────────────────────────────────────────────────────────── + func CoolifyGetConfig(c *fiber.Ctx) error { cfg, err := models.GetCoolifyConfig() if err != nil { @@ -118,6 +127,7 @@ 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 { @@ -127,7 +137,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.StatusBadRequest).JSON(fiber.Map{"error": "No se pudo conectar a Coolify: " + err.Error()}) + return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"error": err.Error()}) } defer resp.Body.Close() body, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) @@ -139,142 +149,171 @@ 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.MethodPost, "/applications/"+c.Params("uuid")+"/start") + return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/start") } + func CoolifyApplicationStop(c *fiber.Ctx) error { - return coolifyProxy(c, http.MethodPost, "/applications/"+c.Params("uuid")+"/stop") + return coolifyProxy(c, http.MethodGet, "/applications/"+c.Params("uuid")+"/stop") } + func CoolifyApplicationRestart(c *fiber.Ctx) error { - return coolifyProxy(c, http.MethodPost, "/applications/"+c.Params("uuid")+"/restart") + return coolifyProxy(c, http.MethodGet, "/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 (deployment events, etc.) -// Configura en Coolify UI → Settings → Notifications → Webhook → URL: https://admin.u-site.app/app/coolify/webhook +// CoolifyWebhook recibe notificaciones push de Coolify (deployments, etc.) +// Acepta el payload y devuelve 200; el procesamiento puede extenderse aquí. func CoolifyWebhook(c *fiber.Ctx) error { - 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}) + return c.SendStatus(fiber.StatusOK) }