let volumesData = []; let volumeContainersMap = {}; async function loadVolumes() { const tbody = document.getElementById("volumes-tbody"); try { const [volumes, containers] = await Promise.all([ fetch("/api/volumes").then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); }), fetch("/api/containers-dashboard").then(r => r.ok ? r.json() : []).catch(() => []) ]); volumesData = Array.isArray(volumes) ? volumes : []; // containers-dashboard geeft Mounts als strings (destination paden). // Volledige mount-info (Type + Name) zit alleen in de inspect endpoint. // Haal inspect op voor alle containers met niet-lege Mounts, parallel. const containerList = Array.isArray(containers) ? containers : []; const withMounts = containerList.filter(c => (c.Mounts || []).length > 0); const inspectResults = await Promise.all( withMounts.map(c => { const name = (c.Names && c.Names[0]) || ""; if (!name) return Promise.resolve(null); return fetch("/api/containers/inspect/" + encodeURIComponent(name)) .then(r => r.ok ? r.json() : null) .catch(() => null); }) ); // Bouw volume → containers mapping: filter op Type === "volume" volumeContainersMap = {}; for (let i = 0; i < withMounts.length; i++) { const inspect = inspectResults[i]; if (!inspect) continue; const cname = (withMounts[i].Names && withMounts[i].Names[0]) || ""; for (const m of (inspect.Mounts || [])) { if (m.Type === "volume" && m.Name) { (volumeContainersMap[m.Name] = volumeContainersMap[m.Name] || []).push(cname); } } } if (typeof window.updateNavCount === "function") { window.updateNavCount("countNavVolumes", volumesData.length); } renderVolumes(volumesData); } catch (e) { volumesData = []; if (typeof window.updateNavCount === "function") window.updateNavCount("countNavVolumes", 0); if (tbody) { const box = typeof window.renderStateBox === "function" ? window.renderStateBox("error", "Volumes laden mislukt", e.message || String(e)) : "Volumes laden mislukt."; tbody.innerHTML = `${box}`; } } } function _volRelTime(isoStr) { if (!isoStr) return "-"; const d = new Date(isoStr); if (isNaN(d)) return String(isoStr); const s = Math.floor((Date.now() - d.getTime()) / 1000); if (s < 60) return `${s}s geleden`; if (s < 3600) return `${Math.floor(s / 60)}m geleden`; if (s < 86400) return `${Math.floor(s / 3600)}u geleden`; return `${Math.floor(s / 86400)} dagen geleden`; } function _volEsc(s) { return String(s || "") .replace(/&/g, "&") .replace(/"/g, """) .replace(//g, ">"); } function renderVolumes(volumes) { const tbody = document.getElementById("volumes-tbody"); if (!tbody) return; tbody.innerHTML = ""; if (!volumes.length) { const box = typeof window.renderStateBox === "function" ? window.renderStateBox("empty", "Geen volumes", "Er zijn momenteel geen volumes gevonden.") : "Geen volumes gevonden."; tbody.innerHTML = `${box}`; return; } volumes.forEach(vol => { const name = vol.Name || "-"; const driver = vol.Driver || "-"; const mp = vol.Mountpoint || ""; const mpShort = mp.length > 45 ? mp.slice(0, 42) + "…" : mp; const created = _volRelTime(vol.CreatedAt); const labels = vol.Labels || {}; const cNames = volumeContainersMap[name] || []; const inUse = cNames.length > 0; const labelHtml = Object.keys(labels).length ? Object.keys(labels).map(k => `${_volEsc(k)}` ).join(" ") : `-`; const containersHtml = cNames.length ? cNames.map(n => `${_volEsc(n)}`).join(" ") : `-`; const nameEnc = encodeURIComponent(name); const disabledAttr = inUse ? `disabled title="In gebruik door een container"` : ""; const tr = document.createElement("tr"); tr.innerHTML = ` ${_volEsc(name)} ${_volEsc(driver)} ${_volEsc(mpShort)} ${created} ${labelHtml} ${containersHtml} `; tbody.appendChild(tr); }); } async function removeVolume(name) { if (!confirm(`Volume '${name}' verwijderen?\nDit kan niet ongedaan worden gemaakt.`)) return; try { const res = await fetch("/api/volumes/" + encodeURIComponent(name), { method: "DELETE" }); if (!res.ok) { const body = await res.text().catch(() => ""); alert(`Verwijderen mislukt (${res.status}): ${body}`); return; } await loadVolumes(); } catch (e) { alert(`Fout: ${e.message}`); } } async function pruneVolumes() { if (!confirm( "Prune volumes\n\n" + "Dit verwijdert alle volumes die niet aan een container gekoppeld zijn.\n" + "Dit kan niet ongedaan worden gemaakt.\n\n" + "Doorgaan?" )) return; try { const res = await fetch("/api/volumes/prune", { method: "POST" }); if (!res.ok) { const body = await res.text().catch(() => ""); alert(`Prune mislukt (${res.status}): ${body}`); return; } const data = await res.json(); const removed = Array.isArray(data) ? data.length : 0; alert(`Prune voltooid. ${removed} volume(s) verwijderd.`); await loadVolumes(); } catch (e) { alert(`Fout: ${e.message}`); } } // ---- Create Volume Modal ---- function openCreateVolumeModal() { document.getElementById("createVolumeModalBack").style.display = "flex"; document.getElementById("createVolumeName").value = ""; document.getElementById("createVolumeLabels").value = ""; } function hideCreateVolumeModal() { document.getElementById("createVolumeModalBack").style.display = "none"; } function closeCreateVolumeModal(e) { if (e.target.id === "createVolumeModalBack") hideCreateVolumeModal(); } async function createVolume() { const name = document.getElementById("createVolumeName").value.trim(); if (!name) { alert("Naam is verplicht."); return; } const labelsRaw = document.getElementById("createVolumeLabels").value.trim(); const labels = {}; if (labelsRaw) { for (const line of labelsRaw.split(/\r?\n/)) { const l = line.trim(); if (!l) continue; const idx = l.indexOf("="); if (idx > 0) labels[l.slice(0, idx).trim()] = l.slice(idx + 1).trim(); } } const body = { name }; if (Object.keys(labels).length) body.labels = labels; try { const res = await fetch("/api/volumes", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) }); if (!res.ok) { const err = await res.text().catch(() => ""); alert(`Aanmaken mislukt (${res.status}): ${err}`); return; } hideCreateVolumeModal(); await loadVolumes(); } catch (e) { alert(`Fout: ${e.message}`); } }