diff --git a/webui/html/assets/js/tabs/containers.js b/webui/html/assets/js/tabs/containers.js
new file mode 100644
index 0000000..81f7852
--- /dev/null
+++ b/webui/html/assets/js/tabs/containers.js
@@ -0,0 +1,394 @@
+// ---- Pods ----
+
+async function podAction(action, name) {
+ try {
+ const res = await api(`/pods/actions/${encodeURIComponent(action)}/${encodeURIComponent(name)}`, 'POST');
+ showModal(`Pod ${action}: ${name}`, JSON.stringify(res, null, 2));
+
+ await fetchContainers(); // refresh pods + containers view in ÊÊn keer
+ } catch (e) {
+ showModal(`Pod ${action} fout`, e.stack || e.message);
+ }
+}
+
+// ---- Containers ----
+
+function _podKey(c) {
+ return (c.PodName && String(c.PodName).trim()) ? String(c.PodName).trim() : '(geen pod)';
+}
+
+function _cmpStr(a, b) {
+ return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });
+}
+
+function _isCollapsed(pod) {
+ return localStorage.getItem('pod_group_collapsed:' + pod) === '1';
+}
+
+function _setCollapsed(pod, v) {
+ localStorage.setItem('pod_group_collapsed:' + pod, v ? '1' : '0');
+}
+
+function renderActionsDropdown(menuId, actionFn, targetEsc) {
+ // actionFn is string: "containerAction" of "podAction"
+ // targetEsc is al esc(...) dus veilig in onclick
+ return `
+
+ `;
+}
+
+function renderContainerRow(c) {
+ const name = (c.Names && c.Names[0]) ? c.Names[0] : (c.Names || c.Name || c.name || '');
+ const status = c.Status || c.State || c.state || '';
+ const podName = c.PodName || '-';
+ const image = c.Image || c.image || '';
+ const managed = c._dashboard_source || 'podman';
+ const menuId = `menu-${cssSafeId('c:' + normalizeContainerName(name))}`;
+ const inPod = !!(c.PodName && String(c.PodName).trim());
+ const ports = inPod
+ ? '' // verberg bij pod-containers
+ : ((c._dashboard_published_ports || []).join(", ")
+ || (c.Ports || []).map(p => `${p.host_port}:${p.container_port}`).join(", "));
+
+ return `
+
+ | ${esc(name)} |
+ ${badgeFromStatus(status)} |
+ ${podName} |
+ ${esc(image)} |
+ ${badgeFromStatus(managed)} |
+ - |
+ - |
+ ${ports || '-'} |
+
+
+
+
+ ${renderActionsDropdown(menuId, 'containerAction', esc(name))}
+
+ |
+
+ `;
+}
+
+let containersC2P = new Map();
+
+function renderContainersGrouped(list, tbody, podStatus) {
+ const groups = new Map();
+ for (const c of (list || [])) {
+ const k = _podKey(c);
+ if (!groups.has(k)) groups.set(k, []);
+ groups.get(k).push(c);
+ }
+
+ for (const podName of Object.keys(podStatus || {})) {
+ if (!groups.has(podName)) groups.set(podName, []);
+ }
+
+ const keys = Array.from(groups.keys()).sort((a, b) => {
+ if (a === '(geen pod)' && b !== '(geen pod)') return 1;
+ if (b === '(geen pod)' && a !== '(geen pod)') return -1;
+ return _cmpStr(a, b);
+ });
+
+ containersC2P = new Map();
+
+ let html = '';
+
+ for (const pod of keys) {
+ const items = groups.get(pod) || [];
+ items.sort((a, b) =>
+ _cmpStr(
+ (a.Names && a.Names[0]) ? a.Names[0] : (a.Name || ''),
+ (b.Names && b.Names[0]) ? b.Names[0] : (b.Name || '')
+ )
+ );
+
+ const collapsed = _isCollapsed(pod);
+ const isRealPod = (pod !== '(geen pod)');
+ const total = items.length;
+ const podMenuId = `menu-${cssSafeId('p:' + pod)}`;
+
+ let podPortsText = "-";
+ if (isRealPod) {
+ const podPorts = Array.from(new Set(
+ items.flatMap(c => (c._dashboard_published_ports || []))
+ )).sort((a, b) => _cmpStr(a, b));
+ podPortsText = podPorts.length ? podPorts.join(", ") : "-";
+ }
+
+ let cls = 'muted';
+ let label = '-';
+
+ if (total > 0) {
+ const running = items.filter(x => {
+ const s = (x.Status || x.State || '').toLowerCase();
+ return s === 'running';
+ }).length;
+
+ label = `${running}/${total}`;
+ if (running === total) cls = 'ok';
+ else if (running === 0) cls = 'bad';
+ else cls = 'warn';
+ } else {
+ const ps = String((podStatus && podStatus[pod]) || '').toLowerCase();
+ if (ps === 'active') { cls = 'ok'; label = 'active'; }
+ else if (ps === 'inactive') { cls = 'bad'; label = 'inactive'; }
+ else { cls = 'muted'; label = ps || 'unknown'; }
+ }
+
+ html += `
+
+ |
+
+ ${collapsed ? 'âļ' : 'âŧ'} ${esc(pod)}
+
+ |
+
+
+
+ ${label}
+
+ |
+
+ - |
+ - |
+ ${isRealPod ? 'pod' : '-'} |
+
+ - |
+ - |
+ ${esc(podPortsText)} |
+
+ ${isRealPod ? `
+
+
+
+
+
+ ${renderActionsDropdown(podMenuId, 'podAction', esc(pod))}
+
+ ` : '-'}
+ |
+
+ `;
+
+ for (const c of items) {
+ const cname = normalizeContainerName((c.Names && c.Names[0]) ? c.Names[0] : (c.Names || c.Name || c.name || ''));
+ if (cname) containersC2P.set(cname, pod);
+ const row = renderContainerRow(c).replace(
+ ' {
+ const t = ev.target.closest('.pod-toggle');
+ if (!t) return;
+
+ const podEnc = t.getAttribute('data-pod') || '';
+ const pod = decodeURIComponent(podEnc);
+ const isNowCollapsed = !_isCollapsed(pod);
+ _setCollapsed(pod, isNowCollapsed);
+
+ tbody.querySelectorAll(`.pod-item-row[data-pod="${CSS.escape(podEnc)}"]`).forEach(r => {
+ r.style.display = isNowCollapsed ? 'none' : '';
+ });
+
+ t.innerHTML =
+ `${isNowCollapsed ? 'âļ' : 'âŧ'} ${pod} ` +
+ `(${tbody.querySelectorAll(`.pod-item-row[data-pod="${CSS.escape(podEnc)}"]`).length})`;
+ };
+}
+
+async function fetchContainers() {
+ const [containers, pods] = await Promise.all([
+ api('/containers-dashboard', 'GET'),
+ api('/pods-dashboard', 'GET')
+ ]);
+
+ const list = Array.isArray(containers) ? containers : (containers?.containers || []);
+ document.getElementById('countContainers').textContent = list.length;
+
+ const podsList = Array.isArray(pods) ? pods : [];
+ const podStatus = {};
+ for (const p of podsList) {
+ const n = p?.Name || p?.name;
+ if (!n) continue;
+ podStatus[n] = (p?.Status || p?.status || '').toLowerCase(); // "active"/"inactive"/...
+ }
+
+ const tbody = document.getElementById('containersTbody');
+ if (!tbody) return;
+ renderContainersGrouped(list, tbody, podStatus);
+}
+
+let containersDashboardStatsTimer = null;
+let containersDashboardStatsInFlight = false;
+
+function startContainersDashboardStatsPoll() {
+ if (containersDashboardStatsTimer) return;
+ // immediate tick, daarna elke 1s
+ pollContainersDashboardStatsOnce();
+ containersDashboardStatsTimer = setInterval(pollContainersDashboardStatsOnce, 1000);
+}
+
+function stopContainersDashboardStatsPoll() {
+ if (!containersDashboardStatsTimer) return;
+ clearInterval(containersDashboardStatsTimer);
+ containersDashboardStatsTimer = null;
+ resetPodTotals();
+}
+
+async function pollContainersDashboardStatsOnce() {
+ if (containersDashboardStatsInFlight) return;
+ containersDashboardStatsInFlight = true;
+ try {
+ const containers = await api('/containers-dashboard', 'GET');
+ const list = Array.isArray(containers) ? containers : (containers?.containers || []);
+
+ // totals per pod voor deze poll tick
+ const podCpu = new Map(); // podName -> cpuPct sum
+ const podMem = new Map(); // podName -> memBytes sum
+ const podMemPct = new Map(); // podName -> memPct sum
+
+ for (const c of (list || [])) {
+ const cname = normalizeContainerName((c?.Names && c.Names[0]) ? c.Names[0] : (c?.Names || c?.Name || c?.name || ''));
+ if (!cname) continue;
+
+ const key = cssSafeId(cname);
+
+ const cpuRaw = c?._dashboard_cpu;
+ const memBytesRaw = c?._dashboard_mem_usage;
+ const memPctRaw = c?._dashboard_mem_perc;
+
+ const cpuPct = Number(cpuRaw);
+ const memBytes = Number(memBytesRaw);
+ const memPct = Number(memPctRaw);
+
+ const pod = containersC2P.get(cname);
+ if (pod) {
+ if (Number.isFinite(cpuPct)) podCpu.set(pod, (podCpu.get(pod) || 0) + cpuPct);
+ if (Number.isFinite(memBytes)) podMem.set(pod, (podMem.get(pod) || 0) + memBytes);
+ if (Number.isFinite(memPct)) podMemPct.set(pod, (podMemPct.get(pod) || 0) + memPct);
+ }
+
+ const cpuEl = document.getElementById(`cpu-${key}`);
+ if (cpuEl) cpuEl.textContent = Number.isFinite(cpuPct) ? (cpuPct.toFixed(2) + "%") : "-";
+
+ const memEl = document.getElementById(`mem-${key}`);
+ if (memEl) {
+ if (Number.isFinite(memBytes) && Number.isFinite(memPct)) {
+ memEl.textContent = `${formatBytes(memBytes)} (${memPct.toFixed(1)}%)`;
+ } else {
+ memEl.textContent = "-";
+ }
+ }
+ }
+
+ for (const [pod, cpuSum] of podCpu.entries()) {
+ const el = document.getElementById(`podcpu-${cssSafeId(pod)}`);
+ if (el) {
+ el.textContent = cpuSum.toFixed(2) + "%";
+ el.classList.remove('stale');
+ }
+ }
+
+ for (const [pod, memSum] of podMem.entries()) {
+ const el = document.getElementById(`podmem-${cssSafeId(pod)}`);
+ if (el) {
+ const mp = podMemPct.get(pod) || 0;
+ el.textContent = `${formatBytes(memSum)} (${Number(mp).toFixed(1)}%)`;
+ el.classList.remove('stale');
+ }
+ }
+ } catch (e) {
+ resetPodTotals();
+ } finally {
+ containersDashboardStatsInFlight = false;
+ }
+}
+
+function resetPodTotals() {
+ document.querySelectorAll('[id^="podcpu-"]').forEach(el => {
+ const ps = String(el.getAttribute('data-podstatus') || '').toLowerCase();
+ if (ps === 'inactive') {
+ el.textContent = '-';
+ el.classList.remove('stale');
+ } else {
+ el.textContent = 'â';
+ el.classList.add('stale');
+ }
+ });
+
+ document.querySelectorAll('[id^="podmem-"]').forEach(el => {
+ const ps = String(el.getAttribute('data-podstatus') || '').toLowerCase();
+ if (ps === 'inactive') {
+ el.textContent = '-';
+ el.classList.remove('stale');
+ } else {
+ el.textContent = 'â';
+ el.classList.add('stale');
+ }
+ });
+}
+
+async function containerInspect(name) {
+ try {
+ const res = await api(`/containers/inspect/${encodeURIComponent(name)}`, 'GET');
+ showModal(`Inspect: ${name}`, JSON.stringify(res, null, 2));
+ } catch (e) {
+ showModal(`Inspect fout: ${name}`, e.stack || e.message);
+ }
+}
+
+async function containerLogs(name) {
+ try {
+ const res = await api(`/containers/logs/${encodeURIComponent(name)}`, 'GET');
+ const logs = res.logs ?? JSON.stringify(res, null, 2);
+ showModal(`Logs: ${name}`, logs);
+ } catch (e) {
+ showModal(`Logs fout: ${name}`, e.stack || e.message);
+ }
+}
+
+async function containerAction(action, name) {
+ try {
+ const res = await api(`/containers/${encodeURIComponent(action)}/${encodeURIComponent(name)}`, 'POST');
+ showModal(`Container ${action}: ${name}`, JSON.stringify(res, null, 2));
+ await fetchContainers();
+ } catch (e) {
+ showModal(`Container ${action} fout`, e.stack || e.message);
+ }
+}
+
+function cssSafeId(s) {
+ const bytes = new TextEncoder().encode(String(s));
+ let bin = '';
+ bytes.forEach(b => bin += String.fromCharCode(b));
+ return btoa(bin).replaceAll('=','').replaceAll('+','-').replaceAll('/','_');
+}
+
+function normalizeContainerName(n) {
+ if (!n) return '';
+ // Podman kan soms "/name" geven, we strippen de leading slash
+ return ('' + n).replace(/^\//, '');
+}
+
+function formatBytes(b) {
+ const n = Number(b || 0);
+ if (n >= 1024**3) return (n / 1024**3).toFixed(1) + "GiB";
+ if (n >= 1024**2) return (n / 1024**2).toFixed(0) + "MiB";
+ if (n >= 1024) return (n / 1024).toFixed(0) + "KiB";
+ return n + "B";
+}
diff --git a/webui/html/assets/js/tabs/files.js b/webui/html/assets/js/tabs/files.js
new file mode 100644
index 0000000..73f06e5
--- /dev/null
+++ b/webui/html/assets/js/tabs/files.js
@@ -0,0 +1,339 @@
+let cmEditor = null;
+
+function _isFolderCollapsed(folderKey) {
+ return localStorage.getItem('files_folder_collapsed:' + folderKey) !== '0';
+ // default collapsed = true
+}
+
+function _setFolderCollapsed(folderKey, v) {
+ localStorage.setItem('files_folder_collapsed:' + folderKey, v ? '1' : '0');
+}
+
+// =========================
+// Files tab (systemd subtree)
+// =========================
+const FILES_ROOT = 'systemd'; // API-root binnen WORKLOADS_DIR
+let filesCurrentUiPath = ''; // zonder "systemd/"
+let filesCurrentApiPath = ''; // met "systemd/"
+
+function cmModeForPath(uiPath) {
+ const p = (uiPath || '').toLowerCase();
+ if (p.endsWith('.yaml') || p.endsWith('.yml') || p.endsWith('.kube') || p.endsWith('.container')) return 'yaml';
+ if (p.endsWith('.json')) return 'application/json';
+ if (p.endsWith('.js')) return 'javascript';
+ return 'text/plain';
+}
+
+function filesToApiPath(uiPath) {
+ let p = (uiPath || '').trim().replace(/^\/+/, '');
+ if (!p) return FILES_ROOT;
+ if (p === FILES_ROOT || p.startsWith(FILES_ROOT + '/')) return p;
+ return `${FILES_ROOT}/${p}`;
+}
+
+function filesToUiPath(apiPath) {
+ const p = (apiPath || '').trim().replace(/^\/+/, '');
+ return p.replace(new RegExp('^' + FILES_ROOT + '/?'), '');
+}
+
+function filesSetCurrent(uiPath) {
+ filesCurrentUiPath = (uiPath || '').trim().replace(/^\/+/, '');
+ filesCurrentApiPath = filesToApiPath(filesCurrentUiPath);
+ document.getElementById('filesCurrent').textContent = filesCurrentUiPath || '-';
+}
+
+async function filesRefresh() {
+ // Files editor: CodeMirror init (alleen als textarea bestaat)
+ if (!cmEditor) {
+ const taFiles = document.getElementById('filesEditor');
+ if (taFiles && window.CodeMirror) {
+ cmEditor = CodeMirror.fromTextArea(taFiles, {
+ lineNumbers: true,
+ lineWrapping: true,
+ mode: 'text/plain',
+ theme: 'material-darker'
+ });
+ cmEditor.setSize('100%', 360);
+ }
+ }
+
+ const treeEl = document.getElementById('filesTree');
+ treeEl.textContent = 'Laden...';
+
+ const data = await api('/files/tree', 'GET');
+
+ // Filter alleen systemd subtree
+ const scoped = (data || []).filter(folder => {
+ const p = (folder.path || '').replace(/^\/+/, '');
+ return p === FILES_ROOT || p.startsWith(FILES_ROOT + '/');
+ });
+
+ if (!scoped.length) {
+ treeEl.textContent = 'Geen bestanden gevonden onder systemd.';
+ return;
+ }
+
+ // Bouw een geneste folder-tree uit de "platte" API response.
+ const folderByPath = new Map();
+ for (const f of scoped) {
+ const apiPath = (f.path || '').replace(/^\/+/, '');
+ folderByPath.set(apiPath, f);
+ }
+
+ function getOrCreateChild(parent, name) {
+ if (!parent.children.has(name)) {
+ const apiPath = parent.apiPath ? `${parent.apiPath}/${name}` : name;
+ parent.children.set(name, {
+ name,
+ apiPath,
+ uiPath: filesToUiPath(apiPath),
+ children: new Map(),
+ });
+ }
+ return parent.children.get(name);
+ }
+
+ const root = { name: FILES_ROOT, apiPath: FILES_ROOT, uiPath: '', children: new Map() };
+
+ // 1) Nodes aanmaken op basis van bekende folder paths
+ for (const apiPath of folderByPath.keys()) {
+ if (apiPath === FILES_ROOT) continue;
+ if (!apiPath.startsWith(FILES_ROOT + '/')) continue;
+ const rel = apiPath.slice((FILES_ROOT + '/').length);
+ const segs = rel.split('/').filter(Boolean);
+ let cur = root;
+ for (const s of segs) cur = getOrCreateChild(cur, s);
+ }
+
+ // 2) Nodes aanvullen op basis van dirs-lijsten (zodat lege tussenfolders ook verschijnen)
+ for (const [apiPath, folder] of folderByPath.entries()) {
+ if (apiPath !== FILES_ROOT && !apiPath.startsWith(FILES_ROOT + '/')) continue;
+
+ let base = root;
+ if (apiPath !== FILES_ROOT) {
+ const rel = apiPath.slice((FILES_ROOT + '/').length);
+ const segs = rel.split('/').filter(Boolean);
+ for (const s of segs) base = getOrCreateChild(base, s);
+ }
+
+ for (const d of (folder.dirs || [])) getOrCreateChild(base, d);
+ }
+
+ function renderNode(node, level) {
+ const folderKey = node.apiPath;
+ const collapsed = _isFolderCollapsed(folderKey);
+ const label = node.uiPath || 'root';
+ const indent = Math.max(0, level) * 14;
+
+ const folder = folderByPath.get(folderKey);
+ const files = (folder && folder.files) ? folder.files : [];
+
+ // subfolders (NU AL beschikbaar voor de badges)
+ const childNames = Array.from(node.children.keys()).sort((a,b) => a.localeCompare(b));
+
+ // files (NU AL beschikbaar voor de badges)
+ const sortedFiles = (files || []).slice().sort((a,b) => a.localeCompare(b));
+
+ const out = [];
+ out.push(`
+
+
+ ${collapsed ? 'âļ' : 'âŧ'}
+ đ ${esc(label)}
+
+
+ đ ${childNames.length}
+ đ ${sortedFiles.length}
+
+
+
+
+
+
+
+ `);
+
+ out.push(``);
+
+ for (const name of childNames) {
+ out.push(renderNode(node.children.get(name), level + 1));
+ }
+
+ for (const f of sortedFiles) {
+ const fullUi = node.uiPath ? `${node.uiPath}/${f}` : f;
+ out.push(`
+
+ đ ${esc(f)}
+
+ `);
+ }
+
+ if (!childNames.length && !sortedFiles.length) {
+ out.push(`
(leeg)
`);
+ }
+
+ out.push(`
`);
+ return out.join('');
+ }
+
+ const parts = [];
+ const topNames = Array.from(root.children.keys()).sort((a,b) => a.localeCompare(b));
+ for (const n of topNames) parts.push(renderNode(root.children.get(n), 0));
+
+ // Files direct onder "systemd/" (root) tonen bovenaan
+ const rootFolder = folderByPath.get(FILES_ROOT);
+ if (rootFolder && (rootFolder.files || []).length) {
+ const folderKey = FILES_ROOT;
+ const collapsed = _isFolderCollapsed(folderKey);
+ parts.unshift(`
+
+
+ ${collapsed ? 'âļ' : 'âŧ'}
+ đ root
+
+
+
+
+
+
+ ${(rootFolder.files || []).slice().sort((a,b)=>a.localeCompare(b)).map(f => {
+ const fullUi = f;
+ return `
+
+ đ ${esc(f)}
+
+ `;
+ }).join('')}
+
+ `);
+ }
+
+ treeEl.innerHTML = parts.join('');
+ treeEl.onclick = (ev) => {
+ const row = ev.target.closest('.file-folder-row');
+ if (!row) return;
+
+ const folderKey = row.getAttribute('data-folder');
+ const isNowCollapsed = !_isFolderCollapsed(folderKey);
+ _setFolderCollapsed(folderKey, isNowCollapsed);
+
+ // pijltje updaten
+ const arrow = row.querySelector('.folder-toggle');
+ if (arrow) arrow.textContent = isNowCollapsed ? 'âļ' : 'âŧ';
+
+ // files block tonen/verbergen
+ const filesBlock = treeEl.querySelector(`[data-folder-files="${CSS.escape(folderKey)}"]`);
+ if (filesBlock) filesBlock.style.display = isNowCollapsed ? 'none' : '';
+ };
+}
+
+async function filesOpen(uiPath) {
+ filesSetCurrent(uiPath);
+
+ const res = await api(`/files/read?path=${encodeURIComponent(filesCurrentApiPath)}`, 'GET');
+
+ const text = res.content || '';
+ if (cmEditor) {
+ cmEditor.setOption('mode', cmModeForPath(uiPath));
+ cmEditor.setValue(text);
+ cmEditor.refresh();
+ } else {
+ document.getElementById('filesEditor').value = text;
+ }
+}
+
+async function filesSave() {
+ if (!filesCurrentApiPath || filesCurrentApiPath === FILES_ROOT) {
+ return showModal('Files', 'Selecteer eerst een bestand.');
+ }
+
+ const content = cmEditor
+ ? cmEditor.getValue()
+ : document.getElementById('filesEditor').value;
+
+ const res = await api(
+ `/files/save?path=${encodeURIComponent(filesCurrentApiPath)}`,
+ 'POST',
+ { content }
+ );
+
+ showModal('Opgeslagen', JSON.stringify(res, null, 2));
+ await filesRefresh();
+}
+
+async function filesDelete() {
+ if (!filesCurrentApiPath || filesCurrentApiPath === FILES_ROOT) {
+ return showModal('Files', 'Selecteer eerst een bestand om te verwijderen.');
+ }
+ if (!confirm(`Verwijderen: ${filesCurrentUiPath}?`)) return;
+
+ const res = await api(`/files/delete?path=${encodeURIComponent(filesCurrentApiPath)}`, 'DELETE');
+ showModal('Verwijderd', JSON.stringify(res, null, 2));
+
+ // reset current
+ filesSetCurrent('');
+ document.getElementById('filesEditor').value = '';
+ await filesRefresh();
+}
+
+async function filesNewFolder() {
+ const ui = prompt('Nieuwe map (onder systemd):\nVoorbeeld: mediaserver', '');
+ if (!ui) return;
+
+ const apiPath = filesToApiPath(ui);
+ const res = await api(`/files/mkdir?path=${encodeURIComponent(apiPath)}`, 'POST');
+ showModal('Map aangemaakt', JSON.stringify(res, null, 2));
+ await filesRefresh();
+}
+
+async function filesNewFile() {
+ const ui = prompt('Nieuw bestand (onder systemd):\nVoorbeeld: demo-web/demo-web.container', '');
+ if (!ui) return;
+
+ const apiPath = filesToApiPath(ui);
+
+ // altijd leeg (jouw keuze) -> leeg bestand
+ const res = await api(`/files/save?path=${encodeURIComponent(apiPath)}`, 'POST', { content: "" });
+ showModal('Bestand aangemaakt', JSON.stringify(res, null, 2));
+
+ // Open direct
+ filesSetCurrent(ui);
+ const editorEl = document.getElementById('filesEditor');
+ if (editorEl) editorEl.value = "";
+ await filesRefresh();
+ await filesOpen(ui);
+}
+
+async function filesNewFileInFolder(uiFolderPath) {
+ const base = (uiFolderPath || '').trim().replace(/^\/+/, '');
+ const name = prompt(`Nieuw bestand in "${base || 'root'}"\nBijv: test.yaml of demo.container`, '');
+ if (!name) return;
+
+ const uiFull = base ? `${base}/${name}` : name;
+ const apiPath = filesToApiPath(uiFull);
+
+ // altijd leeg (jouw keuze)
+ const res = await api(`/files/save?path=${encodeURIComponent(apiPath)}`, 'POST', { content: "" });
+ showModal('Bestand aangemaakt', JSON.stringify(res, null, 2));
+
+ await filesRefresh();
+ await filesOpen(uiFull);
+}
+
+async function filesDeleteFolder(uiFolderPath) {
+ const base = (uiFolderPath || '').trim().replace(/^\/+/, '');
+ if (!base) {
+ return showModal('Files', 'Root map verwijderen mag niet.');
+ }
+ if (!confirm(`Map verwijderen (alleen als leeg): ${base}?`)) return;
+
+ const apiPath = filesToApiPath(base);
+
+ try {
+ const res = await api(`/files/rmdir?path=${encodeURIComponent(apiPath)}`, 'DELETE');
+ showModal('Map verwijderd', JSON.stringify(res, null, 2));
+ await filesRefresh();
+ } catch (e) {
+ showModal('Kan map niet verwijderen', e.message);
+ }
+}
diff --git a/webui/html/index.html b/webui/html/index.html
index 9b91990..62ddbc1 100644
--- a/webui/html/index.html
+++ b/webui/html/index.html
@@ -405,8 +405,9 @@
+
+