New desktop
Some checks failed
Deploy / deploy-staging (push) Failing after 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-06-06 03:20:50 +02:00
parent 756b4e119b
commit 888981c782
127 changed files with 31275 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
(() => {
const form = document.querySelector('[data-command-form]');
const list = document.querySelector('[data-command-list]');
if (!form) return;
const idInput = form.querySelector('input[name="id"]');
const labelInput = form.querySelector('input[name="label"]');
const commandInput = form.querySelector('textarea[name="command"]');
const timeoutInput = form.querySelector('input[name="timeout_sec"]');
const adminInput = form.querySelector('input[name="admin_only"]');
const submitBtn = form.querySelector('[data-command-submit]');
const cancelBtn = form.querySelector('[data-command-cancel]');
const modal = document.querySelector('[data-command-modal]');
const modalTitle = document.querySelector('[data-command-modal-title]');
const closeBtn = document.querySelector('[data-command-close]');
const newBtn = document.querySelector('[data-command-new]');
const unsavedBar = document.querySelector('[data-command-unsaved]');
const discardBtn = document.querySelector('[data-command-discard]');
let initialSnapshot = '';
const resetForm = () => {
if (idInput) idInput.value = '';
if (labelInput) labelInput.value = '';
if (commandInput) commandInput.value = '';
if (timeoutInput) timeoutInput.value = '';
if (adminInput) adminInput.checked = false;
if (submitBtn) submitBtn.textContent = 'Speichern';
if (unsavedBar) unsavedBar.style.display = 'none';
};
const snapshot = () => {
return JSON.stringify({
id: idInput ? idInput.value : '',
label: labelInput ? labelInput.value : '',
command: commandInput ? commandInput.value : '',
timeout: timeoutInput ? timeoutInput.value : '',
admin: adminInput ? adminInput.checked : false,
});
};
const isDirty = () => snapshot() !== initialSnapshot;
const openModal = () => {
if (!modal) return;
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
initialSnapshot = snapshot();
};
const closeModal = (force = false) => {
if (!modal) return;
if (!force && isDirty()) {
if (unsavedBar) unsavedBar.style.display = 'flex';
return;
}
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
if (unsavedBar) unsavedBar.style.display = 'none';
};
document.querySelectorAll('[data-command-edit]').forEach((btn) => {
btn.addEventListener('click', () => {
const item = btn.closest('.command-item');
if (!item) return;
if (idInput) idInput.value = item.dataset.commandId || '';
if (labelInput) labelInput.value = item.dataset.label || '';
if (commandInput) commandInput.value = item.dataset.command || '';
if (timeoutInput) timeoutInput.value = item.dataset.timeout || '';
if (adminInput) adminInput.checked = item.dataset.admin === '1';
if (submitBtn) submitBtn.textContent = 'Aktualisieren';
if (modalTitle) modalTitle.textContent = 'Befehl bearbeiten';
const details = btn.closest('details');
if (details) details.removeAttribute('open');
openModal();
});
});
if (cancelBtn) {
cancelBtn.addEventListener('click', (e) => {
e.preventDefault();
resetForm();
});
}
if (newBtn) {
newBtn.addEventListener('click', () => {
resetForm();
if (modalTitle) modalTitle.textContent = 'Neuer Befehl';
openModal();
});
}
if (closeBtn) {
closeBtn.addEventListener('click', () => closeModal(false));
}
if (discardBtn) {
discardBtn.addEventListener('click', () => {
resetForm();
closeModal(true);
});
}
form.addEventListener('input', () => {
if (unsavedBar && isDirty()) {
unsavedBar.style.display = 'flex';
}
});
if (!list) return;
let dragging = null;
list.querySelectorAll('.command-item').forEach((item) => {
item.addEventListener('dragstart', () => {
dragging = item;
item.classList.add('is-dragging');
});
item.addEventListener('dragend', () => {
item.classList.remove('is-dragging');
dragging = null;
saveOrder();
});
});
list.addEventListener('dragover', (e) => {
e.preventDefault();
if (!dragging) return;
const after = getDragAfterElement(list, e.clientY);
if (after == null) {
list.appendChild(dragging);
} else if (after !== dragging) {
list.insertBefore(dragging, after);
}
});
const getDragAfterElement = (container, y) => {
const elements = [...container.querySelectorAll('.command-item:not(.is-dragging)')];
return elements.reduce(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset, element: child };
}
return closest;
},
{ offset: Number.NEGATIVE_INFINITY, element: null }
).element;
};
const saveOrder = () => {
const order = [...list.querySelectorAll('.command-item')].map((el) => el.dataset.commandId);
fetch(window.location.pathname + '?reorder_json=1', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ order }),
cache: 'no-store',
}).catch(() => {});
};
})();

View File

@@ -0,0 +1,417 @@
(() => {
// Disable any beforeunload prompts on this page
window.onbeforeunload = null;
window.addEventListener(
'beforeunload',
(e) => {
e.stopImmediatePropagation();
},
{ capture: true }
);
const tabBar = document.querySelector('[data-console-tab-bar]');
const tabPanels = document.querySelector('[data-console-tab-panels]');
if (!tabBar || !tabPanels) return;
const consoleFab = document.querySelector('[data-console-fab]');
const consoleModal = document.querySelector('[data-console-modal]');
const consoleClose = document.querySelector('[data-console-close]');
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));
if (consoleFab) {
consoleFab.classList.toggle('is-visible', tabs.length > 0);
}
};
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 openModal = () => {
if (!consoleModal) return;
consoleModal.classList.add('is-open');
consoleModal.setAttribute('aria-hidden', 'false');
};
const closeModal = () => {
if (!consoleModal) return;
consoleModal.classList.remove('is-open');
consoleModal.setAttribute('aria-hidden', 'true');
};
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();
openModal();
const iframe = panel.querySelector('iframe');
const markActive = () => {
idleTimers.set(id, Date.now());
};
idleTimers.set(id, Date.now());
iframe.addEventListener('load', () => {
try {
const disableBeforeUnload = (win) => {
if (!win) return;
try {
win.onbeforeunload = null;
win.addEventListener(
'beforeunload',
(e) => {
e.stopImmediatePropagation();
},
{ capture: true }
);
if (win.document) {
win.document.onbeforeunload = null;
win.document.addEventListener(
'beforeunload',
(e) => {
e.stopImmediatePropagation();
},
{ capture: true }
);
}
} catch (err) {
// ignore
}
};
disableBeforeUnload(iframe.contentWindow);
setInterval(() => disableBeforeUnload(iframe.contentWindow), 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);
}
});
if (consoleFab && tabs.length > 0) {
consoleFab.classList.add('is-visible');
}
}
}
} catch (e) {
// ignore
}
if (consoleFab) {
consoleFab.addEventListener('click', openModal);
}
if (consoleClose) {
consoleClose.addEventListener('click', closeModal);
}
if (consoleModal) {
consoleModal.addEventListener('click', (e) => {
if (e.target === consoleModal) closeModal();
});
}
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
}
})();

View File

@@ -0,0 +1,286 @@
(() => {
const form = document.querySelector('[data-host-form]');
if (!form) return;
const idInput = form.querySelector('input[name="id"]');
const nameInput = form.querySelector('input[name="name"]');
const hostInput = form.querySelector('input[name="host"]');
const portInput = form.querySelector('input[name="port"]');
const userInput = form.querySelector('input[name="username"]');
const authSelect = form.querySelector('select[name="auth_type"]');
const keyInput = form.querySelector('input[name="key_path"]');
const passInput = form.querySelector('input[name="password"]');
const imageInput = form.querySelector('input[name="image_url"]');
const submitBtn = form.querySelector('[data-host-submit]');
const modal = document.querySelector('[data-host-modal]');
const modalTitle = document.querySelector('[data-host-modal-title]');
const closeBtn = document.querySelector('[data-host-close]');
const newBtn = document.querySelector('[data-host-new]');
const checkAllBtn = document.querySelector('[data-host-check-all]');
const cancelBtn = form.querySelector('[data-host-cancel]');
let initialSnapshot = '';
const resetForm = () => {
if (idInput) idInput.value = '';
if (nameInput) nameInput.value = '';
if (hostInput) hostInput.value = '';
if (portInput) portInput.value = '22';
if (userInput) userInput.value = '';
if (authSelect) authSelect.value = 'key';
if (keyInput) keyInput.value = '';
if (passInput) passInput.value = '';
if (imageInput) imageInput.value = '';
if (submitBtn) submitBtn.textContent = 'Speichern';
if (modal && modal.classList.contains('is-open')) {
initialSnapshot = snapshot();
}
};
const snapshot = () => {
return JSON.stringify({
id: idInput ? idInput.value : '',
name: nameInput ? nameInput.value : '',
host: hostInput ? hostInput.value : '',
port: portInput ? portInput.value : '',
user: userInput ? userInput.value : '',
auth: authSelect ? authSelect.value : '',
key: keyInput ? keyInput.value : '',
pass: passInput ? passInput.value : '',
image: imageInput ? imageInput.value : '',
});
};
const isDirty = () => snapshot() !== initialSnapshot;
const openModal = () => {
if (!modal) return;
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
initialSnapshot = snapshot();
};
const closeModal = (force = false) => {
if (!modal) return;
if (!force && isDirty()) {
const ok = window.confirm('Änderungen nicht gespeichert. Ohne Speichern schließen?');
if (!ok) return;
}
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
};
document.querySelectorAll('[data-host-edit]').forEach((btn) => {
btn.addEventListener('click', () => {
const card = btn.closest('.host-card');
if (!card) return;
if (idInput) idInput.value = card.dataset.hostId || '';
if (nameInput) nameInput.value = card.dataset.name || '';
if (hostInput) hostInput.value = card.dataset.host || '';
if (portInput) portInput.value = card.dataset.port || '22';
if (userInput) userInput.value = card.dataset.username || '';
if (authSelect) authSelect.value = card.dataset.auth || 'key';
if (keyInput) keyInput.value = card.dataset.keyPath || '';
if (passInput) passInput.value = '';
if (imageInput) imageInput.value = card.dataset.imageUrl || '';
if (submitBtn) submitBtn.textContent = 'Aktualisieren';
if (modalTitle) modalTitle.textContent = 'Host bearbeiten';
const details = btn.closest('details');
if (details) details.removeAttribute('open');
openModal();
});
});
if (newBtn) {
newBtn.addEventListener('click', () => {
resetForm();
if (modalTitle) modalTitle.textContent = 'Neuer Host';
openModal();
});
}
if (closeBtn) {
closeBtn.addEventListener('click', () => closeModal(false));
}
if (cancelBtn) {
cancelBtn.addEventListener('click', (e) => {
e.preventDefault();
resetForm();
});
}
const updateStatus = (card, status) => {
const dot = card.querySelector('[data-host-status]');
if (!dot) return;
dot.classList.remove('status-ok', 'status-auth', 'status-down');
if (status === 'ok') dot.classList.add('status-ok');
else if (status === 'down') dot.classList.add('status-down');
else dot.classList.add('status-auth');
};
const fetchStatus = (card) => {
const id = card.dataset.hostId;
if (!id) return;
fetch(`${window.location.pathname}?status_json=1&id=${encodeURIComponent(id)}`, { cache: 'no-store' })
.then((res) => res.json())
.then((data) => {
if (data && data.ok) {
updateStatus(card, data.status);
}
})
.catch(() => {});
};
document.querySelectorAll('.host-card').forEach((card) => {
fetchStatus(card);
});
const setUpdateUi = (card, data) => {
const upd = card.querySelector('[data-update-badge]');
const upg = card.querySelector('[data-upgrade-badge]');
const time = card.querySelector('[data-update-time]');
const updDebug = card.querySelector('[data-update-debug]');
const upgDebug = card.querySelector('[data-upgrade-debug]');
if (upd) {
upd.classList.remove('badge-warn', 'badge-ok', 'badge-error');
if (data.updates && data.updates.error) {
upd.textContent = 'Updates: Fehler';
upd.classList.add('badge-error');
upd.setAttribute('title', data.updates.error);
} else if (data.updates && typeof data.updates.count === 'number') {
upd.textContent = `Updates: ${data.updates.count}`;
upd.classList.toggle('badge-warn', data.updates.count > 0);
upd.classList.toggle('badge-ok', data.updates.count === 0);
if (data.updates.preview || data.updates.raw) {
upd.setAttribute('title', data.updates.preview || data.updates.raw);
}
} else {
upd.textContent = 'Updates: ';
if (data.updates && (data.updates.preview || data.updates.raw)) {
upd.setAttribute('title', data.updates.preview || data.updates.raw);
}
}
}
if (upg) {
upg.classList.remove('badge-warn', 'badge-ok', 'badge-error');
if (data.os && data.os.error) {
upg.textContent = 'OS: Fehler';
upg.classList.add('badge-error');
upg.setAttribute('title', data.os.error);
} else if (data.os && typeof data.os.available === 'boolean') {
upg.textContent = data.os.available ? 'OS: Upgrade verfügbar' : 'OS: OK';
upg.classList.toggle('badge-warn', data.os.available);
upg.classList.toggle('badge-ok', !data.os.available);
if (data.os.raw) upg.setAttribute('title', data.os.raw);
} else {
upg.textContent = 'OS: ';
if (data.os && data.os.raw) {
upg.setAttribute('title', data.os.raw);
}
}
}
if (updDebug) {
const raw = (data.updates && (data.updates.raw || data.updates.preview)) || '';
updDebug.textContent = raw ? `Update Debug: ${raw}` : 'Update Debug: ';
}
if (upgDebug) {
const raw = (data.os && data.os.raw) || '';
upgDebug.textContent = raw ? `Upgrade Debug: ${raw}` : 'Upgrade Debug: ';
}
if (time && data.checked_at) {
const dt = new Date(data.checked_at);
time.textContent = isNaN(dt.getTime()) ? data.checked_at : dt.toLocaleString();
}
};
const checkHostUpdates = (card) => {
const id = card.dataset.hostId;
if (!id) return Promise.resolve();
const btn = card.querySelector('[data-host-check]');
if (btn) {
btn.disabled = true;
btn.textContent = 'Prüfe...';
}
return fetch(`${window.location.pathname}?update_json=1&id=${encodeURIComponent(id)}`, { cache: 'no-store' })
.then((res) => res.json())
.then((data) => {
if (data && data.ok) {
setUpdateUi(card, data);
}
})
.finally(() => {
if (btn) {
btn.disabled = false;
btn.textContent = 'Updates prüfen';
}
});
};
document.querySelectorAll('[data-host-check]').forEach((btn) => {
btn.addEventListener('click', () => {
const card = btn.closest('.host-card');
if (card) checkHostUpdates(card);
});
});
if (checkAllBtn) {
checkAllBtn.addEventListener('click', async () => {
const cards = Array.from(document.querySelectorAll('.host-card'));
for (const card of cards) {
await checkHostUpdates(card);
}
});
}
const applyStoredUpdate = (card) => {
const checkedAt = card.dataset.updateChecked || '';
const updateCount = card.dataset.updateCount;
const updateError = card.dataset.updateError || '';
const upgradeAvailable = card.dataset.upgradeAvailable;
const upgradeRaw = card.dataset.upgradeRaw || '';
const upgradeError = card.dataset.upgradeError || '';
const payload = {
updates: {},
os: {},
checked_at: checkedAt || '',
};
if (updateError) {
payload.updates.error = updateError;
} else if (updateCount !== undefined && updateCount !== '') {
payload.updates.count = Number(updateCount);
payload.updates.preview = card.dataset.updatePreview || '';
payload.updates.raw = card.dataset.updatePreview || '';
} else {
payload.updates.preview = card.dataset.updatePreview || '';
payload.updates.raw = card.dataset.updatePreview || '';
}
if (upgradeError) {
payload.os.error = upgradeError;
} else if (upgradeAvailable !== undefined && upgradeAvailable !== '') {
payload.os.available = upgradeAvailable === '1' || upgradeAvailable === 'true';
payload.os.raw = upgradeRaw;
} else {
payload.os.raw = upgradeRaw;
}
setUpdateUi(card, payload);
};
const isStale = (checkedAt) => {
if (!checkedAt) return true;
const dt = new Date(checkedAt);
if (isNaN(dt.getTime())) return true;
const ageMs = Date.now() - dt.getTime();
return ageMs > 24 * 60 * 60 * 1000;
};
document.querySelectorAll('.host-card').forEach((card) => {
applyStoredUpdate(card);
if (isStale(card.dataset.updateChecked || '')) {
checkHostUpdates(card);
}
});
})();

View File

@@ -0,0 +1,279 @@
.form-card { padding: 14px; }
.notice-card {
padding: 12px 14px;
line-height: 1.45;
word-break: break-word;
}
.form-grid { display: grid; gap: 12px; }
.form-field { display: grid; gap: 6px; }
.form-field input,
.form-field select,
.form-field textarea { width: 100%; }
.icon-button {
border: 1px solid var(--line);
background: var(--panel-2);
border-radius: 999px;
padding: 4px 10px;
cursor: pointer;
font-weight: 700;
}
.icon-button:hover { background: var(--panel); }
.console-tabs {
border: 1px solid var(--line);
border-radius: 14px;
overflow: hidden;
background: #0b0f17;
}
.console-tab-bar {
display: flex;
gap: 6px;
flex-wrap: wrap;
padding: 8px;
background: #0f1624;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.console-tab {
background: transparent;
color: #c9d3e3;
border: 1px solid transparent;
border-radius: 10px;
padding: 6px 10px;
cursor: pointer;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
}
.console-tab.is-active {
border-color: rgba(255,255,255,0.2);
background: rgba(255,255,255,0.08);
color: #ffffff;
}
.console-tab-panels {
min-height: 420px;
}
.console-tab-close {
display: inline-flex;
width: 18px;
height: 18px;
align-items: center;
justify-content: center;
border-radius: 999px;
background: rgba(255,255,255,0.12);
color: #ffffff;
font-size: 0.8rem;
line-height: 1;
}
.console-tab-close:hover {
background: rgba(255,255,255,0.25);
}
.console-panel { display: none; }
.console-panel.is-active { display: block; }
.console-panel iframe {
width: 100%;
height: 520px;
border: 0;
background: #0b0f17;
}
.host-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
.host-card {
background: var(--panel);
border: 1px solid var(--line);
border-radius: 16px;
overflow: visible;
display: grid;
grid-template-rows: 120px 1fr;
box-shadow: var(--shadow);
position: relative;
z-index: 1;
}
.host-card-image {
background: linear-gradient(135deg, #2b3a67 0%, #3b2f5c 45%, #1c2b3f 100%);
background-size: cover;
background-position: center;
position: relative;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
}
.host-card-overlay {
position: absolute;
inset: 0;
background: rgba(10, 16, 28, 0.35);
}
.host-card-body {
padding: 12px 14px 14px;
display: grid;
gap: 6px;
}
.host-card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.host-card-title {
display: inline-flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 999px;
display: inline-block;
}
.status-ok { background: #31c48d; }
.status-auth { background: #fbbf24; }
.status-down { background: #ef4444; }
.host-update-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.update-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--line);
background: var(--panel-2);
font-size: 0.75rem;
font-weight: 600;
}
.badge-ok {
border-color: rgba(49,196,141,0.5);
background: rgba(49,196,141,0.15);
}
.badge-warn {
border-color: rgba(251,191,36,0.6);
background: rgba(251,191,36,0.2);
}
.badge-error {
border-color: rgba(239,68,68,0.6);
background: rgba(239,68,68,0.15);
}
.action-menu {
position: relative;
}
.action-menu summary {
list-style: none;
cursor: pointer;
border-radius: 10px;
padding: 2px 6px;
border: 1px solid var(--line);
background: var(--panel-2);
}
.action-menu summary::-webkit-details-marker { display: none; }
.action-menu[open] summary {
background: var(--panel);
}
.action-menu-panel {
position: absolute;
right: 0;
top: calc(100% + 6px);
background: var(--panel);
border: 1px solid var(--line);
border-radius: 12px;
padding: 6px;
min-width: 160px;
display: grid;
gap: 4px;
z-index: 200;
box-shadow: var(--shadow);
}
.action-menu-panel form { margin: 0; }
.host-unsaved {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border: 1px solid #f3c3b8;
background: #fff5f3;
color: #7a2114;
border-radius: 12px;
}
.host-debug {
margin: 6px 0 0;
padding: 6px 8px;
background: #0b1020;
color: #c7d2fe;
border-radius: 8px;
font-size: 0.7rem;
white-space: pre-wrap;
word-break: break-word;
max-height: 120px;
overflow: auto;
}
.command-list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 10px;
}
.command-item {
display: grid;
grid-template-columns: 28px 1fr auto;
gap: 10px;
align-items: start;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--panel);
}
.command-item.is-dragging {
opacity: 0.6;
}
.command-drag {
cursor: grab;
color: var(--muted);
font-size: 1.1rem;
padding-top: 4px;
}
.command-body code {
display: inline-block;
word-break: break-word;
}
.queue-button {
display: inline-flex;
align-items: center;
gap: 8px;
}
.queue-badge {
display: inline-flex;
min-width: 22px;
height: 22px;
align-items: center;
justify-content: center;
border-radius: 999px;
background: #ff5a3c;
color: #fff;
font-weight: 700;
font-size: 0.75rem;
padding: 0 6px;
}
.modal-card {
width: min(1100px, 96vw);
}
.modal-actions {
display: inline-flex;
align-items: center;
gap: 8px;
}