Update coolify.html

This commit is contained in:
Lizandro Guarnizo
2026-05-26 11:36:38 -05:00
parent 304aa33a99
commit d1a668e229
+86 -33
View File
@@ -87,7 +87,7 @@
</div>
<span :class="statusClass(app.status)"
class="text-xs px-2 py-0.5 rounded-full font-medium shrink-0"
x-text="statusLabel(app.status)">
x-text="actionStatusText(app.uuid, app.status)">
</span>
</div>
@@ -158,7 +158,7 @@
<!-- Spinner de acción individual -->
<div x-show="actionLoading[app.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>
Procesando...
<span x-text="actionSpinnerText(app.uuid)"></span>
</div>
</div>
</template>
@@ -230,7 +230,7 @@
</div>
<span :class="statusClass(svc.status)"
class="text-xs px-2 py-0.5 rounded-full font-medium shrink-0"
x-text="statusLabel(svc.status)">
x-text="actionStatusText(svc.uuid, svc.status)">
</span>
</div>
<div x-show="svc.description" class="text-xs text-gray-500" x-text="svc.description"></div>
@@ -249,7 +249,7 @@
</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>
Procesando...
<span x-text="actionSpinnerText(svc.uuid)"></span>
</div>
</div>
</template>
@@ -269,7 +269,7 @@
</div>
<span :class="statusClass(db.status)"
class="text-xs px-2 py-0.5 rounded-full font-medium shrink-0"
x-text="statusLabel(db.status)">
x-text="actionStatusText(db.uuid, db.status)">
</span>
</div>
<div class="text-xs text-gray-500 space-y-0.5">
@@ -289,7 +289,7 @@
</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>
Procesando...
<span x-text="actionSpinnerText(db.uuid)"></span>
</div>
</div>
</template>
@@ -489,10 +489,10 @@
<div class="flex-1 overflow-auto p-5 space-y-2">
<div x-show="envsLoading" class="text-sm text-gray-400 text-center py-4">Cargando variables...</div>
<div x-show="!envsLoading && envsList.length === 0" class="text-sm text-gray-400 text-center py-4">Sin variables de entorno.</div>
<template x-for="env in envsList" :key="env.id">
<template x-for="env in envsList" :key="env.id || env.uuid || env.key || env.name || env">
<div class="flex items-center gap-3 bg-gray-50 rounded-lg px-3 py-2 text-sm">
<span class="font-mono font-semibold text-gray-700 w-40 shrink-0 truncate" x-text="env.key"></span>
<span class="font-mono text-gray-500 flex-1 truncate" x-text="env.is_shown_once ? '••••••••' : (env.value || '')"></span>
<span class="font-mono font-semibold text-gray-700 w-40 shrink-0 truncate" x-text="env.key || env.name || '—'"></span>
<span class="font-mono text-gray-500 flex-1 truncate" x-text="(env.is_shown_once || env.is_masked) ? '••••••••' : (env.value || '')"></span>
<span x-show="env.is_preview" class="text-xs px-1.5 py-0.5 bg-blue-100 text-blue-600 rounded">Preview</span>
</div>
</template>
@@ -677,6 +677,7 @@ document.addEventListener('alpine:init', () => {
// Action loading by uuid
actionLoading: {},
actionPhase: {},
// ─── Config modal ─────────────────────────────
configModal: false,
@@ -744,6 +745,34 @@ document.addEventListener('alpine:init', () => {
return env.description || env.branch || env.fqdn || env.url || '';
},
actionTextFor(action) {
const map = {
start: 'iniciando',
stop: 'deteniendo',
restart: 'reiniciando',
deploy: 'desplegando',
};
return map[action] || 'procesando';
},
actionStatusText(uuid, status) {
return this.actionPhase[uuid] || this.statusLabel(status);
},
actionSpinnerText(uuid) {
return this.actionPhase[uuid] || 'procesando';
},
clearActionState(uuid) {
const nextLoading = { ...this.actionLoading };
delete nextLoading[uuid];
this.actionLoading = nextLoading;
const nextPhase = { ...this.actionPhase };
delete nextPhase[uuid];
this.actionPhase = nextPhase;
},
flash(msg, ok = true) {
this.flashMsg = msg;
this.flashOk = ok;
@@ -967,7 +996,8 @@ document.addEventListener('alpine:init', () => {
try {
const r = await fetch('/app/coolify/deployments');
const j = await r.json();
this.deployments = Array.isArray(j) ? j : (j.data || []);
this.deployments = Array.isArray(j) ? j
: (j.deployments || j.items || j.data || j.result || j.deployments_list || []);
this.lastFetch = this.fmtDate(new Date().toISOString());
} catch(e) { this.flash('Error cargando despliegues: ' + e.message, false); }
finally { this.loading = false; }
@@ -994,20 +1024,25 @@ document.addEventListener('alpine:init', () => {
const prev = { ...this.actionLoading };
prev[uuid] = true;
this.actionLoading = prev;
const phase = this.actionTextFor(action);
this.actionPhase = { ...this.actionPhase, [uuid]: phase };
let clearNow = true;
try {
const r = await fetch(`/app/coolify/apps/${uuid}/${action}`);
const j = await r.json();
if (r.ok) {
this.flash(`App ${action} iniciado correctamente.`);
setTimeout(() => this.loadApps(), 2000);
clearNow = false;
this.flash(`App ${phase} correctamente.`);
setTimeout(async () => {
await this.loadApps();
this.clearActionState(uuid);
}, 2000);
} else {
this.flash((j.message || j.error || `Error en ${action}`), false);
}
} catch(e) { this.flash('Error: ' + e.message, false); }
finally {
const next = { ...this.actionLoading };
next[uuid] = false;
this.actionLoading = next;
if (clearNow) this.clearActionState(uuid);
}
},
@@ -1015,19 +1050,21 @@ document.addEventListener('alpine:init', () => {
const prev = { ...this.actionLoading };
prev[uuid] = true;
this.actionLoading = prev;
this.actionPhase = { ...this.actionPhase, [uuid]: this.actionTextFor('deploy') };
let clearNow = true;
try {
const r = await fetch(`/app/coolify/apps/${uuid}/deploy`, { method: 'POST' });
const j = await r.json();
if (r.ok) {
clearNow = false;
this.flash('Deploy disparado correctamente.');
setTimeout(() => this.clearActionState(uuid), 3000);
} else {
this.flash((j.message || j.error || 'Error al hacer deploy'), false);
}
} catch(e) { this.flash('Error: ' + e.message, false); }
finally {
const next = { ...this.actionLoading };
next[uuid] = false;
this.actionLoading = next;
if (clearNow) this.clearActionState(uuid);
}
},
@@ -1070,7 +1107,7 @@ document.addEventListener('alpine:init', () => {
try {
const r = await fetch(`/app/coolify/apps/${app.uuid}/envs`);
const j = await r.json();
this.envsList = Array.isArray(j) ? j : (j.data || []);
this.envsList = Array.isArray(j) ? j : (j.data || j.variables || j.envs || j.items || j.environment_variables || []);
} catch(e) { this.flash('Error: ' + e.message, false); }
finally { this.envsLoading = false; }
},
@@ -1085,7 +1122,7 @@ document.addEventListener('alpine:init', () => {
try {
const r = await fetch(`/app/coolify/services/${svc.uuid}/envs`);
const j = await r.json();
this.envsList = Array.isArray(j) ? j : (j.data || []);
this.envsList = Array.isArray(j) ? j : (j.data || j.variables || j.envs || j.items || j.environment_variables || []);
} catch(e) { this.flash('Error: ' + e.message, false); }
finally { this.envsLoading = false; }
},
@@ -1110,7 +1147,7 @@ document.addEventListener('alpine:init', () => {
// Recargar lista
const r2 = await fetch(endpoint.replace(/\/envs$/, '/envs'));
const j2 = await r2.json();
this.envsList = Array.isArray(j2) ? j2 : (j2.data || []);
this.envsList = Array.isArray(j2) ? j2 : (j2.data || j2.variables || j2.envs || j2.items || j2.environment_variables || []);
} else {
this.flash(j.message || j.error || 'Error al crear variable.', false);
}
@@ -1127,10 +1164,16 @@ document.addEventListener('alpine:init', () => {
try {
const r = await fetch(`/app/coolify/apps/${app.uuid}/deployments`);
const j = await r.json();
this.appDeploymentsList = Array.isArray(j) ? j : (j.data || []);
let list = Array.isArray(j) ? j
: (j.deployments || j.items || j.data || j.result || j.deployments_list || []);
// Si la respuesta es vacía pero tiene data a nivel superior, prueba parsear como objeto wrapper
if (!Array.isArray(list) && typeof list === 'object' && list !== null) {
list = Object.values(list).filter(v => Array.isArray(v))[0] || [];
}
this.appDeploymentsList = Array.isArray(list) ? list : [];
} catch(e) { this.flash('Error: ' + e.message, false); }
finally { this.appDeploymentsLoading = false; }
},
}
// ─── Deployment log ──────────────────────────────
openDeploymentLog(dep) {
@@ -1143,20 +1186,25 @@ document.addEventListener('alpine:init', () => {
const prev = { ...this.actionLoading };
prev[uuid] = true;
this.actionLoading = prev;
const phase = this.actionTextFor(action);
this.actionPhase = { ...this.actionPhase, [uuid]: phase };
let clearNow = true;
try {
const r = await fetch(`/app/coolify/services/${uuid}/${action}`);
const j = await r.json();
if (r.ok) {
this.flash(`Servicio ${action} iniciado.`);
setTimeout(() => this.loadServices(), 2000);
clearNow = false;
this.flash(`Servicio ${phase} correctamente.`);
setTimeout(async () => {
await this.loadServices();
this.clearActionState(uuid);
}, 2000);
} else {
this.flash(j.message || j.error || `Error en ${action}`, false);
}
} catch(e) { this.flash('Error: ' + e.message, false); }
finally {
const next = { ...this.actionLoading };
next[uuid] = false;
this.actionLoading = next;
if (clearNow) this.clearActionState(uuid);
}
},
@@ -1165,20 +1213,25 @@ document.addEventListener('alpine:init', () => {
const prev = { ...this.actionLoading };
prev[uuid] = true;
this.actionLoading = prev;
const phase = this.actionTextFor(action);
this.actionPhase = { ...this.actionPhase, [uuid]: phase };
let clearNow = true;
try {
const r = await fetch(`/app/coolify/databases/${uuid}/${action}`);
const j = await r.json();
if (r.ok) {
this.flash(`Base de datos ${action} iniciado.`);
setTimeout(() => this.loadDatabases(), 2000);
clearNow = false;
this.flash(`Base de datos ${phase} correctamente.`);
setTimeout(async () => {
await this.loadDatabases();
this.clearActionState(uuid);
}, 2000);
} else {
this.flash(j.message || j.error || `Error en ${action}`, false);
}
} catch(e) { this.flash('Error: ' + e.message, false); }
finally {
const next = { ...this.actionLoading };
next[uuid] = false;
this.actionLoading = next;
if (clearNow) this.clearActionState(uuid);
}
},