diff --git a/resources/views/coolify.html b/resources/views/coolify.html index 6b4b38f..5f943f7 100644 --- a/resources/views/coolify.html +++ b/resources/views/coolify.html @@ -13,22 +13,24 @@

Coolify

Gestión completa de aplicaciones, servicios, bases de datos, servidores y despliegues a través de la API de Coolify.

- +
+ + + +
-
- Sin configuración activa. Haz clic en "Configurar API" para ingresar la URL base y el token de tu instancia de Coolify. +
+ Sin instancia activa. Haz clic en "+ Nueva instancia" para agregar tu primera instancia de Coolify. El token lo generas en Coolify → Keys & Tokens → API tokens.
@@ -517,6 +519,106 @@
+ +
+
+

Instancias de Coolify

+ +
+
+ No hay instancias configuradas. Haz clic en "+ Nueva instancia" para agregar la primera. +
+
+ +
+

+ 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. +

+
+ + +
+
+
+

+ +
+
+
+ + +
+
+ + +

Sin barra final. El sistema agrega /api/v1 automáticamente.

+
+
+ + +
+ +
+
+
+ + +
+
+
+
@@ -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; diff --git a/rest/controllers/coolify_controller.go b/rest/controllers/coolify_controller.go index d80a82d..82d7398 100644 --- a/rest/controllers/coolify_controller.go +++ b/rest/controllers/coolify_controller.go @@ -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") } diff --git a/rest/routes/user.go b/rest/routes/user.go index 5e95b76..2381d49 100755 --- a/rest/routes/user.go +++ b/rest/routes/user.go @@ -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)