diff --git a/webui/html/assets/js/tabs/images.js b/webui/html/assets/js/tabs/images.js
index 382ea59..ba124c9 100644
--- a/webui/html/assets/js/tabs/images.js
+++ b/webui/html/assets/js/tabs/images.js
@@ -17,6 +17,7 @@ function renderImages(images) {
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;
@@ -33,6 +34,7 @@ function renderImages(images) {
${repoTag} |
${shortId} |
${sizeMB} MB |
+ ${created} |
${containers} |
${status} |
@@ -89,4 +91,224 @@ async function pruneUnusedImages() {
});
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 data = await res.json();
+
+ 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;
}
\ No newline at end of file
diff --git a/webui/html/index.html b/webui/html/index.html
index 2f9249e..ce21eb5 100644
--- a/webui/html/index.html
+++ b/webui/html/index.html
@@ -153,6 +153,7 @@
+
@@ -165,6 +166,7 @@
| Repo / Tag |
ID |
Size |
+ Created |
Containers |
Status |
Acties |
@@ -231,6 +233,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Alleen bestanden onder systemd/ die Dockerfile,
+ Containerfile of eindigen op .dockerfile/.containerfile.
+
+
+
+
+
+
+