diff --git a/panel/public/assets/js/user.js b/panel/public/assets/js/user.js index a507ba4..01f0f76 100644 --- a/panel/public/assets/js/user.js +++ b/panel/public/assets/js/user.js @@ -1058,35 +1058,34 @@ window.submitChangePassword = async () => { /* ── Docker (#34) ────────────────────────────────────────────────────────── */ async function dockerPage(el) { el.innerHTML = '
Loading Docker…
'; - const [contRes, quotaRes, catRes] = await Promise.all([ - Nova.api('docker', 'containers'), + const [stackRes, quotaRes, catRes] = await Promise.all([ + Nova.api('docker', 'stacks'), Nova.api('docker', 'quota-get'), Nova.api('docker', 'catalog'), ]); - const containers = contRes?.data?.containers || []; - const quota = quotaRes?.data?.quota || { max_containers: 2, max_memory_mb: 512, max_cpus: 1.0 }; - const catalog = catRes?.data?.catalog || {}; - const used = containers.length; + const stacks = stackRes?.data?.stacks || []; + const quota = quotaRes?.data?.quota || { max_containers: 2, max_memory_mb: 512, max_cpus: 1.0 }; + const catalog = catRes?.data?.catalog || {}; el.innerHTML = ` - +
-
Containers Used
${used} / ${quota.max_containers}
${Nova.progressBar(Math.round(used/Math.max(quota.max_containers,1)*100))}
-
Max Memory / Container
${quota.max_memory_mb} MB
-
Max CPUs / Container
${quota.max_cpus}
+
Apps Deployed
${stacks.length} / ${quota.max_containers}
${Nova.progressBar(Math.round(stacks.length/Math.max(quota.max_containers,1)*100))}
+
Max Memory / App
${quota.max_memory_mb} MB
+
Max CPUs / App
${quota.max_cpus}
- +
Loading…
`; - window._uDockerContainers = containers; - window._uDockerQuota = quota; - window._uDockerCatalog = catalog; - window._uDockerTab = window._uDockerTab || 'my-containers'; + window._uDockerStacks = stacks; + window._uDockerQuota = quota; + window._uDockerCatalog = catalog; + window._uDockerTab = window._uDockerTab || 'my-apps'; window.uDockerTab = async (tab) => { window._uDockerTab = tab; @@ -1094,44 +1093,56 @@ async function dockerPage(el) { const t = b.getAttribute('onclick').match(/'([^']+)'/)?.[1]; b.className = 'btn btn-sm ' + (t === tab ? 'btn-primary' : 'btn-ghost'); }); - uDockerLoadTab(tab); + if (tab === 'my-apps') await uDockerReloadStacks(); + else uDockerLoadTab(tab); }; - uDockerLoadTab(window._uDockerTab); + if (window._uDockerTab === 'my-apps') await uDockerReloadStacks(); + else uDockerLoadTab(window._uDockerTab); } -window._uDockerTab = 'my-containers'; +window._uDockerTab = 'my-apps'; + +async function uDockerReloadStacks() { + const r = await Nova.api('docker', 'stacks'); + window._uDockerStacks = r?.data?.stacks || []; + uDockerLoadTab('my-apps'); +} function uDockerLoadTab(tab) { const tc = document.getElementById('udocker-content'); if (!tc) return; - const containers = window._uDockerContainers || []; - const catalog = window._uDockerCatalog || {}; - const quota = window._uDockerQuota || {}; + const stacks = window._uDockerStacks || []; + const catalog = window._uDockerCatalog || {}; + const quota = window._uDockerQuota || {}; - if (tab === 'my-containers') { + if (tab === 'my-apps') { + const statusColor = s => s==='running'?'green':s==='starting'?'yellow':s==='stopped'?'red':'yellow'; tc.innerHTML = `
- ${containers.length} container${containers.length===1?'':'s'} - + ${stacks.length} app${stacks.length===1?'':'s'} +
+ + +
-${containers.length === 0 +${stacks.length === 0 ? `
🐳
-

No containers yet. Launch an app from the catalog!

+

No apps yet. Launch one from the catalog!

` - : `
-${containers.map(c=>` - - - + : `
NameAppStatusActions
${Nova.escHtml(c.name)}${Nova.escHtml(c.app_key||c.image||'—')}${Nova.badge(c.status, c.status==='running'?'green':c.status==='stopped'?'red':'yellow')}
+${stacks.map(s=>` + + + `).join('')}
AppStatusCreatedActions
${Nova.escHtml(s.name)}${Nova.badge(s.status, statusColor(s.status))}${Nova.relTime(s.created_at)} - ${c.status==='running' - ? ` - ` - : ``} - + ${s.status==='running' + ? `` + : ``} + +
`}`; @@ -1153,21 +1164,29 @@ ${Object.entries(catalog).map(([key,app])=>` } } -window.uDockerAct = async (cid, action) => { - Nova.loading(`${action.charAt(0).toUpperCase()+action.slice(1)}ing container…`); - const r = await Nova.api('docker', 'container-action', { method: 'POST', body: { container_id: cid, action } }); +window.uStackAct = async (stackId, action) => { + const label = action === 'up' ? 'Starting' : 'Stopping'; + Nova.loading(`${label} app…`); + const r = await Nova.api('docker', 'stack-action', { method: 'POST', body: { stack_id: stackId, action } }); Nova.loadingDone(); - Nova.toast(r?.success ? `Container ${action}ed` : (r?.message||'Failed'), r?.success?'success':'error'); - if (r?.success) { - const c = (window._uDockerContainers||[]).find(x=>x.container_id===cid); - if (c) c.status = action==='stop'?'stopped':'running'; - uDockerLoadTab('my-containers'); - } + Nova.toast(r?.success ? `App ${action==='up'?'started':'stopped'}` : (r?.message||'Failed'), r?.success?'success':'error'); + if (r?.success) await uDockerReloadStacks(); }; -window.uDockerLogs = async (cid, name) => { - const r = await Nova.api('docker', 'container-logs', { params: { container_id: cid, lines: 100 } }); - Nova.modal(`Logs: ${name}`, `
${Nova.escHtml(r?.data?.logs||'No logs available')}
`); +window.uStackLogs = async (stackId, name) => { + Nova.loading('Fetching logs…'); + const r = await Nova.api('docker', 'stack-action', { method: 'POST', body: { stack_id: stackId, action: 'logs' } }); + Nova.loadingDone(); + Nova.modal(`Logs: ${name}`, `
${Nova.escHtml(r?.data?.output||'No logs available')}
`); +}; + +window.uStackRemove = async (stackId, name) => { + if (!confirm(`Remove app "${name}"? This will stop and delete its containers and data.`)) return; + Nova.loading('Removing app…'); + const r = await Nova.api('docker', 'remove-stack', { method: 'POST', body: { stack_id: stackId } }); + Nova.loadingDone(); + Nova.toast(r?.success ? 'App removed' : (r?.message||'Failed'), r?.success?'success':'error'); + if (r?.success) await uDockerReloadStacks(); }; window.uDockerLaunchModal = () => uDockerLaunchApp(null); @@ -1210,11 +1229,9 @@ window.uDockerLaunchApp = async (preselect) => { Nova.loading(`Launching ${app.name}… this may take a minute`); const r = await Nova.api('docker', 'launch', { method: 'POST', body: { app_key: key, params } }); Nova.loadingDone(); - Nova.toast(r?.success ? `${app.name} launched!` : (r?.message||'Launch failed'), r?.success?'success':'error'); + Nova.toast(r?.success ? `${app.name} launching — refresh in a moment to see status` : (r?.message||'Launch failed'), r?.success?'success':'error'); if (r?.success) { - const cr = await Nova.api('docker', 'containers'); - window._uDockerContainers = cr?.data?.containers || []; - uDockerTab('my-containers'); + await uDockerTab('my-apps'); } }; };