diff --git a/webui/html/assets/css/app.css b/webui/html/assets/css/app.css index 5bb5718..5daae73 100644 --- a/webui/html/assets/css/app.css +++ b/webui/html/assets/css/app.css @@ -56,6 +56,18 @@ --badge-yellow-text: #111111; --table-zebra: rgba(96,165,250,.03); --sticky-head-bg: rgba(17,26,46,.96); + --state-info-bg: rgba(96,165,250,.08); + --state-info-border: rgba(96,165,250,.35); + --state-empty-bg: rgba(251,191,36,.08); + --state-empty-border: rgba(251,191,36,.35); + --state-error-bg: rgba(251,113,133,.08); + --state-error-border: rgba(251,113,133,.35); + --map-tooltip-bg: rgba(11,18,32,.95); + --map-tooltip-border: rgba(36,52,95,.9); + --fs-body: 14px; + --fs-small: 12px; + --fs-title: 16px; + --fs-kpi: 22px; --radius: 14px; --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; --sans: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji"; @@ -115,11 +127,25 @@ --badge-yellow-text: #fffbeb; --table-zebra: rgba(15,23,42,.03); --sticky-head-bg: rgba(255,255,255,.97); + --state-info-bg: rgba(37,99,235,.08); + --state-info-border: rgba(37,99,235,.3); + --state-empty-bg: rgba(180,83,9,.09); + --state-empty-border: rgba(180,83,9,.35); + --state-error-bg: rgba(225,29,72,.08); + --state-error-border: rgba(225,29,72,.35); + --map-tooltip-bg: rgba(255,255,255,.98); + --map-tooltip-border: rgba(148,163,184,.45); + --fs-body: 14px; + --fs-small: 12px; + --fs-title: 16px; + --fs-kpi: 22px; } *{box-sizing:border-box} body{ margin:0; font-family: var(--sans); + font-size: var(--fs-body); + line-height: 1.45; background: radial-gradient(1200px 600px at 20% 0%, var(--bg-grad-start) 0%, var(--bg) 55%); color: var(--text); } @@ -166,6 +192,10 @@ header{ cursor:pointer; user-select:none; font-size:14px; + transition: transform .12s ease, border-color .16s ease, background .16s ease; +} +.tab:hover{ + transform: translateY(-1px); } .tab.active{ background: var(--tab-active-bg); @@ -200,6 +230,7 @@ header{ .cardTitle{ font-weight:700; display:flex; gap:10px; align-items:center; + font-size: var(--fs-title); } .cardBody{padding:14px} .dashboardKpiGrid{ @@ -225,6 +256,7 @@ header{ font-size:13px; } .btn:hover{background: var(--btn2)} +.btn:active{transform: translateY(1px)} .btn.small{padding:7px 9px; border-radius: 10px} .btn.ghost{background: transparent} .btn.ok{border-color: rgba(45,212,191,.6)} @@ -280,7 +312,7 @@ tr:hover td{background: var(--hover-bg)} padding: 4px 8px; border: 1px solid var(--card-border); border-radius: 999px; - font-size: 12px; + font-size: var(--fs-small); color: var(--muted); white-space: nowrap; /* Tip: dit voorkomt dat je badge tekst afbreekt */ } @@ -338,18 +370,18 @@ pre.code{ } .statValue{ font-weight:800; - font-size: 20px; + font-size: var(--fs-kpi); line-height: 1.1; letter-spacing: .2px; } .statLabel{ color: var(--muted); - font-size: 12px; + font-size: var(--fs-small); margin-top: 4px; } .statHint{ color: var(--muted); - font-size: 12px; + font-size: var(--fs-small); margin-top: 6px; } @@ -543,11 +575,25 @@ pre{ .navLabel { white-space: nowrap; } +.navCount{ + margin-left: auto; + min-width: 26px; + text-align: center; + border: 1px solid var(--card-border); + border-radius: 999px; + font-size: 11px; + color: var(--muted); + padding: 2px 7px; + background: var(--input-bg); +} /* Collapsed: alleen icon zichtbaar */ .sidebar.collapsed .navLabel { display: none; } +.sidebar.collapsed .navCount { + display: none; +} .sidebar.collapsed .tab { justify-content: center; padding: 10px 10px; @@ -666,6 +712,35 @@ pre{ padding:7px 9px; font-size: 12px; } +.stateBox{ + border-radius: 12px; + padding: 10px 12px; + border: 1px solid var(--state-info-border); + background: var(--state-info-bg); +} +.stateBox.empty{ + border-color: var(--state-empty-border); + background: var(--state-empty-bg); +} +.stateBox.error{ + border-color: var(--state-error-border); + background: var(--state-error-bg); +} +.stateTitle{ + font-weight: 700; + margin-bottom: 4px; +} +.stateText{ + color: var(--muted); + font-size: var(--fs-small); +} +.viewAnim{ + animation: viewFade .18s ease; +} +@keyframes viewFade{ + from{opacity:.0; transform: translateY(3px)} + to{opacity:1; transform: translateY(0)} +} .data-table { width: 100%; border-collapse: collapse; @@ -734,6 +809,23 @@ pre{ border-radius: 14px; min-height: 420px; overflow: hidden; + position: relative; +} +.mapTooltip{ + position: absolute; + left: 0; + top: 0; + pointer-events: none; + max-width: 260px; + background: var(--map-tooltip-bg); + color: var(--text); + border: 1px solid var(--map-tooltip-border); + border-radius: 10px; + padding: 7px 9px; + font-size: 12px; + line-height: 1.35; + box-shadow: var(--shadow); + z-index: 3; } .mapLegend{ diff --git a/webui/html/assets/js/tabs/containers.js b/webui/html/assets/js/tabs/containers.js index 81f7852..fa6c999 100644 --- a/webui/html/assets/js/tabs/containers.js +++ b/webui/html/assets/js/tabs/containers.js @@ -219,6 +219,9 @@ async function fetchContainers() { const list = Array.isArray(containers) ? containers : (containers?.containers || []); document.getElementById('countContainers').textContent = list.length; + if (typeof window.updateNavCount === 'function') { + window.updateNavCount('countNavContainers', list.length); + } const podsList = Array.isArray(pods) ? pods : []; const podStatus = {}; diff --git a/webui/html/assets/js/tabs/files.js b/webui/html/assets/js/tabs/files.js index 8e8cec2..95298d0 100644 --- a/webui/html/assets/js/tabs/files.js +++ b/webui/html/assets/js/tabs/files.js @@ -159,7 +159,19 @@ async function filesRefresh() { const treeEl = document.getElementById('filesTree'); treeEl.textContent = 'Laden...'; - const data = await api('/files/tree', 'GET'); + 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 => { @@ -168,10 +180,24 @@ async function filesRefresh() { }); if (!scoped.length) { - treeEl.textContent = 'Geen bestanden gevonden onder systemd.'; + 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) { diff --git a/webui/html/assets/js/tabs/images.js b/webui/html/assets/js/tabs/images.js index 69e633c..36a2b1b 100644 --- a/webui/html/assets/js/tabs/images.js +++ b/webui/html/assets/js/tabs/images.js @@ -2,18 +2,44 @@ let imagesData = []; let imagesSort = { field: null, dir: null }; async function loadImages() { - const res = await fetch("/api/images"); - const images = await res.json(); + const tbody = document.getElementById("images-tbody"); + try { + const res = await fetch("/api/images"); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const images = await res.json(); - imagesData = images; - updateSortIndicators(); - applyImageSorting(); + imagesData = Array.isArray(images) ? images : []; + if (typeof window.updateNavCount === "function") { + window.updateNavCount("countNavImages", imagesData.length); + } + updateSortIndicators(); + applyImageSorting(); + } catch (e) { + imagesData = []; + if (typeof window.updateNavCount === "function") { + window.updateNavCount("countNavImages", 0); + } + if (tbody) { + const box = (typeof window.renderStateBox === "function") + ? window.renderStateBox("error", "Images laden mislukt", e.message || String(e)) + : "Images laden mislukt."; + tbody.innerHTML = `