This commit is contained in:
Lizandro Guarnizo
2026-05-26 11:42:41 -05:00
parent d1a668e229
commit e9548e13ae
4 changed files with 118 additions and 156 deletions
+11
View File
@@ -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()
+54 -83
View File
@@ -87,7 +87,11 @@
</div>
<span :class="statusClass(app.status)"
class="text-xs px-2 py-0.5 rounded-full font-medium shrink-0"
<<<<<<< HEAD
x-text="actionStatusText(app.uuid, app.status)">
=======
x-text="app.status || 'unknown'">
>>>>>>> parent of 71fcf98 (up)
</span>
</div>
@@ -107,49 +111,42 @@
<!-- Acciones -->
<div class="flex flex-wrap gap-2 pt-1 border-t border-gray-100">
<!-- Start: solo cuando está detenida / desconocida -->
<button x-show="!isRunning(app.status) && !isTransitioning(app.status)"
@click="appAction(app.uuid, 'start')"
<button @click="appAction(app.uuid, 'start')"
:disabled="actionLoading[app.uuid]"
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-green-100 text-green-700 hover:bg-green-200 transition disabled:opacity-50 font-medium">
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-green-50 text-green-700 hover:bg-green-100 transition disabled:opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="currentColor" viewBox="0 0 16 16"><path d="M11.596 8.697l-6.363 3.692c-.54.313-1.233-.066-1.233-.697V4.308c0-.63.692-1.01 1.233-.696l6.363 3.692a.802.802 0 0 1 0 1.393z"/></svg>
Start
</button>
<!-- Stop: solo cuando está corriendo -->
<button x-show="isRunning(app.status) || isTransitioning(app.status)"
@click="appAction(app.uuid, 'stop')"
:disabled="actionLoading[app.uuid] || isTransitioning(app.status)"
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-red-100 text-red-700 hover:bg-red-200 transition disabled:opacity-50 font-medium">
<button @click="appAction(app.uuid, 'stop')"
:disabled="actionLoading[app.uuid]"
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-red-50 text-red-700 hover:bg-red-100 transition disabled:opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="currentColor" viewBox="0 0 16 16"><path d="M5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11V5A1.5 1.5 0 0 1 5 3.5z"/></svg>
Stop
</button>
<!-- Restart: solo cuando está corriendo -->
<button x-show="isRunning(app.status)"
@click="appAction(app.uuid, 'restart')"
<button @click="appAction(app.uuid, 'restart')"
:disabled="actionLoading[app.uuid]"
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-yellow-100 text-yellow-700 hover:bg-yellow-200 transition disabled:opacity-50 font-medium">
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-yellow-50 text-yellow-700 hover:bg-yellow-100 transition disabled:opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 0 0 4.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 0 1-15.357-2m15.357 2H15"/></svg>
Restart
</button>
<!-- Deploy: siempre disponible -->
<button @click="deployApp(app.uuid)"
:disabled="actionLoading[app.uuid]"
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-indigo-100 text-indigo-700 hover:bg-indigo-200 transition disabled:opacity-50 font-medium">
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-indigo-50 text-indigo-700 hover:bg-indigo-100 transition disabled:opacity-50">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17v-6l-2 2m0 0l2-2m-2 2h14M3 9l3-3 3 3"/></svg>
Deploy
</button>
<button @click="openAppLogs(app)"
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 transition">
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-gray-50 text-gray-600 hover:bg-gray-100 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5.586a1 1 0 0 1 .707.293l5.414 5.414a1 1 0 0 1 .293.707V19a2 2 0 0 1-2 2z"/></svg>
Logs
</button>
<button @click="openAppEnvs(app)"
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 transition">
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-gray-50 text-gray-600 hover:bg-gray-100 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 0 0 2.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 0 0 1.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 0 0-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 0 0-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 0 0-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 0 0-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 0 0 1.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/></svg>
Envs
</button>
<button @click="openAppDeployments(app)"
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 transition">
class="flex items-center gap-1 text-xs px-2.5 py-1 rounded-lg bg-gray-50 text-gray-600 hover:bg-gray-100 transition">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M12 8v4l3 3m6-3a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"/></svg>
Historial
</button>
@@ -230,22 +227,23 @@
</div>
<span :class="statusClass(svc.status)"
class="text-xs px-2 py-0.5 rounded-full font-medium shrink-0"
<<<<<<< HEAD
x-text="actionStatusText(svc.uuid, svc.status)">
=======
x-text="svc.status || 'unknown'">
>>>>>>> parent of 71fcf98 (up)
</span>
</div>
<div x-show="svc.description" class="text-xs text-gray-500" x-text="svc.description"></div>
<div class="flex flex-wrap gap-2 pt-1 border-t border-gray-100">
<button x-show="!isRunning(svc.status) && !isTransitioning(svc.status)"
@click="serviceAction(svc.uuid, 'start')" :disabled="actionLoading[svc.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-green-100 text-green-700 hover:bg-green-200 transition disabled:opacity-50 font-medium">Start</button>
<button x-show="isRunning(svc.status) || isTransitioning(svc.status)"
@click="serviceAction(svc.uuid, 'stop')" :disabled="actionLoading[svc.uuid] || isTransitioning(svc.status)"
class="text-xs px-2.5 py-1 rounded-lg bg-red-100 text-red-700 hover:bg-red-200 transition disabled:opacity-50 font-medium">Stop</button>
<button x-show="isRunning(svc.status)"
@click="serviceAction(svc.uuid, 'restart')" :disabled="actionLoading[svc.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-yellow-100 text-yellow-700 hover:bg-yellow-200 transition disabled:opacity-50 font-medium">Restart</button>
<button @click="serviceAction(svc.uuid, 'start')" :disabled="actionLoading[svc.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-green-50 text-green-700 hover:bg-green-100 transition disabled:opacity-50">Start</button>
<button @click="serviceAction(svc.uuid, 'stop')" :disabled="actionLoading[svc.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-red-50 text-red-700 hover:bg-red-100 transition disabled:opacity-50">Stop</button>
<button @click="serviceAction(svc.uuid, 'restart')" :disabled="actionLoading[svc.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-yellow-50 text-yellow-700 hover:bg-yellow-100 transition disabled:opacity-50">Restart</button>
<button @click="openServiceEnvs(svc)"
class="text-xs px-2.5 py-1 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 transition">Envs</button>
class="text-xs px-2.5 py-1 rounded-lg bg-gray-50 text-gray-600 hover:bg-gray-100 transition">Envs</button>
</div>
<div x-show="actionLoading[svc.uuid]" class="text-xs text-indigo-500 flex items-center gap-1">
<svg class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
@@ -269,7 +267,11 @@
</div>
<span :class="statusClass(db.status)"
class="text-xs px-2 py-0.5 rounded-full font-medium shrink-0"
<<<<<<< HEAD
x-text="actionStatusText(db.uuid, db.status)">
=======
x-text="db.status || 'unknown'">
>>>>>>> parent of 71fcf98 (up)
</span>
</div>
<div class="text-xs text-gray-500 space-y-0.5">
@@ -277,15 +279,12 @@
<div x-show="db.name">DB: <span class="font-medium text-gray-700" x-text="db.postgres_db || db.mysql_database || db.mongo_database || ''"></span></div>
</div>
<div class="flex flex-wrap gap-2 pt-1 border-t border-gray-100">
<button x-show="!isRunning(db.status) && !isTransitioning(db.status)"
@click="dbAction(db.uuid, 'start')" :disabled="actionLoading[db.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-green-100 text-green-700 hover:bg-green-200 transition disabled:opacity-50 font-medium">Start</button>
<button x-show="isRunning(db.status) || isTransitioning(db.status)"
@click="dbAction(db.uuid, 'stop')" :disabled="actionLoading[db.uuid] || isTransitioning(db.status)"
class="text-xs px-2.5 py-1 rounded-lg bg-red-100 text-red-700 hover:bg-red-200 transition disabled:opacity-50 font-medium">Stop</button>
<button x-show="isRunning(db.status)"
@click="dbAction(db.uuid, 'restart')" :disabled="actionLoading[db.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-yellow-100 text-yellow-700 hover:bg-yellow-200 transition disabled:opacity-50 font-medium">Restart</button>
<button @click="dbAction(db.uuid, 'start')" :disabled="actionLoading[db.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-green-50 text-green-700 hover:bg-green-100 transition disabled:opacity-50">Start</button>
<button @click="dbAction(db.uuid, 'stop')" :disabled="actionLoading[db.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-red-50 text-red-700 hover:bg-red-100 transition disabled:opacity-50">Stop</button>
<button @click="dbAction(db.uuid, 'restart')" :disabled="actionLoading[db.uuid]"
class="text-xs px-2.5 py-1 rounded-lg bg-yellow-50 text-yellow-700 hover:bg-yellow-100 transition disabled:opacity-50">Restart</button>
</div>
<div x-show="actionLoading[db.uuid]" class="text-xs text-indigo-500 flex items-center gap-1">
<svg class="animate-spin h-3 w-3" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"/></svg>
@@ -455,6 +454,17 @@
</button>
</div>
</div>
<!-- Webhook URL para notificaciones -->
<div class="px-6 pb-4 border-t pt-4">
<p class="text-xs text-gray-500 mb-1 font-medium">URL Webhook (notificaciones)</p>
<p class="text-xs text-gray-400 mb-2">Copia esta URL en <strong>Coolify → Settings → Notifications → Webhook</strong> para recibir eventos de deployment/restart.</p>
<div class="flex items-center gap-2">
<code class="flex-1 text-xs bg-gray-50 border rounded px-3 py-1.5 truncate select-all"
x-text="window.location.origin + '/webhooks/coolify'"></code>
<button @click="navigator.clipboard.writeText(window.location.origin + '/webhooks/coolify').then(() => flash('URL copiada'))"
class="text-xs px-3 py-1.5 rounded-lg border text-gray-600 hover:bg-gray-50 shrink-0">Copiar</button>
</div>
</div>
<div x-show="cfgMsg" class="px-6 pb-4 text-sm" :class="cfgOk ? 'text-green-600' : 'text-red-600'" x-text="cfgMsg"></div>
</div>
</div>
@@ -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; }
+24 -5
View File
@@ -446,12 +446,31 @@
<tbody>
<template x-for="m in items" :key="m.id || m._id">
<tr class="hover:bg-gray-50 border-b border-gray-100">
<td class="py-2 px-3 font-medium" x-text="m.titulo || m.title || '—'"></td>
<td class="py-2 px-3 text-xs text-gray-500" x-text="(m.user && m.user.email) ? m.user.email : (m.user_id || '—')"></td>
<td class="py-2 px-3 text-xs text-gray-500" x-text="(m.vcard && m.vcard.nombre) ? m.vcard.nombre : '—'"></td>
<td class="py-2 px-3">
<span :class="m.activo ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="text-xs px-2 py-0.5 rounded-full" x-text="m.activo ? 'Activa' : 'Inactiva'"></span>
<div class="font-medium" x-text="m.titulo || m.title || '—'"></div>
<a x-show="m.url || m.unico" :href="m.url || ('https://web.u-site.app/w/' + m.unico)" target="_blank"
class="text-xs text-blue-500 hover:underline font-mono" x-text="m.unico || m.url || ''"></a>
</td>
<td class="py-2 px-3 text-xs text-gray-500">
<div x-text="(m.user && m.user.name) ? m.user.name : '—'"></div>
<div class="text-gray-400" x-text="(m.user && m.user.email) ? m.user.email : ''"></div>
</td>
<td class="py-2 px-3 text-xs text-gray-500">
<div x-text="(m.vcard && m.vcard.nombre) ? m.vcard.nombre : '—'"></div>
<span x-show="m.vcard" :class="(m.vcard && (m.vcard.estado === true || m.vcard.estado === 'true')) ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-400'"
class="text-xs px-1.5 py-0.5 rounded-full"
x-text="(m.vcard && (m.vcard.estado === true || m.vcard.estado === 'true')) ? 'VCard activa' : 'VCard inactiva'"></span>
</td>
<td class="py-2 px-3">
<template x-if="m.activo !== undefined && m.activo !== null">
<span :class="m.activo ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'"
class="text-xs px-2 py-0.5 rounded-full" x-text="m.activo ? 'Activa' : 'Inactiva'"></span>
</template>
<template x-if="m.activo === undefined || m.activo === null">
<span :class="(m.vcard && (m.vcard.estado === true || m.vcard.estado === 'true')) ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-500'"
class="text-xs px-2 py-0.5 rounded-full"
x-text="(m.vcard && (m.vcard.estado === true || m.vcard.estado === 'true')) ? 'Activa' : 'Sin estado'"></span>
</template>
</td>
</tr>
</template>
+29 -68
View File
@@ -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})
}