feat: add routing tab and fetch routing details for apps in frontend

fix: update compose commands to remove orphan containers in panelctl

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Jakub Dorfman 2026-04-28 23:20:49 +02:00
parent 93a9a0f42b
commit 45a9aec787
2 changed files with 51 additions and 9 deletions

View file

@ -475,6 +475,7 @@ function renderAppCard(app) {
<button class="tab active" data-tab="compose" data-app="${app.name}">Compose</button>
<button class="tab" data-tab="logs" data-app="${app.name}">Logs</button>
<button class="tab" data-tab="backups" data-app="${app.name}">Backups</button>
<button class="tab" data-tab="routing" data-app="${app.name}">Routing</button>
</div>
<div class="tab-panel active" id="tab-compose-${app.name}">
<textarea id="compose-${app.name}" rows="12" placeholder="Loading..."></textarea>
@ -505,6 +506,12 @@ function renderAppCard(app) {
<div style="color:var(--muted);font-size:.85rem;">Click "Refresh" to load backups.</div>
</div>
</div>
<div class="tab-panel" id="tab-routing-${app.name}">
<div class="btn-group" style="margin-bottom:10px;">
<button class="btn-sm btn-primary" data-action="fetch-routing" data-app="${app.name}">Load App Variables</button>
</div>
<div class="log-output" id="routing-${app.name}">Click "Load App Variables" to view routing details & directories.</div>
</div>
</div>
`;
@ -562,6 +569,7 @@ function attachCardListeners() {
document.getElementById(`tab-compose-${app}`).classList.toggle("active", tabName === "compose");
document.getElementById(`tab-logs-${app}`).classList.toggle("active", tabName === "logs");
document.getElementById(`tab-backups-${app}`).classList.toggle("active", tabName === "backups");
document.getElementById(`tab-routing-${app}`).classList.toggle("active", tabName === "routing");
if (tabName === "logs") loadLogs(app);
if (tabName === "backups") loadBackups(app);
@ -572,13 +580,22 @@ function attachCardListeners() {
document.querySelectorAll("[data-action]").forEach(btn => {
btn.onclick = (e) => {
e.stopPropagation();
handleAction(btn.dataset.action, btn.dataset.app);
handleAction(btn.dataset.action, btn.dataset.app, btn);
};
});
}
// ─── Actions ───
async function handleAction(action, name) {
async function handleAction(action, name, btnElement) {
const originalWidth = btnElement ? btnElement.offsetWidth : null;
const originalHtml = btnElement ? btnElement.innerHTML : null;
if (btnElement) {
btnElement.disabled = true;
if (originalWidth) btnElement.style.width = `${originalWidth}px`;
btnElement.innerHTML = `<span class="spinner" style="vertical-align:middle; width:12px; height:12px; margin: 2px;"></span>`;
}
try {
switch (action) {
case "deploy":
@ -628,9 +645,34 @@ async function handleAction(action, name) {
setStatus(`Backing up ${name}...`);
await api.backup(name);
setStatus(`Backup created for ${name}.`);
await loadBackups(name);
case "fetch-routing":
setStatus(`Fetching routing details for ${name}...`);
const appRes = await api.getApp(name);
if (appRes.app) {
const info = appRes.app;
document.getElementById(`routing-${name}`).innerHTML = `
<b>Primary Domain:</b> ${escHtml(info.APP_DOMAIN || "-")}
<b>All Domains:</b> ${escHtml(info.APP_DOMAINS || "-")}
<b>Upstream Target:</b> ${escHtml(info.APP_UPSTREAM || "-")}
<b>Protected:</b> ${escHtml(info.APP_AUTH_PROTECTED || "-")}
<b>Port:</b> ${escHtml(info.APP_PORT || "-")}
<b>Volumes Directory:</b> ${escHtml(info.APP_VOLUME_DIR || "-")}
<b>Stack Directory:</b> ${escHtml(info.APP_STACK_DIR || "-")}
<b>Caddy Route Block:</b> ${escHtml(info.APP_ROUTE_FILE || "-")}
`.trim();
}
setStatus(`Loaded routing for ${name}.`);
break;
case "refresh-backups":
}
} catch (err) {
setStatus(`${action} failed for ${name}: ${err.message}`, true);
} finally {
if (btnElement && originalHtml) {
btnElement.disabled = false;
btnElement.innerHTML = originalHtml;
btnElement.style.width = '';
}
await loadBackups(name);
break;
case "render-route":