diff --git a/static/index.html b/static/index.html index 5b1f895..0cdf290 100644 --- a/static/index.html +++ b/static/index.html @@ -33,12 +33,6 @@ .filters { display: flex; gap: 10px; flex-wrap: wrap; } .filter-label { display: flex; align-items: center; gap: 5px; cursor: pointer; font-size: 12px; } .filter-label input { cursor: pointer; width: 18px; height: 18px; accent-color: #58a6ff; } - .data-ts { font-size: 11px; color: #8b949e; white-space: nowrap; margin-right: 8px; } - .nav-back { padding: 6px 16px; background: #161b22; color: #8b949e; font-size: 12px; border-bottom: 1px solid #30363d; } - .nav-back a { color: #58a6ff; text-decoration: none; } - .nav-back a:hover { text-decoration: underline; } - .dep-item { cursor: pointer; border-radius: 4px; transition: background .15s; } - .dep-item:hover { background: #21262d; } .btn-refresh { padding: 5px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; cursor: pointer; font-size: 12px; } .btn-refresh:hover { background: #30363d; } .btn-graph { padding: 5px 12px; background: #1f6feb; border: 1px solid #3b82f6; border-radius: 6px; color: #fff; cursor: pointer; font-size: 12px; } @@ -188,7 +182,6 @@
- @@ -253,6 +246,9 @@ var graphAllKey = ""; var graphWarmupScheduled = false; var graphDrawing = false; var graphFetchInFlight = false; +var rawDetail = {}; +var navBackUid = null; +var renderTimer = null; function escHtml(v){ return String(v == null ? "" : v) @@ -663,7 +659,18 @@ function depUid(dep){ function visibleUidSet(){ var s = new Set(); - allInstances.forEach(function(i){ if(i && i.instanceUid) s.add(i.instanceUid); }); + var checked = Array.from(document.querySelectorAll("#filters input[value]:checked")).map(function(i){ return i.value; }); + var auxEl = document.getElementById("chk-auxiliary"); + var showAux = auxEl ? auxEl.checked : true; + allInstances.forEach(function(i){ + if(!i || !i.instanceUid) return; + if(checked.length && checked.indexOf(i.explainedStatus) < 0) return; + if(!showAux){ + var det = rawDetail[i.instanceUid]; + if(det && det.isAuxiliary) return; + } + s.add(i.instanceUid); + }); return s; } @@ -776,7 +783,7 @@ function doLogout(){ } function getChecked(){ - return Array.from(document.querySelectorAll("#filters input:checked")).map(function(i){ return i.value; }); + return Array.from(document.querySelectorAll("#filters input[value]:checked")).map(function(i){ return i.value; }); } function sClass(s){ @@ -864,19 +871,26 @@ function toggle(uid, row){ row.classList.add("expanded"); dr.classList.add("open"); if(!cache[uid]){ - dc.innerHTML = "
Загрузка...
"; - fetch(API + "/instances/" + uid, { headers: apiHeaders() }) - .then(function(r){ - if(r.status === 401){ doLogout(); return null; } - return r.json(); - }) - .then(function(d){ - if(!d) return; - cache[uid] = buildDetail(d.instance); - dc.innerHTML = cache[uid]; - bindTabs(dc); - }) - .catch(function(e){ cache[uid] = "
" + e.message + "
"; dc.innerHTML = cache[uid]; }); + if(rawDetail[uid]){ + cache[uid] = buildDetail(rawDetail[uid]); + dc.innerHTML = cache[uid]; + bindTabs(dc); + } else { + dc.innerHTML = "
Загрузка...
"; + fetch(API + "/instances/" + uid, { headers: apiHeaders() }) + .then(function(r){ + if(r.status === 401){ doLogout(); return null; } + return r.json(); + }) + .then(function(d){ + if(!d) return; + rawDetail[uid] = d.instance; + cache[uid] = buildDetail(d.instance); + dc.innerHTML = cache[uid]; + bindTabs(dc); + }) + .catch(function(e){ cache[uid] = "
" + e.message + "
"; dc.innerHTML = cache[uid]; }); + } } else { dc.innerHTML = cache[uid]; bindTabs(dc); @@ -946,10 +960,11 @@ function buildDetail(inst){ var depsHtml = ""; if(deps.length){ deps.forEach(function(d){ - depsHtml += "
" + (d.displayName||d.uid) + - "
" + (d.svc||"") + - (d.param ? " · через " + d.param + "" : "") + - (d.descr ? " · " + d.descr : "") + "
"; + var did = depUid(d); + depsHtml += "
► " + escHtml(d.displayName||d.uid) + + "
" + escHtml(d.svc||"") + + (d.param ? " · через " + escHtml(d.param) + "" : "") + + (d.descr ? " · " + escHtml(d.descr) : "") + "
"; }); } else { depsHtml = "
Нет зависимостей
"; @@ -957,9 +972,10 @@ function buildDetail(inst){ var depOnHtml = ""; if(depOn.length){ depOn.forEach(function(d){ - depOnHtml += "
" + (d.displayName||d.uid) + - "
" + (d.svc||"") + - (d.param ? " · через " + d.param + "" : "") + "
"; + var did = depUid(d); + depOnHtml += "
► " + escHtml(d.displayName||d.uid) + + "
" + escHtml(d.svc||"") + + (d.param ? " · через " + escHtml(d.param) + "" : "") + "
"; }); } else { depOnHtml = "
Никто не зависит
"; @@ -996,30 +1012,137 @@ function renderP(obj, pfx){ return r || "
Пусто
"; } -function loadInstances(){ - var checked = getChecked(); - document.getElementById("tbody").innerHTML = "Загрузка..."; - var p = new URLSearchParams(); - (checked.length ? checked : ["__none__"]).forEach(function(s){ p.append("status", s); }); - fetch(API + "/instances?" + p, { headers: apiHeaders() }) +function filteredInstances(){ + var checked = Array.from(document.querySelectorAll("#filters input[value]:checked")).map(function(i){ return i.value; }); + var auxEl = document.getElementById("chk-auxiliary"); + var showAux = auxEl ? auxEl.checked : true; + return allInstances.filter(function(inst){ + if(checked.length && checked.indexOf(inst.explainedStatus) < 0) return false; + if(!showAux){ + var det = rawDetail[inst.instanceUid]; + if(det && det.isAuxiliary) return false; + } + return true; + }); +} + +function updateTimestamp(){ + var ts = document.getElementById("data-ts"); + if(ts) ts.textContent = "Актуально на " + new Date().toLocaleTimeString("ru-RU",{hour:"2-digit",minute:"2-digit",second:"2-digit"}); +} + +function scheduleRender(){ + if(renderTimer) clearTimeout(renderTimer); + renderTimer = setTimeout(function(){ + renderTimer = null; + renderStats(allInstances); + render(filteredInstances()); + }, 150); +} + +function loadAllDetails(){ + allInstances = []; + cache = {}; + rawDetail = {}; + graphAllData = null; + graphRunningData = null; + graphFullData = null; + graphAllKey = ""; + expandedUid = null; + navBackUid = null; + var nb = document.getElementById("nav-back"); + if(nb) nb.style.display = "none"; + var ts = document.getElementById("data-ts"); + if(ts) ts.textContent = ""; + document.getElementById("tbody").innerHTML = "Загрузка данных..."; + + fetch(API + "/instances/stream", { headers: apiHeaders() }) .then(function(r){ if(r.status === 401){ doLogout(); return null; } if(!r.ok) throw new Error("HTTP " + r.status); - return r.json(); + return r; }) - .then(function(data){ - if(!data) return; - allInstances = data.instances || []; - renderStats(allInstances); - render(allInstances); - var ts = document.getElementById("data-ts"); - if(ts) ts.textContent = "Данные на: " + new Date().toLocaleTimeString("ru-RU", {hour:"2-digit",minute:"2-digit",second:"2-digit"}); + .then(function(r){ + if(!r) return; + var reader = r.body.getReader(); + var decoder = new TextDecoder(); + var buffer = ""; + + function processLine(line){ + var obj; + try { obj = JSON.parse(line); } catch(e){ return; } + if(obj._done){ + updateTimestamp(); + renderStats(allInstances); + render(filteredInstances()); + return; + } + var uid = obj.instanceUid; + if(!uid) return; + var detail = obj._detail; + if(detail !== undefined) delete obj._detail; + if(detail) rawDetail[uid] = detail; + allInstances.push(obj); + scheduleRender(); + } + + function pump(){ + return reader.read().then(function(result){ + if(result.done){ + if(buffer.trim()) processLine(buffer.trim()); + updateTimestamp(); + renderStats(allInstances); + render(filteredInstances()); + return; + } + buffer += decoder.decode(result.value, {stream: true}); + var lines = buffer.split("\n"); + buffer = lines.pop(); + lines.forEach(function(line){ if(line.trim()) processLine(line.trim()); }); + return pump(); + }); + } + return pump(); }) .catch(function(e){ - document.getElementById("tbody").innerHTML = "" + e.message + ""; + document.getElementById("tbody").innerHTML = "" + escHtml(e.message) + ""; }); } +function navigateToDep(uid){ + var rows = document.querySelectorAll("tr.data-row[data-uid=\"" + uid + "\"]"); + if(!rows.length){ + var nb = document.getElementById("nav-back"); + if(nb){ nb.innerHTML = "⚠ Инстанс скрыт текущими фильтрами"; nb.style.display = "block"; } + return; + } + navBackUid = expandedUid; + var nb = document.getElementById("nav-back"); + if(nb && navBackUid){ + var backName = instanceName(navBackUid); + nb.innerHTML = "← Назад: " + escHtml(backName) + ""; + nb.style.display = "block"; + } + var row = rows[0]; + toggle(uid, row); + row.scrollIntoView({behavior:"smooth", block:"center"}); +} + +function navBack(){ + var uid = navBackUid; + navBackUid = null; + var nb = document.getElementById("nav-back"); + if(nb) nb.style.display = "none"; + if(!uid) return; + var rows = document.querySelectorAll("tr.data-row[data-uid=\"" + uid + "\"]"); + if(!rows.length) return; + var row = rows[0]; + toggle(uid, row); + row.scrollIntoView({behavior:"smooth", block:"center"}); +} + +function loadInstances(){ loadAllDetails(); } + function applyTheme(theme){ var isLight = theme === "light"; document.body.classList.toggle("light", isLight); @@ -1052,7 +1175,7 @@ function doLogin(){ if(!data) return; localStorage.setItem("deck_token", token); localStorage.setItem("deck_env", env); - initApp(data.instances || []); + initApp(); }) .catch(function(e){ showLoginErr("Ошибка соединения: " + e.message); @@ -1061,7 +1184,7 @@ function doLogin(){ }); } -function initApp(preloaded){ +function initApp(){ document.getElementById("login-screen").classList.add("hidden"); document.getElementById("app").style.display = "flex"; var email = decodeEmail(getToken()); @@ -1070,45 +1193,28 @@ function initApp(preloaded){ th.addEventListener("click", function(){ if(sortCol === th.dataset.col) sortDir *= -1; else { sortCol = th.dataset.col; sortDir = 1; } - render(allInstances); + render(filteredInstances()); }); }); - document.querySelectorAll("#filters input").forEach(function(cb){ - cb.addEventListener("change", loadInstances); + document.querySelectorAll("#filters input[value]").forEach(function(cb){ + cb.addEventListener("change", function(){ cache = {}; render(filteredInstances()); }); }); - if(preloaded && preloaded.length > 0){ - allInstances = preloaded; - renderStats(allInstances); - render(allInstances); - } else { - loadInstances(); - } + document.getElementById("chk-auxiliary").addEventListener("change", function(){ + cache = {}; + render(filteredInstances()); + if(graphRunningData){ graphDrawing = false; drawGraph("running-graph", graphRunningData, ""); } + }); + loadAllDetails(); scheduleGraphWarmup(); } document.getElementById("btn-login").addEventListener("click", doLogin); -document.getElementById("btn-refresh").addEventListener("click", function(){ - // Reset all caches so data is fetched fresh - allInstances = []; - cache = {}; - graphAllData = null; - graphAllKey = ""; - graphRunningData = null; - graphFullData = null; - graphWarmupScheduled = false; - loadInstances(); -}); +document.getElementById("btn-refresh").addEventListener("click", loadAllDetails); document.getElementById("btn-running-graph").addEventListener("click", openRunningGraph); document.getElementById("btn-theme").addEventListener("click", toggleTheme); document.getElementById("btn-close-graph").addEventListener("click", closeRunningGraph); document.getElementById("btn-exit").addEventListener("click", doLogout); -document.getElementById("chk-auxiliary").addEventListener("change", function(){ - // Redraw current graph with updated auxiliary filter - if(graphRunningData){ - graphDrawing = false; - drawGraph("running-graph", graphRunningData, ""); - } -}); + document.getElementById("graph-modal").addEventListener("click", function(e){ if(e.target && e.target.id === "graph-modal") closeRunningGraph(); }); @@ -1118,7 +1224,7 @@ applyTheme(localStorage.getItem("dashboard_theme") || "dark"); var envSelect = document.getElementById("deck-env"); if(envSelect) envSelect.value = getDeckEnv(); -if(getToken()) initApp([]); +if(getToken()) initApp(); })();