feat: soporte multi-instancia Coolify en panel web
This commit is contained in:
+215
-72
@@ -13,22 +13,24 @@
|
||||
<h1 class="text-2xl font-bold">Coolify</h1>
|
||||
<p class="text-xs text-slate-500 mt-0.5">Gestión completa de aplicaciones, servicios, bases de datos, servidores y despliegues a través de la API de Coolify.</p>
|
||||
</div>
|
||||
<button @click="openConfigModal()"
|
||||
class="flex items-center gap-2 text-white text-sm font-medium px-4 py-2 rounded-lg"
|
||||
style="background-color:#6366f1"
|
||||
onmouseover="this.style.backgroundColor='#4f46e5'"
|
||||
onmouseout="this.style.backgroundColor='#6366f1'">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
Configurar API
|
||||
</button>
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<!-- Selector de instancia -->
|
||||
<select x-show="configs.length > 1" x-model="selectedConfigId" @change="onInstanceChange()"
|
||||
class="text-sm border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-300">
|
||||
<template x-for="cfg in configs" :key="cfg.ID">
|
||||
<option :value="cfg.ID" x-text="cfg.Nombre || cfg.nombre || cfg.base_url"></option>
|
||||
</template>
|
||||
</select>
|
||||
<button @click="setTab('instancias')"
|
||||
class="flex items-center gap-2 text-sm font-medium px-4 py-2 rounded-lg border border-indigo-200 text-indigo-600 hover:bg-indigo-50 transition">
|
||||
+ Nueva instancia
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Alerta sin config -->
|
||||
<div x-show="!hasConfig" class="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-700">
|
||||
<strong>Sin configuración activa.</strong> Haz clic en "Configurar API" para ingresar la URL base y el token de tu instancia de Coolify.
|
||||
<div x-show="!hasConfig && activeTab !== 'instancias'" class="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg text-sm text-yellow-700">
|
||||
<strong>Sin instancia activa.</strong> Haz clic en "+ Nueva instancia" para agregar tu primera instancia de Coolify.
|
||||
El token lo generas en <strong>Coolify → Keys & Tokens → API tokens</strong>.
|
||||
</div>
|
||||
|
||||
@@ -517,6 +519,106 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════ TAB: INSTANCIAS ══════════════════════ -->
|
||||
<div x-show="activeTab==='instancias'">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-base font-semibold text-gray-700">Instancias de Coolify</h2>
|
||||
<button @click="openInstModal()"
|
||||
class="flex items-center gap-2 text-sm font-medium px-4 py-2 rounded-lg text-white"
|
||||
style="background-color:#6366f1"
|
||||
onmouseover="this.style.backgroundColor='#4f46e5'"
|
||||
onmouseout="this.style.backgroundColor='#6366f1'">
|
||||
+ Nueva instancia
|
||||
</button>
|
||||
</div>
|
||||
<div x-show="configs.length === 0" class="text-sm text-gray-400 py-8 text-center">
|
||||
No hay instancias configuradas. Haz clic en "+ Nueva instancia" para agregar la primera.
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<template x-for="cfg in configs" :key="cfg.ID || cfg.id">
|
||||
<div class="flex items-center justify-between bg-gray-50 border rounded-xl px-4 py-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<span :class="(cfg.Activo || cfg.activo) ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-400'"
|
||||
class="text-xs px-2 py-0.5 rounded-full font-medium"
|
||||
x-text="(cfg.Activo || cfg.activo) ? 'Activa' : 'Inactiva'">
|
||||
</span>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-gray-800" x-text="cfg.Nombre || cfg.nombre || 'Sin nombre'"></p>
|
||||
<p class="text-xs text-gray-400 font-mono" x-text="cfg.BaseURL || cfg.base_url"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<button @click="selectedConfigId = cfg.ID || cfg.id; flash('Instancia seleccionada: ' + (cfg.Nombre || cfg.nombre))"
|
||||
:class="selectedConfigId === (cfg.ID || cfg.id) ? 'bg-indigo-100 text-indigo-700 border-indigo-200' : 'bg-white text-gray-600 border-gray-200'"
|
||||
class="text-xs px-3 py-1.5 rounded-lg border font-medium hover:bg-indigo-50 transition">
|
||||
Usar
|
||||
</button>
|
||||
<button @click="testInst(cfg.ID || cfg.id)"
|
||||
class="text-xs px-3 py-1.5 rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-gray-100 transition">
|
||||
Probar
|
||||
</button>
|
||||
<button @click="openInstModal(cfg)"
|
||||
class="text-xs px-3 py-1.5 rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-gray-100 transition">
|
||||
Editar
|
||||
</button>
|
||||
<button @click="deleteInst(cfg.ID || cfg.id)"
|
||||
class="text-xs px-3 py-1.5 rounded-lg border border-red-100 bg-white text-red-500 hover:bg-red-50 transition">
|
||||
Eliminar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<p class="text-xs text-gray-400 mt-4">
|
||||
La instancia activa se selecciona automáticamente. Puedes cambiarla con el selector del encabezado o con el botón "Usar".
|
||||
Todas las peticiones a Coolify (apps, servidores, servicios, etc.) se envían a la instancia seleccionada.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════ MODAL: CREAR / EDITAR INSTANCIA ══════════════════════ -->
|
||||
<div x-show="instModal" x-transition class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div @click.outside="instModal = false" class="bg-white rounded-2xl shadow-xl w-full max-w-md">
|
||||
<div class="p-5 border-b flex items-center justify-between">
|
||||
<h2 class="text-base font-bold" x-text="instEditId ? 'Editar instancia' : 'Nueva instancia Coolify'"></h2>
|
||||
<button @click="instModal = false" class="text-gray-400 hover:text-gray-700 text-xl leading-none">×</button>
|
||||
</div>
|
||||
<div class="p-5 space-y-4">
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">Nombre</label>
|
||||
<input x-model="instForm.nombre" type="text" placeholder="Ej: Producción, Staging..."
|
||||
class="w-full text-sm border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-300">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">URL base <span class="text-red-400">*</span></label>
|
||||
<input x-model="instForm.base_url" type="url" placeholder="https://coolify.tudominio.com"
|
||||
class="w-full text-sm border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-300">
|
||||
<p class="text-xs text-gray-400 mt-1">Sin barra final. El sistema agrega <code>/api/v1</code> automáticamente.</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-600 mb-1">API Token <span x-show="instEditId" class="text-gray-400">(dejar vacío para no cambiar)</span><span x-show="!instEditId" class="text-red-400">*</span></label>
|
||||
<input x-model="instForm.api_token" type="password" placeholder="Token de Coolify → Keys & Tokens → API tokens"
|
||||
class="w-full text-sm border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-300">
|
||||
</div>
|
||||
<label class="flex items-center gap-2 cursor-pointer">
|
||||
<input x-model="instForm.activo" type="checkbox" class="rounded">
|
||||
<span class="text-sm text-gray-700">Instancia activa</span>
|
||||
</label>
|
||||
<div x-show="instMsg" :class="instOk ? 'text-green-600 bg-green-50' : 'text-red-600 bg-red-50'"
|
||||
class="text-xs rounded-lg px-3 py-2" x-text="instMsg"></div>
|
||||
</div>
|
||||
<div class="p-5 border-t flex items-center justify-end gap-3">
|
||||
<button @click="instModal = false" class="text-sm px-4 py-2 rounded-lg border text-gray-600 hover:bg-gray-50">Cancelar</button>
|
||||
<button @click="saveInst()" :disabled="instLoading"
|
||||
class="text-sm px-4 py-2 rounded-lg text-white font-medium disabled:opacity-50"
|
||||
style="background-color:#6366f1"
|
||||
onmouseover="this.style.backgroundColor='#4f46e5'"
|
||||
onmouseout="this.style.backgroundColor='#6366f1'"
|
||||
x-text="instLoading ? 'Guardando...' : (instEditId ? 'Actualizar' : 'Crear instancia')">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══════════════════════ MODAL: DEPLOYMENTS HISTORIAL ══════════════════════ -->
|
||||
<div x-show="appDeploymentsModal" x-transition class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
|
||||
<div @click.outside="appDeploymentsModal = false" class="bg-white rounded-2xl shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
|
||||
@@ -660,8 +762,13 @@ document.addEventListener('alpine:init', () => {
|
||||
{ id: 'projects', label: 'Proyectos' },
|
||||
{ id: 'deployments', label: 'Despliegues' },
|
||||
{ id: 'team', label: 'Equipo' },
|
||||
{ id: 'instancias', label: 'Instancias' },
|
||||
],
|
||||
|
||||
// ─── Multi-instancia ──────────────────────────
|
||||
configs: [],
|
||||
selectedConfigId: null,
|
||||
|
||||
// Datos por tab
|
||||
apps: [],
|
||||
servers: [],
|
||||
@@ -679,13 +786,21 @@ document.addEventListener('alpine:init', () => {
|
||||
actionLoading: {},
|
||||
actionPhase: {},
|
||||
|
||||
// ─── Config modal ─────────────────────────────
|
||||
// ─── Config modal (legado) ────────────────────
|
||||
configModal: false,
|
||||
cfgLoading: false,
|
||||
cfgMsg: '',
|
||||
cfgOk: false,
|
||||
cfgForm: { nombre: '', base_url: '', api_token: '', activo: true },
|
||||
|
||||
// ─── Instancia modal ──────────────────────────
|
||||
instModal: false,
|
||||
instLoading: false,
|
||||
instMsg: '',
|
||||
instOk: false,
|
||||
instEditId: null,
|
||||
instForm: { nombre: '', base_url: '', api_token: '', activo: true },
|
||||
|
||||
// ─── Logs modal ───────────────────────────────
|
||||
logsModal: false,
|
||||
logsLoading: false,
|
||||
@@ -729,10 +844,19 @@ document.addEventListener('alpine:init', () => {
|
||||
|
||||
// ═════════════════════════════════════════════
|
||||
async init() {
|
||||
await this.loadConfig();
|
||||
await this.loadConfigs();
|
||||
if (this.hasConfig) this.loadTab();
|
||||
},
|
||||
|
||||
// Retorna el querystring para seleccionar instancia: ?config_id=X
|
||||
configQs(sep = '?') {
|
||||
return this.selectedConfigId ? `${sep}config_id=${this.selectedConfigId}` : '';
|
||||
},
|
||||
|
||||
onInstanceChange() {
|
||||
if (this.activeTab !== 'instancias') this.loadTab();
|
||||
},
|
||||
|
||||
// ─── Helpers ────────────────────────────────────
|
||||
projectEnvLabel(env) {
|
||||
if (!env) return '—';
|
||||
@@ -843,72 +967,89 @@ document.addEventListener('alpine:init', () => {
|
||||
return 'bg-gray-100 text-gray-500';
|
||||
},
|
||||
|
||||
// ─── Config ──────────────────────────────────────
|
||||
async loadConfig() {
|
||||
const r = await fetch('/app/coolify/config');
|
||||
const j = await r.json();
|
||||
if (j.config) {
|
||||
this.hasConfig = j.config.activo;
|
||||
this.cfgForm.nombre = j.config.nombre || '';
|
||||
this.cfgForm.base_url = j.config.base_url || '';
|
||||
this.cfgForm.activo = j.config.activo;
|
||||
// ─── Instancias ───────────────────────────────────
|
||||
async loadConfigs() {
|
||||
try {
|
||||
const r = await fetch('/app/coolify/configs');
|
||||
const j = await r.json();
|
||||
this.configs = j.items || [];
|
||||
const active = this.configs.find(c => c.Activo || c.activo);
|
||||
if (active && !this.selectedConfigId) {
|
||||
this.selectedConfigId = active.ID || active.id;
|
||||
}
|
||||
this.hasConfig = this.configs.some(c => c.Activo || c.activo);
|
||||
} catch(e) {
|
||||
this.hasConfig = false;
|
||||
}
|
||||
},
|
||||
|
||||
openConfigModal() {
|
||||
this.cfgMsg = '';
|
||||
this.configModal = true;
|
||||
openInstModal(cfg = null) {
|
||||
this.instMsg = '';
|
||||
this.instEditId = cfg ? (cfg.ID || cfg.id) : null;
|
||||
this.instForm = cfg
|
||||
? { nombre: cfg.Nombre || cfg.nombre || '', base_url: cfg.BaseURL || cfg.base_url || '', api_token: '', activo: cfg.Activo ?? cfg.activo ?? true }
|
||||
: { nombre: '', base_url: '', api_token: '', activo: true };
|
||||
this.instModal = true;
|
||||
},
|
||||
|
||||
async saveConfig() {
|
||||
this.cfgLoading = true;
|
||||
this.cfgMsg = '';
|
||||
async saveInst() {
|
||||
this.instLoading = true;
|
||||
this.instMsg = '';
|
||||
try {
|
||||
const r = await fetch('/app/coolify/config', {
|
||||
method: 'POST',
|
||||
const isEdit = !!this.instEditId;
|
||||
const url = isEdit ? `/app/coolify/configs/${this.instEditId}` : '/app/coolify/configs';
|
||||
const meth = isEdit ? 'PUT' : 'POST';
|
||||
const r = await fetch(url, {
|
||||
method: meth,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(this.cfgForm)
|
||||
body: JSON.stringify(this.instForm)
|
||||
});
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
this.cfgOk = true;
|
||||
this.cfgMsg = 'Configuración guardada correctamente.';
|
||||
this.hasConfig = this.cfgForm.activo;
|
||||
if (this.hasConfig) this.loadTab();
|
||||
setTimeout(() => { this.configModal = false; }, 1200);
|
||||
this.instOk = true;
|
||||
this.instMsg = isEdit ? 'Instancia actualizada.' : 'Instancia creada.';
|
||||
await this.loadConfigs();
|
||||
if (!isEdit && j.id) this.selectedConfigId = j.id;
|
||||
setTimeout(() => { this.instModal = false; if (this.hasConfig && this.activeTab !== 'instancias') this.loadTab(); }, 1200);
|
||||
} else {
|
||||
this.cfgOk = false;
|
||||
this.cfgMsg = j.error || 'Error al guardar.';
|
||||
this.instOk = false;
|
||||
this.instMsg = j.error || 'Error al guardar.';
|
||||
}
|
||||
} catch(e) {
|
||||
this.cfgOk = false;
|
||||
this.cfgMsg = 'Error de red: ' + e.message;
|
||||
this.instOk = false;
|
||||
this.instMsg = 'Error de red: ' + e.message;
|
||||
} finally {
|
||||
this.cfgLoading = false;
|
||||
this.instLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async testConnection() {
|
||||
this.cfgLoading = true;
|
||||
this.cfgMsg = '';
|
||||
try {
|
||||
const r = await fetch('/app/coolify/health');
|
||||
const j = await r.json();
|
||||
if (r.ok) {
|
||||
this.cfgOk = true;
|
||||
this.cfgMsg = '✓ Conexión exitosa con Coolify.';
|
||||
} else {
|
||||
this.cfgOk = false;
|
||||
this.cfgMsg = 'Error ' + r.status + ': ' + (j.message || j.error || JSON.stringify(j));
|
||||
}
|
||||
} catch(e) {
|
||||
this.cfgOk = false;
|
||||
this.cfgMsg = 'Error de red: ' + e.message;
|
||||
} finally {
|
||||
this.cfgLoading = false;
|
||||
async deleteInst(id) {
|
||||
if (!confirm('¿Eliminar esta instancia de Coolify?')) return;
|
||||
const r = await fetch(`/app/coolify/configs/${id}`, { method: 'DELETE' });
|
||||
const j = await r.json();
|
||||
if (j.ok) {
|
||||
if (this.selectedConfigId === id) this.selectedConfigId = null;
|
||||
await this.loadConfigs();
|
||||
this.flash('Instancia eliminada.');
|
||||
} else {
|
||||
this.flash(j.error || 'Error al eliminar.', false);
|
||||
}
|
||||
},
|
||||
|
||||
async testInst(id) {
|
||||
const r = await fetch(`/app/coolify/configs/${id}/test`);
|
||||
if (r.ok) {
|
||||
this.flash('✓ Conexión exitosa con la instancia.');
|
||||
} else {
|
||||
const j = await r.json().catch(() => ({}));
|
||||
this.flash('Error: ' + (j.error || r.status), false);
|
||||
}
|
||||
},
|
||||
|
||||
// Legado: mantiene compatibilidad con código existente
|
||||
openConfigModal() { this.openInstModal(); },
|
||||
async testConnection() { if (this.selectedConfigId) await this.testInst(this.selectedConfigId); },
|
||||
|
||||
// ─── Tab navigation ──────────────────────────────
|
||||
setTab(tab) {
|
||||
this.activeTab = tab;
|
||||
@@ -916,6 +1057,7 @@ document.addEventListener('alpine:init', () => {
|
||||
},
|
||||
|
||||
loadTab() {
|
||||
if (this.activeTab === 'instancias') return; // se carga en init
|
||||
if (!this.hasConfig) return;
|
||||
const map = {
|
||||
apps: () => this.loadApps(),
|
||||
@@ -934,11 +1076,11 @@ document.addEventListener('alpine:init', () => {
|
||||
async loadApps() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const qs = this.appTagFilter ? `?tag=${encodeURIComponent(this.appTagFilter)}` : '';
|
||||
let qs = this.configQs();
|
||||
if (this.appTagFilter) qs += (qs ? '&' : '?') + `tag=${encodeURIComponent(this.appTagFilter)}`;
|
||||
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); }
|
||||
@@ -948,7 +1090,7 @@ document.addEventListener('alpine:init', () => {
|
||||
async loadServers() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const r = await fetch('/app/coolify/servers');
|
||||
const r = await fetch('/app/coolify/servers' + this.configQs());
|
||||
const j = await r.json();
|
||||
this.servers = Array.isArray(j) ? j : (j.data || []);
|
||||
this.lastFetch = this.fmtDate(new Date().toISOString());
|
||||
@@ -959,7 +1101,7 @@ document.addEventListener('alpine:init', () => {
|
||||
async loadServices() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const r = await fetch('/app/coolify/services');
|
||||
const r = await fetch('/app/coolify/services' + this.configQs());
|
||||
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;
|
||||
@@ -971,7 +1113,7 @@ document.addEventListener('alpine:init', () => {
|
||||
async loadDatabases() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const r = await fetch('/app/coolify/databases');
|
||||
const r = await fetch('/app/coolify/databases' + this.configQs());
|
||||
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;
|
||||
@@ -983,7 +1125,7 @@ document.addEventListener('alpine:init', () => {
|
||||
async loadProjects() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const r = await fetch('/app/coolify/projects');
|
||||
const r = await fetch('/app/coolify/projects' + this.configQs());
|
||||
const j = await r.json();
|
||||
this.projects = Array.isArray(j) ? j : (j.data || []);
|
||||
this.lastFetch = this.fmtDate(new Date().toISOString());
|
||||
@@ -994,7 +1136,7 @@ document.addEventListener('alpine:init', () => {
|
||||
async loadDeployments() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const r = await fetch('/app/coolify/deployments');
|
||||
const r = await fetch('/app/coolify/deployments' + this.configQs());
|
||||
const j = await r.json();
|
||||
this.deployments = Array.isArray(j) ? j
|
||||
: (j.deployments || j.items || j.data || j.result || j.deployments_list || []);
|
||||
@@ -1006,9 +1148,10 @@ document.addEventListener('alpine:init', () => {
|
||||
async loadTeam() {
|
||||
this.loading = true;
|
||||
try {
|
||||
const qs = this.configQs();
|
||||
const [rt, rm] = await Promise.all([
|
||||
fetch('/app/coolify/team'),
|
||||
fetch('/app/coolify/team/members'),
|
||||
fetch('/app/coolify/team' + qs),
|
||||
fetch('/app/coolify/team/members' + qs),
|
||||
]);
|
||||
const jt = await rt.json();
|
||||
const jm = await rm.json();
|
||||
@@ -1028,7 +1171,7 @@ document.addEventListener('alpine:init', () => {
|
||||
this.actionPhase = { ...this.actionPhase, [uuid]: phase };
|
||||
let clearNow = true;
|
||||
try {
|
||||
const r = await fetch(`/app/coolify/apps/${uuid}/${action}`);
|
||||
const r = await fetch(`/app/coolify/apps/${uuid}/${action}${this.configQs()}`);
|
||||
const j = await r.json();
|
||||
if (r.ok) {
|
||||
clearNow = false;
|
||||
@@ -1053,7 +1196,7 @@ document.addEventListener('alpine:init', () => {
|
||||
this.actionPhase = { ...this.actionPhase, [uuid]: this.actionTextFor('deploy') };
|
||||
let clearNow = true;
|
||||
try {
|
||||
const r = await fetch(`/app/coolify/apps/${uuid}/deploy`, { method: 'POST' });
|
||||
const r = await fetch(`/app/coolify/apps/${uuid}/deploy${this.configQs()}`, { method: 'POST' });
|
||||
const j = await r.json();
|
||||
if (r.ok) {
|
||||
clearNow = false;
|
||||
|
||||
@@ -113,11 +113,11 @@ func coolifyProxy(c *fiber.Ctx, method, endpoint string) error {
|
||||
// ─── Página principal ─────────────────────────────────────────────────────────
|
||||
|
||||
func CoolifyIndex(c *fiber.Ctx) error {
|
||||
cfg, _ := models.GetCoolifyConfig()
|
||||
cfgs, _ := models.GetAllCoolifyConfigs()
|
||||
return c.Render("coolify", fiber.Map{
|
||||
"user": c.Locals("user").(map[string]interface{}),
|
||||
"modules": c.Locals("userModules"),
|
||||
"config": cfg,
|
||||
"configs": cfgs,
|
||||
}, "layouts/main")
|
||||
}
|
||||
|
||||
|
||||
@@ -192,6 +192,13 @@ func UserRoutes(app fiber.Router) {
|
||||
|
||||
// ─── Coolify API ──────────────────────────────────────────────────
|
||||
protected.Get("/coolify", middlewares.MenuMiddleware, controllers.CoolifyIndex)
|
||||
// Gestión de instancias Coolify (multi-instancia)
|
||||
protected.Get("/coolify/configs", controllers.CoolifyListConfigs)
|
||||
protected.Post("/coolify/configs", controllers.CoolifyCreateConfig)
|
||||
protected.Put("/coolify/configs/:id", controllers.CoolifyUpdateConfig)
|
||||
protected.Delete("/coolify/configs/:id", controllers.CoolifyDeleteConfig)
|
||||
protected.Get("/coolify/configs/:id/test", controllers.CoolifyTestConfig)
|
||||
// Compat legado (upsert sobre primer registro)
|
||||
protected.Get("/coolify/config", controllers.CoolifyGetConfig)
|
||||
protected.Post("/coolify/config", controllers.CoolifySaveConfig)
|
||||
protected.Get("/coolify/health", controllers.CoolifyTestConnection)
|
||||
|
||||
Reference in New Issue
Block a user