Files
nexus/modules/pi_control/assets/hosts.js
2026-03-08 22:40:54 +01:00

265 lines
9.0 KiB
JavaScript

(() => {
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]');
if (upd) {
upd.classList.remove('badge-warn', 'badge-ok', 'badge-error');
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) {
upd.setAttribute('title', data.updates.preview);
}
} else {
upd.textContent = 'Updates: Fehler';
upd.classList.add('badge-error');
if (data.updates && data.updates.error) {
upd.setAttribute('title', data.updates.error);
}
}
}
if (upg) {
upg.classList.remove('badge-warn', 'badge-ok', 'badge-error');
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: Fehler';
upg.classList.add('badge-error');
if (data.os && data.os.error) {
upg.setAttribute('title', data.os.error);
}
}
}
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 = '';
}
if (upgradeError) {
payload.os.error = upgradeError;
} else if (upgradeAvailable !== undefined && upgradeAvailable !== '') {
payload.os.available = upgradeAvailable === '1' || upgradeAvailable === 'true';
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);
}
});
})();