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 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="logs" data-app="${app.name}">Logs</button>
<button class="tab" data-tab="backups" data-app="${app.name}">Backups</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>
<div class="tab-panel active" id="tab-compose-${app.name}"> <div class="tab-panel active" id="tab-compose-${app.name}">
<textarea id="compose-${app.name}" rows="12" placeholder="Loading..."></textarea> <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 style="color:var(--muted);font-size:.85rem;">Click "Refresh" to load backups.</div>
</div> </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> </div>
`; `;
@ -562,6 +569,7 @@ function attachCardListeners() {
document.getElementById(`tab-compose-${app}`).classList.toggle("active", tabName === "compose"); document.getElementById(`tab-compose-${app}`).classList.toggle("active", tabName === "compose");
document.getElementById(`tab-logs-${app}`).classList.toggle("active", tabName === "logs"); document.getElementById(`tab-logs-${app}`).classList.toggle("active", tabName === "logs");
document.getElementById(`tab-backups-${app}`).classList.toggle("active", tabName === "backups"); 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 === "logs") loadLogs(app);
if (tabName === "backups") loadBackups(app); if (tabName === "backups") loadBackups(app);
@ -572,13 +580,22 @@ function attachCardListeners() {
document.querySelectorAll("[data-action]").forEach(btn => { document.querySelectorAll("[data-action]").forEach(btn => {
btn.onclick = (e) => { btn.onclick = (e) => {
e.stopPropagation(); e.stopPropagation();
handleAction(btn.dataset.action, btn.dataset.app); handleAction(btn.dataset.action, btn.dataset.app, btn);
}; };
}); });
} }
// ─── Actions ─── // ─── 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 { try {
switch (action) { switch (action) {
case "deploy": case "deploy":
@ -628,9 +645,34 @@ async function handleAction(action, name) {
setStatus(`Backing up ${name}...`); setStatus(`Backing up ${name}...`);
await api.backup(name); await api.backup(name);
setStatus(`Backup created for ${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; 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); await loadBackups(name);
break; break;
case "render-route": case "render-route":

View file

@ -344,7 +344,7 @@ cmd_deploy() {
cmd_render_route "${name}" cmd_render_route "${name}"
if ! run_compose -f "${APP_COMPOSE_FILE}" up -d 2>&1 | systemd-cat -t panelctl -p info 2>/dev/null; then if ! run_compose -f "${APP_COMPOSE_FILE}" up -d --remove-orphans 2>&1 | systemd-cat -t panelctl -p info 2>/dev/null; then
log err "Deployment failed for app '${name}'" log err "Deployment failed for app '${name}'"
fail "compose up failed" fail "compose up failed"
fi fi
@ -359,9 +359,9 @@ cmd_restart() {
log info "Restarting app '${name}'" log info "Restarting app '${name}'"
run_compose -f "${APP_COMPOSE_FILE}" down || fail "compose down failed" run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans || fail "compose down failed"
if ! run_compose -f "${APP_COMPOSE_FILE}" up -d 2>&1; then if ! run_compose -f "${APP_COMPOSE_FILE}" up -d --remove-orphans 2>&1; then
fail "compose up failed during restart" fail "compose up failed during restart"
fi fi
@ -373,7 +373,7 @@ cmd_stop() {
validate_name "${name}" validate_name "${name}"
load_app "${name}" load_app "${name}"
run_compose -f "${APP_COMPOSE_FILE}" down || fail "compose down failed" run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans || fail "compose down failed"
log info "stopped app '${name}'" log info "stopped app '${name}'"
} }
@ -429,7 +429,7 @@ cmd_remove() {
validate_name "${name}" validate_name "${name}"
load_app "${name}" load_app "${name}"
run_compose -f "${APP_COMPOSE_FILE}" down 2>/dev/null || true run_compose -f "${APP_COMPOSE_FILE}" down --remove-orphans 2>/dev/null || true
# Remove this app's block from the aggregate routes file. # Remove this app's block from the aggregate routes file.
local route_file local route_file