Files
nexus/modules/pi_control/assets/console.js
2026-03-07 23:20:39 +01:00

357 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
const tabBar = document.querySelector('[data-console-tab-bar]');
const tabPanels = document.querySelector('[data-console-tab-panels]');
if (!tabBar || !tabPanels) return;
let tabCount = 0;
const idleMs = 5 * 60 * 1000;
const idleTimers = new Map();
const storageKey = 'pi_control_console_tabs';
const saveTabs = () => {
const tabs = [];
tabBar.querySelectorAll('.console-tab').forEach((btn) => {
const id = btn.dataset.tabId;
const panel = tabPanels.querySelector(`.console-panel[data-tab-id="${id}"]`);
const iframe = panel ? panel.querySelector('iframe') : null;
if (iframe && iframe.src) {
tabs.push({
label: btn.firstChild ? btn.firstChild.textContent : btn.textContent,
url: iframe.src,
openedAt: Date.now(),
});
}
});
localStorage.setItem(storageKey, JSON.stringify(tabs));
};
const activateTab = (id) => {
tabBar.querySelectorAll('.console-tab').forEach((btn) => {
btn.classList.toggle('is-active', btn.dataset.tabId === id);
});
tabPanels.querySelectorAll('.console-panel').forEach((panel) => {
panel.classList.toggle('is-active', panel.dataset.tabId === id);
});
};
const getActiveIframe = () => {
const activePanel = tabPanels.querySelector('.console-panel.is-active');
if (!activePanel) return null;
return activePanel.querySelector('iframe');
};
const trySendToIframe = (iframe, command) => {
if (!iframe) return false;
try {
const win = iframe.contentWindow;
if (!win) return false;
const term = win.term || win.xterm || win.terminal;
if (term && typeof term.write === 'function') {
term.write(command + '\r\n');
return true;
}
} catch (e) {
return false;
}
return false;
};
const openTab = (label, url, persist = true) => {
const id = `tab-${++tabCount}`;
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'console-tab';
btn.textContent = label || 'Konsole';
btn.dataset.tabId = id;
btn.addEventListener('click', () => activateTab(id));
const closeBtn = document.createElement('span');
closeBtn.className = 'console-tab-close';
closeBtn.textContent = '×';
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
const panel = tabPanels.querySelector(`.console-panel[data-tab-id="${id}"]`);
if (panel) panel.remove();
btn.remove();
idleTimers.delete(id);
saveTabs();
const next = tabBar.querySelector('.console-tab');
if (next) activateTab(next.dataset.tabId);
});
btn.appendChild(closeBtn);
const panel = document.createElement('div');
panel.className = 'console-panel';
panel.dataset.tabId = id;
panel.innerHTML = `<iframe src="${url}" title="${label || 'Konsole'}"></iframe>`;
tabBar.appendChild(btn);
tabPanels.appendChild(panel);
activateTab(id);
if (persist) saveTabs();
const iframe = panel.querySelector('iframe');
const markActive = () => {
idleTimers.set(id, Date.now());
};
idleTimers.set(id, Date.now());
iframe.addEventListener('load', () => {
try {
const clearUnload = () => {
if (!iframe.contentWindow) return;
iframe.contentWindow.onbeforeunload = null;
if (iframe.contentWindow.document) {
iframe.contentWindow.document.onbeforeunload = null;
}
};
clearUnload();
setInterval(clearUnload, 2000);
const doc = iframe.contentWindow.document;
['keydown', 'mousedown', 'wheel', 'touchstart'].forEach((evt) => {
doc.addEventListener(evt, markActive, { passive: true });
});
const observer = new MutationObserver(markActive);
observer.observe(doc.body, { childList: true, subtree: true, characterData: true });
} catch (e) {
// cross-origin or blocked; rely on timer only
}
});
const idleCheck = setInterval(() => {
const last = idleTimers.get(id) || 0;
if (Date.now() - last > idleMs) {
const panelEl = tabPanels.querySelector(`.console-panel[data-tab-id="${id}"]`);
const btnEl = tabBar.querySelector(`.console-tab[data-tab-id="${id}"]`);
if (panelEl) panelEl.remove();
if (btnEl) btnEl.remove();
idleTimers.delete(id);
saveTabs();
clearInterval(idleCheck);
const next = tabBar.querySelector('.console-tab');
if (next) activateTab(next.dataset.tabId);
}
}, 10000);
};
document.querySelectorAll('.console-launch').forEach((el) => {
const url = el.dataset.url;
const host = el.dataset.host || 'Konsole';
if (url) {
openTab(host, url);
}
el.remove();
});
const showRestoreNotice = (text) => {
const notice = document.querySelector('[data-console-notice]');
if (!notice) return;
notice.textContent = text;
notice.style.display = 'block';
};
try {
const raw = localStorage.getItem(storageKey);
if (raw) {
const tabs = JSON.parse(raw);
if (Array.isArray(tabs)) {
const now = Date.now();
tabs.forEach((t) => {
if (t && t.url) {
if (t.openedAt && now - t.openedAt > (10 * 60 * 1000)) {
showRestoreNotice('Token abgelaufen, bitte Konsole neu öffnen.');
return;
}
openTab(t.label || 'Konsole', t.url, false);
}
});
}
}
} catch (e) {
// ignore
}
const queueBody = document.querySelector('[data-queue-body]');
const countdownEl = document.querySelector('[data-queue-countdown]');
const refreshBtn = document.querySelector('[data-queue-refresh]');
const queueBtn = document.querySelector('[data-queue-button]');
const queueCount = document.querySelector('[data-queue-count]');
const queueModal = document.querySelector('[data-queue-modal]');
const queueClose = document.querySelector('[data-queue-close]');
if (!queueBody || !countdownEl) return;
let remaining = 10;
const fetchQueue = async () => {
const url = new URL(window.location.href);
url.searchParams.set('queue_json', '1');
try {
const res = await fetch(url.toString(), { cache: 'no-store' });
if (!res.ok) return;
const data = await res.json();
if (data && data.html) {
queueBody.innerHTML = data.html;
}
if (queueCount && typeof data.count === 'number') {
queueCount.textContent = String(data.count);
queueCount.style.display = data.count > 0 ? 'inline-flex' : 'none';
}
remaining = data && data.next ? data.next : 10;
} catch (e) {
// ignore
}
};
const tick = () => {
remaining -= 1;
if (remaining <= 0) {
fetchQueue();
remaining = 10;
}
countdownEl.textContent = String(remaining);
};
setInterval(tick, 1000);
fetchQueue();
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
fetchQueue();
remaining = 10;
countdownEl.textContent = String(remaining);
});
}
if (queueBtn && queueModal) {
queueBtn.addEventListener('click', () => {
queueModal.classList.add('is-open');
queueModal.setAttribute('aria-hidden', 'false');
fetchQueue();
});
}
if (queueClose && queueModal) {
queueClose.addEventListener('click', () => {
queueModal.classList.remove('is-open');
queueModal.setAttribute('aria-hidden', 'true');
});
}
if (queueModal) {
queueModal.addEventListener('click', (e) => {
if (e.target === queueModal) {
queueModal.classList.remove('is-open');
queueModal.setAttribute('aria-hidden', 'true');
}
});
}
queueBody.addEventListener('click', async (e) => {
const btn = e.target.closest('[data-queue-action]');
if (!btn) return;
const runId = btn.getAttribute('data-run-id');
const action = btn.getAttribute('data-queue-action');
if (!runId || !action) return;
const url = new URL(window.location.href);
url.searchParams.set('queue_action_json', '1');
const formData = new FormData();
formData.set('run_id', runId);
formData.set('action', action);
try {
const res = await fetch(url.toString(), { method: 'POST', body: formData, cache: 'no-store' });
const data = await res.json();
if (!data.ok) {
if (consoleNotice) {
consoleNotice.textContent = data.error || 'Aktion fehlgeschlagen.';
consoleNotice.style.display = 'block';
}
}
} catch (err) {
if (consoleNotice) {
consoleNotice.textContent = 'Aktion fehlgeschlagen.';
consoleNotice.style.display = 'block';
}
}
fetchQueue();
remaining = 10;
countdownEl.textContent = String(remaining);
});
const consoleForm = document.querySelector('[data-console-form]');
const consoleError = document.querySelector('[data-console-error]');
const consoleNotice = document.querySelector('[data-console-notice]');
const tokenEl = document.querySelector('[data-console-token]');
if (consoleForm) {
consoleForm.addEventListener('submit', (e) => e.preventDefault());
const presetSelect = consoleForm.querySelector('select[name="terminal_preset_id"]');
const commandTextarea = consoleForm.querySelector('textarea[name="terminal_command_text"]');
if (presetSelect && commandTextarea) {
presetSelect.addEventListener('change', () => {
const opt = presetSelect.selectedOptions[0];
const cmd = opt && opt.dataset && opt.dataset.command ? opt.dataset.command : '';
if (cmd) {
commandTextarea.value = cmd;
}
});
}
const submitOpen = async () => {
const formData = new FormData(consoleForm);
if (presetSelect) formData.set('terminal_preset_id', '');
const url = new URL(window.location.href);
url.searchParams.set('open_console_json', '1');
const res = await fetch(url.toString(), { method: 'POST', body: formData, cache: 'no-store' });
const data = await res.json();
if (!data.ok) {
if (consoleError) {
consoleError.textContent = data.error || 'Fehler beim Öffnen der Konsole.';
consoleError.style.display = 'block';
}
return;
}
if (consoleError) consoleError.style.display = 'none';
if (consoleNotice) consoleNotice.style.display = 'none';
if (tokenEl) tokenEl.textContent = data.token || '';
if (data.url) openTab(data.host || 'Konsole', data.url);
};
const submitRun = async () => {
const formData = new FormData(consoleForm);
if (presetSelect) formData.set('terminal_preset_id', '');
const url = new URL(window.location.href);
url.searchParams.set('run_command_json', '1');
const res = await fetch(url.toString(), { method: 'POST', body: formData, cache: 'no-store' });
const data = await res.json();
if (!data.ok) {
if (consoleError) {
consoleError.textContent = data.error || 'Fehler beim Ausführen.';
consoleError.style.display = 'block';
}
return;
}
if (consoleError) consoleError.style.display = 'none';
if (consoleNotice) {
consoleNotice.textContent = data.notice || '';
consoleNotice.style.display = 'block';
}
fetchQueue();
remaining = 10;
countdownEl.textContent = String(remaining);
};
const openBtn = consoleForm.querySelector('[data-open-console]');
const runBtn = consoleForm.querySelector('[data-run-command]');
if (openBtn) {
openBtn.addEventListener('click', (e) => {
e.preventDefault();
submitOpen();
});
}
if (runBtn) {
runBtn.addEventListener('click', (e) => {
e.preventDefault();
submitRun();
});
}
// send-active removed
}
})();