const form = document.getElementById('createForm'); const nodeEl = document.getElementById('node'); const storageEl = document.getElementById('storage'); const templateEl = document.getElementById('template'); const vmidEl = document.getElementById('vmid'); const ipModeEl = document.getElementById('ip_mode'); const refreshVmidBtn = document.getElementById('refreshVmid'); const submitBtn = document.getElementById('submitBtn'); const taskProgressBar = document.getElementById('taskProgressBar'); const taskStage = document.getElementById('taskStage'); const taskLogs = document.getElementById('taskLogs'); const containerDetails = document.getElementById('containerDetails'); const globalAlert = document.getElementById('globalAlert'); let taskPollTimer = null; function showAlert(message, level = 'danger') { globalAlert.innerHTML = `
${message}
`; } function clearAlert() { globalAlert.innerHTML = ''; } function setFieldError(name, message = '') { const target = document.querySelector(`[data-error-for="${name}"]`); const field = form.querySelector(`[name="${name}"]`); if (target) target.textContent = message || ''; if (field) field.classList.toggle('is-invalid', Boolean(message)); } function clearErrors() { form.querySelectorAll('.is-invalid').forEach((el) => el.classList.remove('is-invalid')); form.querySelectorAll('[data-error-for]').forEach((el) => { el.textContent = ''; }); } async function parseResponse(response, url) { const text = await response.text(); let data; try { data = JSON.parse(text); } catch (e) { throw new Error(`Server nevrátil JSON pro ${url}.`); } return data; } async function apiGet(url) { const response = await fetch(url, { credentials: 'same-origin', }); const data = await parseResponse(response, url); if (!response.ok || data.success === false) { throw new Error(data.error || `HTTP ${response.status}`); } return data; } async function apiPost(url, payload) { const response = await fetch(url, { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), }); const data = await parseResponse(response, url); if (!response.ok || data.success === false) { throw new Error(data.error || `HTTP ${response.status}`); } return data; } function option(text, value) { const opt = document.createElement('option'); opt.value = value; opt.textContent = text; return opt; } async function loadNodes() { const data = await apiGet('/api/nodes'); nodeEl.innerHTML = ''; data.nodes.forEach((node) => { nodeEl.appendChild(option(node, node)); }); if (window.APP_DEFAULTS?.defaultNode && data.nodes.includes(window.APP_DEFAULTS.defaultNode)) { nodeEl.value = window.APP_DEFAULTS.defaultNode; } } async function loadStorage() { if (!nodeEl.value) { storageEl.innerHTML = ''; return; } const data = await apiGet(`/api/storage/${encodeURIComponent(nodeEl.value)}`); storageEl.innerHTML = ''; data.storage.forEach((item) => { storageEl.appendChild(option(item, item)); }); if (window.APP_DEFAULTS?.defaultStorage && data.storage.includes(window.APP_DEFAULTS.defaultStorage)) { storageEl.value = window.APP_DEFAULTS.defaultStorage; } } async function loadTemplates() { templateEl.innerHTML = ''; if (!nodeEl.value || !storageEl.value) { return; } const data = await apiGet( `/api/templates/${encodeURIComponent(nodeEl.value)}/${encodeURIComponent(storageEl.value)}` ); data.templates.forEach((item) => { templateEl.appendChild(option(item, item)); }); } async function loadNextId() { const data = await apiGet('/api/next-vmid'); vmidEl.value = data.vmid; } function toggleStaticIpFields() { const staticOnly = document.querySelectorAll('.static-ip-only'); const isStatic = ipModeEl.value === 'static'; staticOnly.forEach((el) => el.classList.toggle('d-none', !isStatic)); } function collectFormData() { return { node: nodeEl.value, storage: storageEl.value, template: templateEl.value, vmid: vmidEl.value, hostname: document.getElementById('hostname').value, bridge: document.getElementById('bridge').value, cores: document.getElementById('cores').value, memory: document.getElementById('memory').value, disk: document.getElementById('disk_gb').value, ip_mode: document.getElementById('ip_mode').value, ip_last_octet: document.getElementById('ip_host').value, vlan: document.getElementById('vlan_tag').value, gateway: document.getElementById('gateway').value, nameserver: document.getElementById('dns').value, searchdomain: document.getElementById('searchdomain').value, password: document.getElementById('password').value, start_on_boot: document.getElementById('onboot').checked, start_now: document.getElementById('start_now').checked, unprivileged: document.getElementById('unprivileged').checked, enable_nesting: document.getElementById('enable_nesting').checked, }; } function renderContainerDetails(container) { containerDetails.innerHTML = `
Hostname
${container.hostname || '-'}
VMID
${container.vmid || '-'}
Node
${container.node || '-'}
Stav
${container.status || '-'}
CPU
${container.cores || '-'}
RAM
${container.memory || '-'} MB
RootFS
${container.rootfs || '-'}
Síť
${container.net0 || '-'}
DNS
${container.nameserver || '-'}
Search domain
${container.searchdomain || '-'}
Template
${container.ostemplate || '-'}
`; } function updateProgress(progress, text, logs = [], variant = 'primary') { taskProgressBar.style.width = `${progress}%`; taskProgressBar.textContent = `${progress}%`; taskProgressBar.className = `progress-bar ${progress < 100 ? 'progress-bar-striped progress-bar-animated' : ''} bg-${variant}`.trim(); taskStage.textContent = text; taskLogs.textContent = logs.length ? logs.join('\n') : '-'; } function stopPolling() { if (taskPollTimer) { clearTimeout(taskPollTimer); taskPollTimer = null; } } async function loadContainerDetails(node, vmid) { const data = await apiGet(`/api/container-details/${encodeURIComponent(node)}/${encodeURIComponent(vmid)}`); if (data.details) { renderContainerDetails(data.details); } } async function startContainerNow(node, vmid) { return apiPost('/api/start-lxc', { node, vmid }); } async function pollTask(node, upid) { const data = await apiGet( `/api/task-status?node=${encodeURIComponent(node)}&upid=${encodeURIComponent(upid)}` ); const failed = !data.running && data.exitstatus && data.exitstatus !== 'OK'; let stageText = 'Úloha běží...'; if (!data.running && data.exitstatus === 'OK') { stageText = 'Úloha byla úspěšně dokončena.'; } else if (!data.running && data.exitstatus && data.exitstatus !== 'OK') { stageText = `Úloha skončila chybou: ${data.exitstatus}`; } updateProgress( data.progress ?? 0, stageText, data.logs || [], failed ? 'danger' : (data.progress === 100 && data.exitstatus === 'OK' ? 'success' : 'primary') ); return data; } async function waitForTask(node, upid) { while (true) { const data = await pollTask(node, upid); if (!data.running) { return data; } await new Promise((resolve) => { taskPollTimer = setTimeout(resolve, 2000); }); } } nodeEl.addEventListener('change', async () => { clearAlert(); try { await loadStorage(); await loadTemplates(); } catch (error) { showAlert(error.message); } }); storageEl.addEventListener('change', async () => { clearAlert(); try { await loadTemplates(); } catch (error) { showAlert(error.message); } }); ipModeEl.addEventListener('change', toggleStaticIpFields); refreshVmidBtn.addEventListener('click', async () => { try { await loadNextId(); } catch (error) { showAlert(error.message); } }); form.addEventListener('reset', () => { stopPolling(); clearErrors(); clearAlert(); setTimeout(() => { toggleStaticIpFields(); updateProgress(0, 'Ještě nebyla spuštěna žádná úloha.', []); containerDetails.textContent = 'Po úspěšném vytvoření se tady zobrazí detaily.'; }, 0); }); form.addEventListener('submit', async (event) => { event.preventDefault(); stopPolling(); clearErrors(); clearAlert(); submitBtn.disabled = true; containerDetails.textContent = 'Čekám na dokončení vytváření kontejneru.'; updateProgress(8, 'Odesílám požadavek do Proxmoxu...', []); try { const payload = collectFormData(); const startNow = payload.start_now; const createData = await apiPost('/api/create-lxc', payload); showAlert(`Úloha byla spuštěna. VMID ${createData.vmid}, hostname ${createData.hostname}.`, 'info'); updateProgress(12, 'Vytváření kontejneru bylo přijato Proxmoxem.', []); const createTaskResult = await waitForTask(createData.node, createData.upid); if (createTaskResult.exitstatus !== 'OK') { submitBtn.disabled = false; showAlert(`Vytvoření kontejneru selhalo: ${createTaskResult.exitstatus || 'neznámá chyba'}`, 'danger'); return; } if (startNow) { updateProgress(90, 'Spouštím nově vytvořený kontejner...', createTaskResult.logs || []); const startData = await startContainerNow(createData.node, createData.vmid); const startTaskResult = await waitForTask(startData.node, startData.upid); if (startTaskResult.exitstatus !== 'OK') { submitBtn.disabled = false; showAlert(`Kontejner byl vytvořen, ale nepodařilo se ho spustit: ${startTaskResult.exitstatus || 'neznámá chyba'}`, 'warning'); await loadContainerDetails(createData.node, createData.vmid); return; } } updateProgress(100, 'Kontejner byl úspěšně vytvořen.', [], 'success'); showAlert(`Kontejner VMID ${createData.vmid} byl úspěšně vytvořen.`, 'success'); await loadContainerDetails(createData.node, createData.vmid); submitBtn.disabled = false; } catch (error) { submitBtn.disabled = false; showAlert(error.message); updateProgress(100, `Požadavek selhal: ${error.message}`, [], 'danger'); } }); (async function init() { toggleStaticIpFields(); try { await loadNodes(); await loadStorage(); await loadTemplates(); await loadNextId(); } catch (error) { showAlert(error.message); } })();