let cmEditor = null; let filesDirty = false; let filesSuppressDirtyEvent = false; let filesTextareaBound = false; function filesCurrentTheme() { const t = document.documentElement.getAttribute('data-theme'); return (t === 'light') ? 'light' : 'dark'; } function filesCodeMirrorTheme() { return filesCurrentTheme() === 'light' ? 'default' : 'material-darker'; } function filesSetEditorTheme(themeName) { if (!cmEditor) return; const cmTheme = (themeName === 'light') ? 'default' : 'material-darker'; cmEditor.setOption('theme', cmTheme); cmEditor.refresh(); } window.filesSetEditorTheme = filesSetEditorTheme; 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 filesModeLabel(uiPath) { const mode = cmModeForPath(uiPath); if (mode === 'yaml') return 'YAML'; if (mode === 'application/json') return 'JSON'; if (mode === 'javascript') return 'JavaScript'; return 'Text'; } function filesCursorLabel() { if (cmEditor) { const c = cmEditor.getCursor(); return `Ln ${c.line + 1}, Kol ${c.ch + 1}`; } return ''; } function filesUpdateEditorStatus() { const el = document.getElementById('filesEditorStatus'); if (!el) return; if (!filesCurrentUiPath) { el.textContent = 'Geen bestand geselecteerd'; return; } const dirtyTxt = filesDirty ? 'Niet opgeslagen' : 'Opgeslagen'; const parts = [ dirtyTxt, filesModeLabel(filesCurrentUiPath), filesCurrentUiPath, ]; const cursor = filesCursorLabel(); if (cursor) parts.push(cursor); el.textContent = parts.join(' | '); } function filesUpdateTreeSelection() { const treeEl = document.getElementById('filesTree'); if (!treeEl) return; treeEl.querySelectorAll('.file-entry').forEach(row => { row.classList.remove('active', 'dirty'); const state = row.querySelector('.file-entry-state'); if (state) state.textContent = ''; }); if (!filesCurrentUiPath) return; const key = encodeURIComponent(filesCurrentUiPath); const row = treeEl.querySelector(`.file-entry[data-file="${CSS.escape(key)}"]`); if (!row) return; row.classList.add('active'); if (filesDirty) { row.classList.add('dirty'); const state = row.querySelector('.file-entry-state'); if (state) state.textContent = '●'; } } function filesSetDirty(v) { filesDirty = !!v && !!filesCurrentUiPath; filesUpdateEditorStatus(); filesUpdateTreeSelection(); } 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 || '-'; filesSetDirty(false); } 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: filesCodeMirrorTheme() }); cmEditor.setSize('100%', 360); cmEditor.on('change', () => { if (filesSuppressDirtyEvent || !filesCurrentUiPath) return; filesSetDirty(true); }); cmEditor.on('cursorActivity', filesUpdateEditorStatus); } else if (taFiles && !filesTextareaBound) { filesTextareaBound = true; taFiles.addEventListener('input', () => { if (!filesCurrentUiPath) return; filesSetDirty(true); }); } } const treeEl = document.getElementById('filesTree'); treeEl.textContent = 'Laden...'; let data; try { data = await api('/files/tree', 'GET'); } catch (e) { if (typeof window.updateNavCount === 'function') { window.updateNavCount('countNavFiles', 0); } treeEl.innerHTML = (typeof window.renderStateBox === 'function') ? window.renderStateBox('error', 'Files laden mislukt', e.message || String(e)) : 'Files laden mislukt.'; filesUpdateEditorStatus(); return; } // 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) { if (typeof window.updateNavCount === 'function') { window.updateNavCount('countNavFiles', 0); } treeEl.innerHTML = (typeof window.renderStateBox === 'function') ? window.renderStateBox('empty', 'Geen bestanden', 'Er zijn geen bestanden gevonden onder systemd.') : 'Geen bestanden gevonden onder systemd.'; filesUpdateEditorStatus(); return; } let totalFiles = 0; for (const folder of scoped) { totalFiles += Array.isArray(folder?.files) ? folder.files.length : 0; } if (typeof window.updateNavCount === 'function') { window.updateNavCount('countNavFiles', totalFiles); } // 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(`