fix: make backend/.env optional in compose.yml to fix first-run error on clean servers

This commit is contained in:
markmorado
2026-03-08 12:22:38 +01:00
parent 85aa59e7b6
commit 65a71a5742
3 changed files with 21 additions and 19 deletions

View File

@@ -480,7 +480,8 @@ function onServerChange() {
// ===== Nodes ===== // ===== Nodes =====
async function fetchNodes(serverId) { async function fetchNodes(serverId) {
try { try {
const data = await fetchWithAuth(`/proxmox/api/servers/${serverId}/cluster-info`); const resp = await fetchWithAuth(`/proxmox/api/servers/${serverId}/cluster-info`);
const data = await resp.json();
return (data.nodes || []).filter(Boolean); return (data.nodes || []).filter(Boolean);
} catch { return []; } } catch { return []; }
} }
@@ -495,7 +496,7 @@ function fillNodeSelect(selectId, nodes) {
async function populateStorageDropdown(targetId = 'storageSelectBackups') { async function populateStorageDropdown(targetId = 'storageSelectBackups') {
if (!currentServerId) return; if (!currentServerId) return;
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/storages/${currentServerId}`); const r = await (await fetchWithAuth(`/proxmox/api/backups/storages/${currentServerId}`)).json();
const backupStorages = (r.storages || []).filter(s => (s.content || '').includes('backup')); const backupStorages = (r.storages || []).filter(s => (s.content || '').includes('backup'));
const sel = document.getElementById(targetId); const sel = document.getElementById(targetId);
sel.innerHTML = '<option value="">{{ t("select_storage") }}</option>'; sel.innerHTML = '<option value="">{{ t("select_storage") }}</option>';
@@ -521,7 +522,7 @@ async function loadBackups() {
let url = `/proxmox/api/backups/list/${currentServerId}?node=${node}&storage=${storage}`; let url = `/proxmox/api/backups/list/${currentServerId}?node=${node}&storage=${storage}`;
if (vmid) url += `&vmid=${vmid}`; if (vmid) url += `&vmid=${vmid}`;
try { try {
const r = await fetchWithAuth(url); const r = await (await fetchWithAuth(url)).json();
const backups = r.backups || []; const backups = r.backups || [];
const tbody = document.getElementById('backupsTbody'); const tbody = document.getElementById('backupsTbody');
if (!backups.length) { tbody.innerHTML = '<tr><td colspan="7" class="text-center py-3 text-muted">{{ t("backup_empty") }}</td></tr>'; return; } if (!backups.length) { tbody.innerHTML = '<tr><td colspan="7" class="text-center py-3 text-muted">{{ t("backup_empty") }}</td></tr>'; return; }
@@ -562,10 +563,10 @@ function formatSize(bytes) {
async function deleteBackup(volid, node, storage) { async function deleteBackup(volid, node, storage) {
if (!confirm('{{ t("backup_delete_confirm") }}')) return; if (!confirm('{{ t("backup_delete_confirm") }}')) return;
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/${currentServerId}/backup`, { const r = await (await fetchWithAuth(`/proxmox/api/backups/${currentServerId}/backup`, {
method: 'DELETE', method: 'DELETE',
body: JSON.stringify({ node, storage, volid }), body: JSON.stringify({ node, storage, volid }),
}); })).json();
if (r.success) { showToast('{{ t("success") }}', '{{ t("backup_deleted") }}', 'success'); loadBackups(); } if (r.success) { showToast('{{ t("success") }}', '{{ t("backup_deleted") }}', 'success'); loadBackups(); }
} catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); } } catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); }
} }
@@ -591,7 +592,7 @@ async function submitCreateBackup() {
notes: document.getElementById('cbNotes').value || null, notes: document.getElementById('cbNotes').value || null,
}; };
try { try {
const r = await fetchWithAuth('/proxmox/api/backups/create', { method: 'POST', body: JSON.stringify(payload) }); const r = await (await fetchWithAuth('/proxmox/api/backups/create', { method: 'POST', body: JSON.stringify(payload) })).json();
if (r.success) { if (r.success) {
bootstrap.Modal.getInstance(document.getElementById('createBackupModal'))?.hide(); bootstrap.Modal.getInstance(document.getElementById('createBackupModal'))?.hide();
showToast('{{ t("success") }}', '{{ t("backup_started") }}', 'success'); showToast('{{ t("success") }}', '{{ t("backup_started") }}', 'success');
@@ -628,7 +629,7 @@ async function submitRestore() {
unique: document.getElementById('restoreUnique').checked, unique: document.getElementById('restoreUnique').checked,
}; };
try { try {
const r = await fetchWithAuth('/proxmox/api/backups/restore', { method: 'POST', body: JSON.stringify(payload) }); const r = await (await fetchWithAuth('/proxmox/api/backups/restore', { method: 'POST', body: JSON.stringify(payload) })).json();
if (r.success) { if (r.success) {
bootstrap.Modal.getInstance(document.getElementById('restoreModal'))?.hide(); bootstrap.Modal.getInstance(document.getElementById('restoreModal'))?.hide();
showToast('{{ t("success") }}', '{{ t("backup_restore_started") }}', 'success'); showToast('{{ t("success") }}', '{{ t("backup_restore_started") }}', 'success');
@@ -646,7 +647,7 @@ async function openTaskLog(serverId, node, upid) {
let done = false; let done = false;
while (!done) { while (!done) {
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/task/${serverId}/${node}/${encodeURIComponent(upid)}`); const r = await (await fetchWithAuth(`/proxmox/api/backups/task/${serverId}/${node}/${encodeURIComponent(upid)}`)).json();
const status = r.status || {}; const status = r.status || {};
const lines = (r.log || []).map(l => l.t || '').join('\n'); const lines = (r.log || []).map(l => l.t || '').join('\n');
document.getElementById('taskLogContent').textContent = lines; document.getElementById('taskLogContent').textContent = lines;
@@ -670,7 +671,7 @@ async function openTaskLog(serverId, node, upid) {
async function loadStorages() { async function loadStorages() {
if (!currentServerId) { document.getElementById('storagesTbody').innerHTML = '<tr><td colspan="7" class="text-center py-3 text-muted">{{ t("select_server") }}</td></tr>'; return; } if (!currentServerId) { document.getElementById('storagesTbody').innerHTML = '<tr><td colspan="7" class="text-center py-3 text-muted">{{ t("select_server") }}</td></tr>'; return; }
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/storages/${currentServerId}`); const r = await (await fetchWithAuth(`/proxmox/api/backups/storages/${currentServerId}`)).json();
const storages = r.storages || []; const storages = r.storages || [];
const tbody = document.getElementById('storagesTbody'); const tbody = document.getElementById('storagesTbody');
if (!storages.length) { tbody.innerHTML = '<tr><td colspan="7" class="text-center py-3 text-muted">{{ t("backup_no_storages") }}</td></tr>'; return; } if (!storages.length) { tbody.innerHTML = '<tr><td colspan="7" class="text-center py-3 text-muted">{{ t("backup_no_storages") }}</td></tr>'; return; }
@@ -697,7 +698,7 @@ async function loadStorages() {
async function deleteStorage(storageId) { async function deleteStorage(storageId) {
if (!confirm('{{ t("backup_storage_delete_confirm") }}')) return; if (!confirm('{{ t("backup_storage_delete_confirm") }}')) return;
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/storages/${currentServerId}/${storageId}`, { method: 'DELETE' }); const r = await (await fetchWithAuth(`/proxmox/api/backups/storages/${currentServerId}/${storageId}`, { method: 'DELETE' })).json();
if (r.success) { showToast('{{ t("success") }}', '{{ t("backup_storage_deleted") }}', 'success'); loadStorages(); } if (r.success) { showToast('{{ t("success") }}', '{{ t("backup_storage_deleted") }}', 'success'); loadStorages(); }
} catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); } } catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); }
} }
@@ -731,7 +732,7 @@ async function submitAddStorage() {
if (type === 'cifs') { payload.server = document.getElementById('asCifsServer').value; payload.share = document.getElementById('asCifsShare').value; payload.username = document.getElementById('asCifsUser').value; payload.password = document.getElementById('asCifsPass').value; } if (type === 'cifs') { payload.server = document.getElementById('asCifsServer').value; payload.share = document.getElementById('asCifsShare').value; payload.username = document.getElementById('asCifsUser').value; payload.password = document.getElementById('asCifsPass').value; }
if (type === 'pbs') { payload.server = document.getElementById('asPbsServer').value; payload.datastore = document.getElementById('asPbsDatastore').value; payload.username = document.getElementById('asPbsUser').value; payload.password = document.getElementById('asPbsPass').value; if (document.getElementById('asPbsFingerprint').value) payload.fingerprint = document.getElementById('asPbsFingerprint').value; } if (type === 'pbs') { payload.server = document.getElementById('asPbsServer').value; payload.datastore = document.getElementById('asPbsDatastore').value; payload.username = document.getElementById('asPbsUser').value; payload.password = document.getElementById('asPbsPass').value; if (document.getElementById('asPbsFingerprint').value) payload.fingerprint = document.getElementById('asPbsFingerprint').value; }
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/storages/${currentServerId}`, { method: 'POST', body: JSON.stringify(payload) }); const r = await (await fetchWithAuth(`/proxmox/api/backups/storages/${currentServerId}`, { method: 'POST', body: JSON.stringify(payload) })).json();
if (r.success) { bootstrap.Modal.getInstance(document.getElementById('addStorageModal'))?.hide(); showToast('{{ t("success") }}', '{{ t("backup_storage_added") }}', 'success'); loadStorages(); } if (r.success) { bootstrap.Modal.getInstance(document.getElementById('addStorageModal'))?.hide(); showToast('{{ t("success") }}', '{{ t("backup_storage_added") }}', 'success'); loadStorages(); }
} catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); } } catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); }
} }
@@ -739,7 +740,7 @@ async function submitAddStorage() {
// ===== JOBS Tab ===== // ===== JOBS Tab =====
async function loadJobs() { async function loadJobs() {
try { try {
const r = await fetchWithAuth('/proxmox/api/backups/jobs'); const r = await (await fetchWithAuth('/proxmox/api/backups/jobs')).json();
const jobs = r.jobs || []; const jobs = r.jobs || [];
const tbody = document.getElementById('jobsTbody'); const tbody = document.getElementById('jobsTbody');
if (!jobs.length) { tbody.innerHTML = '<tr><td colspan="8" class="text-center py-3 text-muted">{{ t("backup_no_jobs") }}</td></tr>'; return; } if (!jobs.length) { tbody.innerHTML = '<tr><td colspan="8" class="text-center py-3 text-muted">{{ t("backup_no_jobs") }}</td></tr>'; return; }
@@ -784,7 +785,7 @@ async function onJobServerChange(serverId) {
async function populateJobStorages(serverId) { async function populateJobStorages(serverId) {
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/storages/${serverId}`); const r = await (await fetchWithAuth(`/proxmox/api/backups/storages/${serverId}`)).json();
const backupStorages = (r.storages || []).filter(s => (s.content || '').includes('backup')); const backupStorages = (r.storages || []).filter(s => (s.content || '').includes('backup'));
const sel = document.getElementById('ajStorage'); const sel = document.getElementById('ajStorage');
sel.innerHTML = ''; sel.innerHTML = '';
@@ -854,7 +855,7 @@ async function submitSaveJob() {
try { try {
const url = jobId ? `/proxmox/api/backups/jobs/${jobId}` : '/proxmox/api/backups/jobs'; const url = jobId ? `/proxmox/api/backups/jobs/${jobId}` : '/proxmox/api/backups/jobs';
const method = jobId ? 'PUT' : 'POST'; const method = jobId ? 'PUT' : 'POST';
const r = await fetchWithAuth(url, { method, body: JSON.stringify(payload) }); const r = await (await fetchWithAuth(url, { method, body: JSON.stringify(payload) })).json();
if (r.success) { if (r.success) {
bootstrap.Modal.getInstance(document.getElementById('addJobModal'))?.hide(); bootstrap.Modal.getInstance(document.getElementById('addJobModal'))?.hide();
showToast('{{ t("success") }}', '{{ t("saved") }}', 'success'); showToast('{{ t("success") }}', '{{ t("saved") }}', 'success');
@@ -865,14 +866,14 @@ async function submitSaveJob() {
async function toggleJob(jobId) { async function toggleJob(jobId) {
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/jobs/${jobId}/toggle`, { method: 'PATCH' }); const r = await (await fetchWithAuth(`/proxmox/api/backups/jobs/${jobId}/toggle`, { method: 'PATCH' })).json();
if (r.success) loadJobs(); if (r.success) loadJobs();
} catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); } } catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); }
} }
async function runJobNow(jobId) { async function runJobNow(jobId) {
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/jobs/${jobId}/run-now`, { method: 'POST' }); const r = await (await fetchWithAuth(`/proxmox/api/backups/jobs/${jobId}/run-now`, { method: 'POST' })).json();
if (r.success) { showToast('{{ t("success") }}', '{{ t("backup_job_triggered") }}', 'success'); setTimeout(loadJobs, 2000); } if (r.success) { showToast('{{ t("success") }}', '{{ t("backup_job_triggered") }}', 'success'); setTimeout(loadJobs, 2000); }
} catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); } } catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); }
} }
@@ -880,7 +881,7 @@ async function runJobNow(jobId) {
async function deleteJob(jobId) { async function deleteJob(jobId) {
if (!confirm('{{ t("backup_job_delete_confirm") }}')) return; if (!confirm('{{ t("backup_job_delete_confirm") }}')) return;
try { try {
const r = await fetchWithAuth(`/proxmox/api/backups/jobs/${jobId}`, { method: 'DELETE' }); const r = await (await fetchWithAuth(`/proxmox/api/backups/jobs/${jobId}`, { method: 'DELETE' })).json();
if (r.success) { showToast('{{ t("success") }}', '{{ t("deleted") }}', 'success'); loadJobs(); } if (r.success) { showToast('{{ t("success") }}', '{{ t("deleted") }}', 'success'); loadJobs(); }
} catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); } } catch(e) { showToast('{{ t("error") }}', e.message, 'danger'); }
} }

View File

@@ -554,7 +554,7 @@ body.update-in-progress .layout {
<svg class="material-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg> <svg class="material-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect><line x1="8" y1="21" x2="16" y2="21"></line><line x1="12" y1="17" x2="12" y2="21"></line></svg>
<span>{{ t('nav_vms') }}</span> <span>{{ t('nav_vms') }}</span>
</a></li> </a></li>
<li><a href="/vms" class="{{ 'active' if request.path == '/vms' or (request.path.startswith('/vms/') or (request.path.startswith('/proxmox/') and not request.path.startswith('/proxmox/server'))) else '' }}" title="{{ t('nav_proxmox') }}"> <li><a href="/vms" class="{{ 'active' if request.path == '/vms' or (request.path.startswith('/vms/') or (request.path.startswith('/proxmox/') and not request.path.startswith('/proxmox/server') and not request.path.startswith('/proxmox/api/backups'))) else '' }}" title="{{ t('nav_proxmox') }}">
<svg class="material-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg> <svg class="material-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
<span>{{ t('nav_proxmox') }}</span> <span>{{ t('nav_proxmox') }}</span>
</a></li> </a></li>

View File

@@ -45,7 +45,8 @@ services:
ports: ports:
- "8000:8000" - "8000:8000"
env_file: env_file:
- ./backend/.env - path: ./backend/.env
required: false
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy