up
This commit is contained in:
@@ -87,11 +87,7 @@
|
||||
</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>
|
||||
|
||||
@@ -111,42 +107,49 @@
|
||||
|
||||
<!-- Acciones -->
|
||||
<div class="flex flex-wrap gap-2 pt-1 border-t border-gray-100">
|
||||
<button @click="appAction(app.uuid, 'start')"
|
||||
<!-- Start: solo cuando está detenida / desconocida -->
|
||||
<button x-show="!isRunning(app.status) && !isTransitioning(app.status)"
|
||||
@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-50 text-green-700 hover:bg-green-100 transition disabled:opacity-50">
|
||||
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">
|
||||
<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>
|
||||
<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">
|
||||
<!-- 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">
|
||||
<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>
|
||||
<button @click="appAction(app.uuid, 'restart')"
|
||||
<!-- Restart: solo cuando está corriendo -->
|
||||
<button x-show="isRunning(app.status)"
|
||||
@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-50 text-yellow-700 hover:bg-yellow-100 transition disabled:opacity-50">
|
||||
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">
|
||||
<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-50 text-indigo-700 hover:bg-indigo-100 transition disabled:opacity-50">
|
||||
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">
|
||||
<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-50 text-gray-600 hover:bg-gray-100 transition">
|
||||
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">
|
||||
<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-50 text-gray-600 hover:bg-gray-100 transition">
|
||||
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">
|
||||
<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-50 text-gray-600 hover:bg-gray-100 transition">
|
||||
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">
|
||||
<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>
|
||||
@@ -227,23 +230,22 @@
|
||||
</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 @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 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="openServiceEnvs(svc)"
|
||||
class="text-xs px-2.5 py-1 rounded-lg bg-gray-50 text-gray-600 hover:bg-gray-100 transition">Envs</button>
|
||||
class="text-xs px-2.5 py-1 rounded-lg bg-gray-100 text-gray-600 hover:bg-gray-200 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>
|
||||
@@ -267,11 +269,7 @@
|
||||
</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">
|
||||
@@ -279,12 +277,15 @@
|
||||
<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 @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>
|
||||
<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>
|
||||
</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>
|
||||
@@ -454,17 +455,6 @@
|
||||
</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>
|
||||
@@ -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; }
|
||||
|
||||
@@ -446,31 +446,12 @@
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user