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);
}
})();