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();
})();