diff --git a/panel/api/endpoints/docker.php b/panel/api/endpoints/docker.php index 75122e2..0fc6ee6 100644 --- a/panel/api/endpoints/docker.php +++ b/panel/api/endpoints/docker.php @@ -29,6 +29,36 @@ match ($action) { Response::success(null, $msg); })(), + 'uninstall-account' => (function() use ($dm, $currentUser, $isAdmin, $_userAccountId, $body) { + // Stop and remove all containers and stacks belonging to one account. + // Users can only remove their own; admins can specify any account_id. + $accountId = $isAdmin ? (int)($body['account_id'] ?? $_userAccountId) : ($_userAccountId ?? 0); + if (!$accountId) Response::error('account_id required'); + + $db = DB::getInstance(); + + // Tear down compose stacks + $stacks = $db->fetchAll("SELECT * FROM docker_compose_stacks WHERE account_id = ?", [$accountId]); + foreach ($stacks as $stack) { + if (is_dir($stack['stack_dir']) && file_exists("{$stack['stack_dir']}/docker-compose.yml")) { + shell_exec("sudo docker compose -f " . escapeshellarg("{$stack['stack_dir']}/docker-compose.yml") . " down -v 2>/dev/null"); + } + $db->execute("DELETE FROM docker_compose_stacks WHERE id = ?", [$stack['id']]); + } + + // Stop and remove bare containers + $containers = $db->fetchAll("SELECT container_id FROM docker_containers WHERE account_id = ?", [$accountId]); + foreach ($containers as $c) { + if ($c['container_id']) { + shell_exec("sudo docker rm -f " . escapeshellarg($c['container_id']) . " 2>/dev/null"); + } + } + $db->execute("DELETE FROM docker_containers WHERE account_id = ?", [$accountId]); + + audit("docker.uninstall-account", "account:{$accountId}"); + Response::success(null, 'All Docker apps and containers removed for this account'); + })(), + 'prune' => (function() use ($dm, $body, $isAdmin) { if (!$isAdmin) Response::error('Admin only', 403); $out = $dm->systemPrune((bool)($body['volumes'] ?? false)); diff --git a/panel/public/assets/js/admin.js b/panel/public/assets/js/admin.js index a8e9ee6..b74715b 100644 --- a/panel/public/assets/js/admin.js +++ b/panel/public/assets/js/admin.js @@ -4047,8 +4047,8 @@ async function docker() { ${(status.disk||[]).map(d=>`
${Nova.escHtml(d.Type||d.type||'?')}
${Nova.escHtml(d.TotalCount||d.Size||'—')}
${Nova.escHtml(d.Reclaimable||d.reclaimable||'')}
`).join('')}
- ${tab('containers','Containers')} ${tab('images','Images')} ${tab('volumes','Volumes')} ${tab('networks','Networks')} ${tab('stacks','Compose Stacks')} ${tab('quotas','User Quotas')} - + ${tab('containers','Containers')} ${tab('images','Images')} ${tab('volumes','Volumes')} ${tab('networks','Networks')} ${tab('stacks','Compose Stacks')} ${tab('catalog','App Catalog')} ${tab('quotas','User Quotas')} +
Loading…
`; } @@ -4148,6 +4148,23 @@ ${stacks.map(s=>` `).join('')} `}`; + } else if (tab === 'catalog') { + const r = await Nova.api('docker', 'catalog'); + const catalog = r?.data?.catalog || {}; + tc.innerHTML = ` +

One-click app deployment. Each app runs as an isolated Docker Compose stack. Select an account after clicking Launch.

+
+${Object.entries(catalog).map(([key,app])=>` +
+
+
${Nova.escHtml(app.icon)}
+
${Nova.escHtml(app.name)}
+
${Nova.escHtml(app.description)}
+ +
+
`).join('')} +
`; + } else if (tab === 'quotas') { const r = await Nova.api('accounts', 'list', { params: { limit: 200 } }); const users = r?.data || []; @@ -4165,6 +4182,47 @@ ${users.map(u=>` } } +window.dockerAdminLaunchApp = async (preselect) => { + const catRes = await Nova.api('docker', 'catalog'); + const catalog = catRes?.data?.catalog || {}; + const acctRes = await Nova.api('accounts', 'list', { params: { limit: 200 } }); + const accounts = acctRes?.data || []; + const appOpts = Object.entries(catalog).map(([k,a])=>``).join(''); + const acctOpts = accounts.map(a=>``).join(''); + + window.dockerAdminUpdateParams = (key) => { + const app = catalog[key]; if (!app) return; + const tc = document.getElementById('dal-params'); if (!tc) return; + tc.innerHTML = (app.params||[]).map(p=>` +
+
`).join(''); + }; + + const ov = Nova.modal('Launch App (Admin)', + `
+
+
`, + ` + ` + ); + dockerAdminUpdateParams(preselect || Object.keys(catalog)[0] || ''); + + window.dockerAdminLaunchSubmit = async () => { + const appKey = document.getElementById('dal-app').value; + const accountId = parseInt(document.getElementById('dal-account').value); + const app = catalog[appKey]; if (!app) return; + const params = {}; + (app.params||[]).forEach(p => { const el = document.getElementById(`dal-${p.key}`); if (el) params[p.key] = el.value.trim(); }); + const missing = (app.params||[]).filter(p => p.required && !params[p.key]); + if (missing.length) { Nova.toast(`Required: ${missing.map(p=>p.label).join(', ')}`, 'error'); return; } + ov.remove(); + Nova.toast(`Launching ${app.name}…`, 'info', 15000); + const r = await Nova.api('docker', 'launch', { method: 'POST', body: { app_key: appKey, account_id: accountId, params } }); + Nova.toast(r?.success ? `${app.name} launched!` : (r?.error || r?.message || 'Launch failed'), r?.success ? 'success' : 'error'); + if (r?.success) dockerLoadTab('stacks'); + }; +}; + window.dockerContainerAct = async (cid, action) => { const r = await Nova.api('docker', 'container-action', { method: 'POST', body: { container_id: cid, action } }); Nova.toast(r?.success ? `Container ${action}ed` : (r?.message || 'Failed'), r?.success ? 'success' : 'error'); diff --git a/panel/public/assets/js/user.js b/panel/public/assets/js/user.js index 01f0f76..bbe8b5a 100644 --- a/panel/public/assets/js/user.js +++ b/panel/public/assets/js/user.js @@ -1076,9 +1076,10 @@ async function dockerPage(el) {
Max CPUs / App
${quota.max_cpus}
-
+
+
Loading…
`; @@ -1103,6 +1104,18 @@ async function dockerPage(el) { window._uDockerTab = 'my-apps'; +window.uDockerUninstallAll = () => Nova.confirm( + 'Remove ALL your Docker apps? This will stop and delete every container and stack you own. Your hosting account and websites are not affected.', + async () => { + Nova.loading('Removing all your Docker apps…'); + const r = await Nova.api('docker', 'uninstall-account', { method: 'POST', body: {} }); + Nova.loadingDone(); + Nova.toast(r?.success ? 'All Docker apps removed' : (r?.error || r?.message || 'Failed'), r?.success ? 'success' : 'error'); + if (r?.success) await uDockerReloadStacks(); + }, + true +); + async function uDockerReloadStacks() { const r = await Nova.api('docker', 'stacks'); window._uDockerStacks = r?.data?.stacks || [];