let imagesData = []; let imagesSort = { field: null, dir: null }; async function loadImages() { const res = await fetch("/api/images"); const images = await res.json(); imagesData = images; updateSortIndicators(); applyImageSorting(); } function renderImages(images) { const tbody = document.getElementById("images-tbody"); tbody.innerHTML = ""; images.forEach(img => { const tr = document.createElement("tr"); const repoTag = (img.RepoTags && img.RepoTags.length > 0) ? img.RepoTags[0] : ""; const shortId = img.Id.substring(0, 12); const sizeMB = (img.Size / 1024 / 1024).toFixed(1); const created = img.Created ? new Date(img.Created * 1000).toLocaleString() : "-"; const containers = img.Containers || 0; const fullId = img.Id; const status = containers > 0 ? `In use` : `Unused`; const disabled = containers > 0 ? "disabled" : ""; tr.innerHTML = ` ${repoTag} ${shortId} ${sizeMB} MB ${created} ${containers} ${status} `; tbody.appendChild(tr); }); } function toggleSelectAllImages(master) { document.querySelectorAll(".image-checkbox:not(:disabled)") .forEach(cb => cb.checked = master.checked); } async function removeSingleImage(id) { if (!confirm("Image verwijderen?")) return; await fetch("/api/images/" + encodeURIComponent(id), { method: "DELETE" }); await loadImages(); } async function removeSelectedImages() { const selected = Array.from(document.querySelectorAll(".image-checkbox:checked")) .map(cb => cb.value); if (!selected.length) { alert("Geen images geselecteerd."); return; } if (!confirm("Geselecteerde images verwijderen?")) return; await fetch("/api/images/remove", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ images: selected }) }); await loadImages(); } async function pruneUnusedImages() { if (!confirm("Alle unused images verwijderen?")) return; await fetch("/api/images/prune?all=true", { method: "POST" }); await loadImages(); } // ---------- Build Modal ---------- function openBuildModal() { document.getElementById("buildModalBack").style.display = "flex"; document.getElementById("buildOutput").value = ""; const ctxEl = document.getElementById("buildContext"); const tagEl = document.getElementById("buildTag"); // Reset auto-mode tagEl.dataset.auto = "1"; // Update tag whenever context changes ctxEl.oninput = () => { if (tagEl.dataset.auto === "1") { const suggestion = suggestTagFromContext(ctxEl.value); tagEl.value = suggestion; } }; // If user types manually → stop auto mode tagEl.oninput = () => { tagEl.dataset.auto = "0"; }; } function hideBuildModal() { document.getElementById("buildModalBack").style.display = "none"; } function closeBuildModal(e) { if (e.target.id === "buildModalBack") hideBuildModal(); } async function buildImage() { const context = document.getElementById("buildContext").value.trim(); const dockerfile = document.getElementById("buildDockerfile").value.trim(); const tag = document.getElementById("buildTag").value.trim(); const pull = document.getElementById("buildPull").checked; const nocache = document.getElementById("buildNoCache").checked; const outputBox = document.getElementById("buildOutput"); if (!context || !dockerfile || !tag) { alert("Vul context_dir, dockerfile en tag in."); return; } if (!ensureSystemdContextOrAlert(context)) { return; } outputBox.value = "Starting build...\n"; try { const res = await fetch("/api/images/build", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ context_dir: context, dockerfile: dockerfile, tag: tag, pull: pull, nocache: nocache }) }); const ct = (res.headers.get("content-type") || "").toLowerCase(); let data; if (ct.includes("application/json")) { data = await res.json(); } else { const text = await res.text(); data = { ok: res.ok, status: res.status, non_json: true, body: text.slice(0, 4000) }; } if (!res.ok) { outputBox.value += "\nERROR:\n" + JSON.stringify(data, null, 2); return; } outputBox.value += data.output || "Build completed."; await loadImages(); } catch (err) { outputBox.value += "\nERROR:\n" + err.message; } } // ---------- Dockerfile picker ---------- function openDockerfilePicker() { document.getElementById("dfPickerBack").style.display = "flex"; const search = document.getElementById("dfPickerSearch"); if (search) { search.value = ""; search.oninput = () => renderDockerfilePickerList(window.__dfPickerAll || []); } refreshDockerfilePicker(); } function hideDockerfilePicker() { document.getElementById("dfPickerBack").style.display = "none"; } function closeDockerfilePicker(e) { if (e.target.id === "dfPickerBack") hideDockerfilePicker(); } async function refreshDockerfilePicker() { const listEl = document.getElementById("dfPickerList"); listEl.textContent = "Laden..."; try { const res = await fetch("/api/files/tree"); const tree = await res.json(); // [{path:"systemd/..", files:[...]}] const candidates = []; for (const folder of (tree || [])) { const folderPath = (folder.path || "").replace(/^\/+/, ""); // e.g. systemd/foo if (!folderPath || !(folderPath === "systemd" || folderPath.startsWith("systemd/"))) continue; const files = folder.files || []; for (const f of files) { if (!isDockerfileName(f)) continue; // full path under workloads-root (without leading slash) const full = folderPath === "systemd" ? `systemd/${f}` : `${folderPath}/${f}`; candidates.push(full); } } // sort nice candidates.sort((a, b) => a.localeCompare(b)); window.__dfPickerAll = candidates; renderDockerfilePickerList(candidates); } catch (e) { listEl.textContent = "Fout bij laden: " + (e.message || e); } } function isDockerfileName(name) { const n = String(name || "").toLowerCase(); if (n === "dockerfile" || n === "containerfile") return true; if (n.endsWith(".dockerfile") || n.endsWith(".containerfile")) return true; return false; } function renderDockerfilePickerList(all) { const listEl = document.getElementById("dfPickerList"); const q = (document.getElementById("dfPickerSearch")?.value || "").trim().toLowerCase(); const filtered = (all || []).filter(p => !q || p.toLowerCase().includes(q)); if (!filtered.length) { listEl.innerHTML = `
Geen matches.
`; return; } // Render as clickable buttons listEl.innerHTML = filtered.map(p => { const safe = p.replace(/"/g, """); return `
${safe}
`; }).join(""); } function chooseDockerfilePath(encodedPath) { const fullPath = decodeURIComponent(encodedPath); const idx = fullPath.lastIndexOf("/"); const contextDir = idx > 0 ? fullPath.substring(0, idx) : "systemd"; const dockerfile = idx > 0 ? fullPath.substring(idx + 1) : fullPath; const ctxEl = document.getElementById("buildContext"); const tagEl = document.getElementById("buildTag"); ctxEl.value = contextDir; document.getElementById("buildDockerfile").value = dockerfile; if (tagEl.dataset.auto !== "0") { const suggestion = suggestTagFromContext(contextDir); tagEl.value = suggestion; tagEl.dataset.auto = "1"; } hideDockerfilePicker(); } // ---------- Build helpers (4.3c) ---------- function suggestTagFromContext(contextDir) { const p = String(contextDir || "").trim().replace(/^\/+/, ""); if (!p.startsWith("systemd/")) return ""; const parts = p.split("/").filter(Boolean); // Als alleen "systemd" of "systemd/" → geen geldige image naam if (parts.length <= 1) return ""; const name = parts[parts.length - 1]; const safe = name .toLowerCase() .replace(/[^a-z0-9._-]+/g, "-") .replace(/-+/g, "-") .replace(/^-|-$/g, ""); return safe ? `localhost/${safe}:latest` : ""; } function ensureSystemdContextOrAlert(contextDir) { const p = String(contextDir || "").trim().replace(/^\/+/, ""); if (!p.startsWith("systemd/")) { alert("Context directory moet beginnen met: systemd/\nVoorbeeld: systemd/buildtests/hello"); return false; } return true; } function sortImages(field) { if (imagesSort.field !== field) { imagesSort.field = field; imagesSort.dir = "asc"; } else if (imagesSort.dir === "asc") { imagesSort.dir = "desc"; } else if (imagesSort.dir === "desc") { imagesSort.field = null; imagesSort.dir = null; } else { imagesSort.dir = "asc"; } updateSortIndicators(); applyImageSorting(); } function applyImageSorting() { let data = [...imagesData]; if (imagesSort.field && imagesSort.dir) { data.sort((a, b) => { let va, vb; switch (imagesSort.field) { case "repo": va = (a.RepoTags && a.RepoTags[0]) || ""; vb = (b.RepoTags && b.RepoTags[0]) || ""; break; case "id": va = a.Id || ""; vb = b.Id || ""; break; case "size": va = a.Size || 0; vb = b.Size || 0; break; case "created": va = a.Created || 0; vb = b.Created || 0; break; case "containers": va = a.Containers || 0; vb = b.Containers || 0; break; } if (typeof va === "string") { return imagesSort.dir === "asc" ? va.localeCompare(vb) : vb.localeCompare(va); } else { return imagesSort.dir === "asc" ? va - vb : vb - va; } }); } renderImages(data); } function updateSortIndicators() { // default: toon dat alles sorteerbaar is document.querySelectorAll(".sort-indicator").forEach(el => el.textContent = "↕"); // als er geen sort actief is: laat defaults staan if (!imagesSort.field || !imagesSort.dir) return; // actieve kolom: ▲ of ▼ const el = document.getElementById("sort-" + imagesSort.field); if (el) { el.textContent = imagesSort.dir === "asc" ? "▲" : "▼"; } }