v9.44: remove all graph code (backend endpoint, frontend SVG/panzoom/CSS, ~536 lines dead code)

This commit is contained in:
Naeel
2026-04-17 08:56:53 +03:00
parent b778f5554c
commit f6d15eb7e8
3 changed files with 3 additions and 536 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ spec:
spec:
containers:
- name: cloud-dashboard
image: naeel/cloud-dashboard:v9.43
image: naeel/cloud-dashboard:v9.44
imagePullPolicy: Always
ports:
- containerPort: 8080
-175
View File
@@ -3,7 +3,6 @@ from fastapi.responses import StreamingResponse
from fastapi.staticfiles import StaticFiles
import httpx
from typing import Annotated
import asyncio
import json
import time
from datetime import datetime, timezone
@@ -16,8 +15,6 @@ DECK_APIS = {
"test": "https://deck-api-test.ngcloud.ru/api/v1",
}
DEFAULT_DECK_ENV = "test"
CACHE_TTL_SECONDS = 180
GRAPH_CACHE: dict[str, tuple[float, dict]] = {}
# Centralized backend policy for table-related derived fields.
DELETE_POLICY = {
@@ -55,38 +52,6 @@ def _as_text(value) -> str:
return ""
def extract_platform_name(inst: dict | None) -> str:
if not isinstance(inst, dict):
return ""
sources = [
inst.get("parameters"),
inst.get("params"),
inst.get("instanceParameters"),
inst.get("inputParameters"),
]
needles = ("platform", "kuber", "k8s", "cluster", "shturval", "штурвал")
for src in sources:
if isinstance(src, dict):
for key, value in src.items():
key_s = str(key).strip().lower()
if any(n in key_s for n in needles):
txt = _as_text(value)
if txt:
return txt
elif isinstance(src, list):
for item in src:
if not isinstance(item, dict):
continue
key_s = str(item.get("name") or item.get("key") or item.get("code") or "").strip().lower()
if any(n in key_s for n in needles):
txt = _as_text(item.get("value") or item.get("displayValue") or item.get("text"))
if txt:
return txt
return ""
def _parse_ts(value) -> float | None:
if not value:
return None
@@ -204,146 +169,6 @@ async def list_instances(
return {"instances": all_results, "total": len(all_results)}
@app.get("/api/graph")
async def get_graph(
request: Request,
x_deck_token: Annotated[str | None, Header()] = None,
x_deck_env: Annotated[str | None, Header()] = None,
):
if not x_deck_token:
raise HTTPException(status_code=401, detail="Token required")
env, deck_api = resolve_deck_api(x_deck_env)
status_list = request.query_params.getlist("status")
root_uid = request.query_params.get("root_uid")
depth_raw = request.query_params.get("depth", "2")
try:
depth = max(1, min(5, int(depth_raw)))
except ValueError:
depth = 2
status_key = ",".join(sorted(status_list)) if status_list else "__all__"
cache_key = f"{x_deck_token[:24]}::{env}::{status_key}"
now = time.time()
cached = GRAPH_CACHE.get(cache_key)
if cached and (now - cached[0] < CACHE_TTL_SECONDS):
base_graph = cached[1]
else:
all_instances = await fetch_all_instances(x_deck_token, deck_api)
if status_list:
all_instances = [i for i in all_instances if i.get("explainedStatus") in status_list]
uid_to_inst = {
i.get("instanceUid"): i
for i in all_instances
if i.get("instanceUid")
}
allowed_uids = set(uid_to_inst.keys())
detail_map: dict[str, dict] = {}
async with httpx.AsyncClient(timeout=25) as shared_client:
for uid in allowed_uids:
try:
r = await shared_client.get(
f"{deck_api}/index.cfm/instances/{uid}",
headers=auth_headers(x_deck_token),
)
if r.status_code == 401:
raise HTTPException(status_code=401, detail="Invalid token")
if r.status_code >= 400:
continue
inst = r.json().get("instance")
if inst:
detail_map[uid] = inst
except httpx.TimeoutException:
continue
# Categorise singletons by service name
_PLATFORM_PATTERNS = ("виртуальный датацентр", "vdc", "virtual data center", "cloud director", "kubernetes", "k8s")
_SINGLETON_PATTERNS = ("object storage", "s3", "edge", "шлюз периметра", "dns", "container registry", "реестр контейнеров")
def _is_platform_svc(svc_name: str) -> bool:
s = svc_name.strip().lower()
return any(p in s for p in _PLATFORM_PATTERNS)
def _is_singleton_svc(svc_name: str) -> bool:
s = svc_name.strip().lower()
return any(p in s for p in _SINGLETON_PATTERNS)
# Build realm→platform_uid map: instances whose resourceRealmCnt > 0 ARE platforms
realm_to_platform: dict[str, str] = {}
for uid_d, inst_d in detail_map.items():
cnt = inst_d.get("resourceRealmCnt") or 0
realm_name = inst_d.get("resourceRealm") or ""
if cnt and realm_name:
realm_to_platform[realm_name] = uid_d
nodes = []
for uid_i in allowed_uids:
i = uid_to_inst.get(uid_i, {})
detail = detail_map.get(uid_i, {})
svc = i.get("svc") or ""
realm_cnt = detail.get("resourceRealmCnt") or 0
is_aux = bool(detail.get("isAuxiliary"))
# Determine lane by service name
if _is_platform_svc(svc):
lane = "platform"
elif _is_singleton_svc(svc):
lane = "singleton"
else:
lane = "regular"
nodes.append(
{
"id": uid_i,
"label": i.get("displayName") or uid_i,
"status": i.get("explainedStatus") or "unknown",
"svc": svc,
"realm": i.get("resourceRealm") or "",
"lane": lane,
"isAuxiliary": is_aux,
"created": i.get("instanceConfigDtCreated") or i.get("dtCreated") or "",
}
)
# Debug: log lane distribution
import sys
_lane_counts = {}
for _n in nodes:
_l = _n.get("lane", "unknown")
_lane_counts[_l] = _lane_counts.get(_l, 0) + 1
print(f"[GRAPH] Lanes: {_lane_counts}, nodes: {len(nodes)}", flush=True)
for _n in nodes:
if _n["lane"] in ("platform", "singleton"):
print(f" {_n['lane'].upper()}: {_n['label']} | svc={_n['svc']}", flush=True)
base_graph = {"nodes": nodes, "edges": []}
GRAPH_CACHE[cache_key] = (now, base_graph)
# Return full graph fast
if not root_uid:
return {
"nodes": base_graph["nodes"],
"edges": [],
"total_nodes": len(base_graph["nodes"]),
"total_edges": 0,
"cached": bool(cached),
}
node_ids = {n["id"] for n in base_graph["nodes"]}
if root_uid not in node_ids:
return {"nodes": [], "edges": [], "total_nodes": 0, "total_edges": 0, "cached": bool(cached)}
sub_nodes = [n for n in base_graph["nodes"] if n["id"] == root_uid]
return {
"nodes": sub_nodes,
"edges": [],
"total_nodes": len(sub_nodes),
"total_edges": 0,
"cached": bool(cached),
}
@app.get("/api/instances/stream")
async def stream_instances(
request: Request,
+2 -360
View File
@@ -36,12 +36,8 @@
.filter-label input { cursor: pointer; width: 18px; height: 18px; accent-color: #58a6ff; }
.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; }
.btn-graph:hover { background: #2563eb; }
.btn-theme { padding: 5px 10px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; cursor: pointer; font-size: 14px; line-height: 1; }
.btn-theme:hover { background: #30363d; }
.btn-graph { padding: 5px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #e1e4e8; cursor: pointer; font-size: 12px; }
.btn-graph:hover { background: #30363d; }
.header-user { display: flex; align-items: center; gap: 10px; }
.user-email { font-size: 12px; color: #8b949e; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.btn-exit { padding: 5px 12px; background: #21262d; border: 1px solid #30363d; border-radius: 6px; color: #f85149; cursor: pointer; font-size: 12px; }
@@ -100,20 +96,6 @@
body.light .panel-history { background: #dcfce7; border-top: 2px solid #4ade80; }
body.light .panel-deps { background: #fef3c7; border-top: 2px solid #f59e0b; }
.panel.active { display: block; }
.inst-graph { height: 420px; border: 1px solid #30363d; border-radius: 8px; background: #0b0f14; }
.graph-hint { color: #8b949e; font-size: 12px; margin-bottom: 8px; }
.graph-loading { display: flex; align-items: center; justify-content: center; height: 100%; color: #9fb3c8; font-size: 13px; }
.graph-loading::before { content: ""; width: 14px; height: 14px; border: 2px solid #3b82f6; border-top-color: transparent; border-radius: 50%; display: inline-block; margin-right: 8px; animation: spin .9s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
.graph-modal { position: fixed; inset: 0; background: rgba(2, 8, 20, .75); display: none; align-items: center; justify-content: center; z-index: 1200; }
.graph-modal.open { display: flex; }
.graph-modal-box { width: min(98vw, 1700px); height: min(95vh, 1100px); background: #0d1117; border: 1px solid #30363d; border-radius: 12px; display: flex; flex-direction: column; }
.graph-modal-head { padding: 10px 14px; border-bottom: 1px solid #30363d; display: flex; align-items: center; gap: 10px; }
.graph-modal-title { font-size: 14px; font-weight: 600; }
.graph-modal-close { margin-left: auto; padding: 4px 10px; background: #21262d; border: 1px solid #30363d; color: #e1e4e8; border-radius: 6px; cursor: pointer; }
.graph-modal-body { flex: 1; min-height: 0; }
.running-graph { width: 100%; height: 100%; }
.graph-inline-note { color: #8b949e; font-size: 12px; margin-top: 8px; }
.params-grid { display: grid; grid-template-columns: max-content max-content; gap: 14px 68px; max-width: none; margin: 0; justify-content: start; align-content: start; }
.params-section { min-width: 0; }
.params-section h4 { font-size: 11px; font-weight: 600; color: #8b949e; text-transform: uppercase; margin-bottom: 8px; }
@@ -169,8 +151,8 @@
body.light .header-logo .logo-text { color: #0f172a; }
body.light .header-logo .logo-sub, body.light .user-email { color: #475569; }
body.light .data-ts { color: #64748b; }
body.light .btn-refresh, body.light .btn-theme, body.light .btn-exit, body.light .btn-graph { background: #f8fafc; border-color: #cbd5e1; color: #0f172a; }
body.light .btn-refresh:hover, body.light .btn-theme:hover, body.light .btn-exit:hover, body.light .btn-graph:hover { background: #e2e8f0; }
body.light .btn-refresh, body.light .btn-theme, body.light .btn-exit { background: #f8fafc; border-color: #cbd5e1; color: #0f172a; }
body.light .btn-refresh:hover, body.light .btn-theme:hover, body.light .btn-exit:hover { background: #e2e8f0; }
body.light .main { background: #f3f6fb; }
body.light .stat-card { background: #ffffff; border-color: #cbd5e1; }
body.light .stat-card .lbl { color: #64748b; }
@@ -199,10 +181,6 @@
body.light .tabs { border-bottom-color: #b6c6e8; background: #dfe9fa; }
body.light .tab { color: #475569; }
body.light .tab.active { color: #1d4ed8; border-bottom-color: #1d4ed8; }
body.light .graph-modal { background: rgba(15, 23, 42, .35); }
body.light .graph-modal-box { background: #ffffff; border-color: #cbd5e1; }
body.light .graph-modal-head { border-bottom-color: #cbd5e1; }
body.light .graph-modal-close { background: #f8fafc; border-color: #cbd5e1; color: #0f172a; }
body.light .p-item { border-bottom-color: #e2e8f0; color: #0f172a; }
body.light .p-item:nth-child(odd) { background: rgba(148, 163, 184, .12); }
body.light .p-item:nth-child(even) { background: rgba(148, 163, 184, .18); }
@@ -304,7 +282,6 @@
</div>
</div>
<div id="js-err" style="display:none;position:fixed;bottom:0;left:0;right:0;background:#f85149;color:#fff;padding:10px;font-family:monospace;font-size:12px;z-index:9999"></div>
<script src="https://cdn.jsdelivr.net/npm/panzoom@9.4.3/dist/panzoom.min.js"></script>
<script>
window.onerror=function(m,s,l){var e=document.getElementById("js-err");if(e){e.style.display="block";e.textContent="JS ERROR line "+l+": "+m;}};
</script>
@@ -318,11 +295,6 @@ var sortCol = "displayName";
var sortDir = 1;
var expandedUid = null;
var cache = {};
var graphAllData = null;
var graphRunningData = null;
var graphFullData = null;
var graphAllKey = "";
var graphFetchInFlight = false;
var rawDetail = {};
var navBackUid = null;
var renderTimer = null;
@@ -435,332 +407,6 @@ function visibleUidSet(){
}
// ─── GRAPH: SVG + panzoom ─────────────────────────────────────────────────
// Status color for node border
function nodeStatusColor(status){
var m = {
"running": "#238636", "suspended": "#e3b341", "deleting": "#f85149",
"deleted": "#6e7681", "not created": "#8b949e", "pending": "#a371f7",
"creating": "#1f6feb"
};
return m[status] || "#8b949e";
}
// Service → background color
function nodeBgColor(svc){
var s = (svc || "").toLowerCase();
if(/postgres|sql/.test(s)) return "#0c1e36";
if(/kubernetes|k8s|шт[уy]рвал/.test(s)) return "#0f1e0f";
if(/s3|object storage|бакет/.test(s)) return "#1a1200";
if(/edge|шлюз/.test(s)) return "#200c1a";
if(/vm|виртуальн/.test(s)) return "#1a1014";
if(/redis/.test(s)) return "#200a0a";
if(/rabbit/.test(s)) return "#0a1a10";
if(/dns/.test(s)) return "#0a1220";
if(/cloud director|vdc|датацентр/.test(s)) return "#001430";
return "#161b22";
}
// Break text into lines fitting maxWidth (chars-based estimate)
function wrapText(text, maxChars){
var words = text.split(/(?=[_\-])/); // split on _ and - keeping delimiter
var lines = [];
var cur = "";
for(var i = 0; i < words.length; i++){
var w = words[i];
if((cur + w).length > maxChars && cur.length > 0){
lines.push(cur);
cur = w;
} else {
cur += w;
}
}
if(cur) lines.push(cur);
return lines.length ? lines : [text];
}
// Fetch graph data from backend
function fetchGraph(params){
if(graphFetchInFlight) return Promise.resolve(null);
graphFetchInFlight = true;
var p = new URLSearchParams();
Object.keys(params || {}).forEach(function(k){
var v = params[k];
if(Array.isArray(v)) v.forEach(function(x){ p.append(k, x); });
else if(v !== undefined && v !== null && v !== "") p.append(k, String(v));
});
return fetch(API + "/graph?" + p.toString(), { headers: apiHeaders() })
.then(function(r){
graphFetchInFlight = false;
if(r.status === 401){ doLogout(); return null; }
if(!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.catch(function(e){
graphFetchInFlight = false;
throw e;
});
}
function ensureAllGraphLoaded(){
var statuses = getChecked().slice().sort();
var key = statuses.join(",");
if(key === graphAllKey && graphAllData) return Promise.resolve(graphAllData);
if(statuses.length === 0){
graphAllKey = key;
graphAllData = {nodes:[], edges:[]};
return Promise.resolve(graphAllData);
}
return fetchGraph({ status: statuses }).then(function(data){
graphAllKey = key;
graphAllData = data || {nodes:[], edges:[]};
return graphAllData;
});
}
function ensureFullGraphLoaded(){
if(graphFullData) return Promise.resolve(graphFullData);
return fetchGraph({}).then(function(data){
graphFullData = data || {nodes:[], edges:[]};
return graphFullData;
});
}
// Build subgraph from rawDetail dependencies (BFS, no edges needed)
function buildDepSubgraphFromDetail(rootUid){
var seen = new Set([rootUid]);
var frontier = new Set([rootUid]);
for(var i = 0; i < 3; i++){
var nxt = new Set();
frontier.forEach(function(u){
var det = rawDetail[u];
if(!det) return;
(det.dependencies || []).forEach(function(dep){
var duid = depUid(dep);
if(duid && !seen.has(duid)) nxt.add(duid);
});
(det.dependentInstances || []).forEach(function(dep){
var duid = depUid(dep);
if(duid && !seen.has(duid)) nxt.add(duid);
});
});
if(nxt.size === 0) break;
nxt.forEach(function(x){ seen.add(x); });
frontier = nxt;
}
var allNodes = (graphFullData && graphFullData.nodes) || [];
var filtered = allNodes.filter(function(n){ return seen.has(n.id); });
if(!filtered.some(function(n){ return n.id === rootUid; })){
for(var j = 0; j < allInstances.length; j++){
if(allInstances[j].instanceUid === rootUid){
var inst = allInstances[j];
filtered.push({
id: rootUid, label: inst.displayName || rootUid,
status: inst.explainedStatus || "unknown", svc: inst.svc || "",
lane: "regular", created: inst.instanceConfigDtCreated || inst.dtCreated || ""
});
break;
}
}
}
return { nodes: filtered, edges: [] };
}
// ─── MAIN DRAW FUNCTION ────────────────────────────────────────────────────
function drawGraph(containerId, data, focusUid){
var el = document.getElementById(containerId);
if(!el) return;
el.innerHTML = "";
var nodes = data.nodes || [];
if(nodes.length === 0){
el.innerHTML = '<div class="empty-msg" style="padding:20px">Нет данных</div>';
return;
}
var NODE_W = 200;
var NODE_H = 80;
var GAP_X = 30;
var GAP_Y = 50;
var MAX_COLS = 5;
var PAD = 40;
// Sort: platforms first, singletons second, regular last
var sorted = nodes.slice().sort(function(a, b){
var rank = function(n){ return n.lane === "platform" ? 0 : n.lane === "singleton" ? 1 : 2; };
return rank(a) - rank(b);
});
// Assign grid positions
var cols = Math.min(sorted.length, MAX_COLS);
sorted.forEach(function(n, idx){
n._col = idx % cols;
n._row = Math.floor(idx / cols);
});
var rows = Math.ceil(sorted.length / cols);
var svgW = PAD * 2 + cols * NODE_W + (cols - 1) * GAP_X;
var svgH = PAD * 2 + rows * NODE_H + (rows - 1) * GAP_Y;
// ── Create container with overflow scroll ──
var wrapper = document.createElement("div");
wrapper.style.cssText = "width:100%;height:100%;overflow:hidden;position:relative;background:#0d1117;";
var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", svgW);
svg.setAttribute("height", svgH);
svg.setAttribute("viewBox", "0 0 " + svgW + " " + svgH);
svg.style.cssText = "display:block;cursor:grab;user-select:none;";
var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
sorted.forEach(function(n){
var x = PAD + n._col * (NODE_W + GAP_X);
var y = PAD + n._row * (NODE_H + GAP_Y);
var cx = x + NODE_W / 2;
var cy = y + NODE_H / 2;
var bg = nodeBgColor(n.svc || "");
var bc = nodeStatusColor(n.status || "");
var isFocus = focusUid && n.id === focusUid;
// Rect
var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
rect.setAttribute("x", x);
rect.setAttribute("y", y);
rect.setAttribute("width", NODE_W);
rect.setAttribute("height", NODE_H);
rect.setAttribute("rx", 8);
rect.setAttribute("fill", bg);
rect.setAttribute("stroke", isFocus ? "#facc15" : bc);
rect.setAttribute("stroke-width", isFocus ? 3 : 2);
rect.style.cursor = "pointer";
rect.addEventListener("click", function(e){
e.stopPropagation();
navigateToDep(n.id);
});
g.appendChild(rect);
// Name — wrapped
var nameLines = wrapText(n.label || n.id || "", 20);
var nameFs = nameLines.length > 2 ? 10 : nameLines.length > 1 ? 11 : 12;
var lineH = nameFs * 1.5;
var totalTextH = nameLines.length * lineH + (n.svc ? lineH : 0);
var startY = cy - totalTextH / 2 + nameFs;
nameLines.forEach(function(line, li){
var t = document.createElementNS("http://www.w3.org/2000/svg", "text");
t.setAttribute("x", cx);
t.setAttribute("y", startY + li * lineH);
t.setAttribute("text-anchor", "middle");
t.setAttribute("font-size", nameFs);
t.setAttribute("font-weight", "bold");
t.setAttribute("fill", "#dbeafe");
t.setAttribute("font-family", "system-ui,sans-serif");
t.style.pointerEvents = "none";
t.textContent = line;
g.appendChild(t);
});
// Service
if(n.svc){
var st = document.createElementNS("http://www.w3.org/2000/svg", "text");
st.setAttribute("x", cx);
st.setAttribute("y", startY + nameLines.length * lineH);
st.setAttribute("text-anchor", "middle");
st.setAttribute("font-size", 9);
st.setAttribute("font-style", "italic");
st.setAttribute("fill", "#93c5fd");
st.setAttribute("font-family", "system-ui,sans-serif");
st.style.pointerEvents = "none";
st.textContent = n.svc;
g.appendChild(st);
}
// Age
var age = humanAge(n.created || "");
if(age){
var at = document.createElementNS("http://www.w3.org/2000/svg", "text");
at.setAttribute("x", cx);
at.setAttribute("y", y + NODE_H - 6);
at.setAttribute("text-anchor", "middle");
at.setAttribute("font-size", 9);
at.setAttribute("fill", "#9ca3af");
at.setAttribute("font-family", "system-ui,sans-serif");
at.style.pointerEvents = "none";
at.textContent = "\u23f1 " + age;
g.appendChild(at);
}
});
svg.appendChild(g);
wrapper.appendChild(svg);
el.appendChild(wrapper);
// ── panzoom: pan + scroll-zoom ──
if(window.panzoom){
var instance = window.panzoom(g, {
smoothScroll: false,
bounds: false,
zoomDoubleClickSpeed: 1,
minZoom: 0.2,
maxZoom: 3
});
svg.addEventListener("dblclick", function(){
instance.moveTo(0, 0);
instance.zoomAbs(0, 0, 1);
});
}
}
// ── Open / Close running graph modal ──────────────────────────────────────
function openRunningGraph(){
var modal = document.getElementById("graph-modal");
modal.classList.add("open");
var titleEl = document.getElementById("graph-modal-title");
var statuses = getChecked();
if(titleEl) titleEl.textContent = "Общий граф зависимостей: " + (statuses.length ? statuses.join(", ") : "все");
var target = document.getElementById("running-graph");
if(target) target.innerHTML = '<div class="graph-loading">Собираю граф...</div>';
ensureAllGraphLoaded()
.then(function(data){ drawGraph("running-graph", data || {nodes:[], edges:[]}, ""); })
.catch(function(e){
var el2 = document.getElementById("running-graph");
if(el2) el2.innerHTML = '<div class="err-msg">' + escHtml(e.message) + '</div>';
});
}
function closeRunningGraph(){
document.getElementById("graph-modal").classList.remove("open");
}
function showGraphModal(title, data, focusUid){
var modal = document.getElementById("graph-modal");
var titleEl = document.getElementById("graph-modal-title");
if(titleEl) titleEl.textContent = title;
modal.classList.add("open");
drawGraph("running-graph", data || {nodes:[], edges:[]}, focusUid || "");
}
// ── Instance graph (вкладка Граф в детали) ─────────────────────────────────
function renderInstanceGraph(uid){
var holderId = "g-" + uid;
var holder = document.getElementById(holderId);
if(holder) holder.innerHTML = '<div class="graph-loading">Строю граф...</div>';
ensureFullGraphLoaded().then(function(){
var g = buildDepSubgraphFromDetail(uid);
if((g.nodes || []).length > 2){
showGraphModal("Граф зависимостей: " + escHtml(instanceName(uid)), g, uid);
} else {
drawGraph(holderId, g, uid);
}
}).catch(function(e){
var el = document.getElementById(holderId);
if(el) el.innerHTML = '<div class="err-msg">' + escHtml(e.message) + '</div>';
});
}
function getToken(){
return localStorage.getItem("deck_token") || "";
}
@@ -1236,10 +882,6 @@ function scheduleRender(){
allInstances = [];
cache = {};
rawDetail = {};
graphAllData = null;
graphRunningData = null;
graphFullData = null;
graphAllKey = "";
expandedUid = null;
navBackUid = null;
var nb = document.getElementById("nav-back");