diff --git a/main.go b/main.go
index 7d2ca3f..e2034de 100755
--- a/main.go
+++ b/main.go
@@ -49,10 +49,6 @@ 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{},
@@ -70,7 +66,6 @@ func main() {
// Notificaciones
&models.NotifEventoConfig{},
&models.SistemaNotificacion{},
- &models.ServidorAlertaUmbral{},
// Shield
&models.ShieldConfig{},
// Partner
@@ -80,12 +75,6 @@ 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 3dc6400..3ab2a38 100644
--- a/resources/views/coolify.html
+++ b/resources/views/coolify.html
@@ -87,7 +87,7 @@
+ x-text="statusLabel(app.status)">
@@ -107,42 +107,49 @@
-
+ x-text="statusLabel(svc.status)">
- Start
- Stop
- Restart
+ Start
+ Stop
+ Restart
Envs
+ class="text-xs px-2.5 py-1 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 transition">Envs
+ x-text="statusLabel(db.status)">
@@ -267,12 +277,15 @@
DB:
- Start
- Stop
- Restart
+ Start
+ Stop
+ Restart
-
-
-
URL Webhook (notificaciones)
-
Copia esta URL en Coolify → Settings → Notifications → Webhook para recibir eventos de deployment/restart.
-
-
- flash('URL copiada'))"
- class="text-xs px-3 py-1.5 rounded-lg border text-gray-600 hover:bg-gray-50 shrink-0">Copiar
-
-
@@ -743,18 +745,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();
@@ -861,6 +898,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; }
@@ -883,6 +922,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; }
@@ -894,6 +934,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)
}