adl-toolkit 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL_TOOLKIT.md +163 -0
- package/README.md +269 -2
- package/adl_drift/__init__.py +6 -0
- package/adl_drift/cli/__init__.py +1 -0
- package/adl_drift/cli/commands.py +993 -0
- package/adl_drift/cli/main.py +744 -0
- package/adl_drift/cortex/__init__.py +1 -0
- package/adl_drift/cortex/embeddings.py +96 -0
- package/adl_drift/cortex/memory.py +1749 -0
- package/adl_drift/cortex/patterns.py +89 -0
- package/adl_drift/cortex/telemetry.py +192 -0
- package/adl_drift/detectors/__init__.py +1 -0
- package/adl_drift/detectors/async_patterns.py +115 -0
- package/adl_drift/detectors/base.py +121 -0
- package/adl_drift/detectors/config.py +122 -0
- package/adl_drift/detectors/data_access.py +128 -0
- package/adl_drift/detectors/decorators.py +122 -0
- package/adl_drift/detectors/dynamic_patterns.py +260 -0
- package/adl_drift/detectors/errors.py +134 -0
- package/adl_drift/detectors/imports.py +132 -0
- package/adl_drift/detectors/logging_patterns.py +108 -0
- package/adl_drift/detectors/routes.py +257 -0
- package/adl_drift/detectors/security.py +236 -0
- package/adl_drift/detectors/taint.py +406 -0
- package/adl_drift/detectors/tests.py +95 -0
- package/adl_drift/detectors/type_conventions.py +81 -0
- package/adl_drift/graph/__init__.py +1 -0
- package/adl_drift/graph/builder.py +2670 -0
- package/adl_drift/graph/cfg.py +461 -0
- package/adl_drift/graph/cluster.py +40 -0
- package/adl_drift/graph/edges.py +86 -0
- package/adl_drift/graph/node.py +90 -0
- package/adl_drift/harness/__init__.py +10 -0
- package/adl_drift/harness/setup.py +832 -0
- package/adl_drift/harness/skills/drift-antigravity.md +19 -0
- package/adl_drift/harness/skills/drift-generic.md +19 -0
- package/adl_drift/harness/skills/drift.md +25 -0
- package/adl_drift/harness/watch.py +84 -0
- package/adl_drift/lang/__init__.py +8 -0
- package/adl_drift/lang/base.py +464 -0
- package/adl_drift/lang/java/__init__.py +6 -0
- package/adl_drift/lang/java/backend.py +97 -0
- package/adl_drift/lang/java/cfg.py +1424 -0
- package/adl_drift/lang/java/config.py +707 -0
- package/adl_drift/lang/java/detectors/__init__.py +29 -0
- package/adl_drift/lang/java/detectors/additional.py +445 -0
- package/adl_drift/lang/java/detectors/base.py +154 -0
- package/adl_drift/lang/java/detectors/data_access.py +148 -0
- package/adl_drift/lang/java/detectors/imports.py +53 -0
- package/adl_drift/lang/java/detectors/routes.py +159 -0
- package/adl_drift/lang/java/detectors/security.py +386 -0
- package/adl_drift/lang/java/detectors/tests.py +74 -0
- package/adl_drift/lang/java/extractor.py +582 -0
- package/adl_drift/lang/java/ids.py +99 -0
- package/adl_drift/lang/java/parser.py +49 -0
- package/adl_drift/lang/java/resolution/__init__.py +1 -0
- package/adl_drift/lang/java/resolution/constants.py +1619 -0
- package/adl_drift/lang/java/resolution/constructors.py +278 -0
- package/adl_drift/lang/java/resolution/context.py +305 -0
- package/adl_drift/lang/java/resolution/dispatch.py +540 -0
- package/adl_drift/lang/java/resolution/functional.py +1318 -0
- package/adl_drift/lang/java/resolution/generated.py +95 -0
- package/adl_drift/lang/java/resolution/model.py +153 -0
- package/adl_drift/lang/java/resolution/orchestration.py +866 -0
- package/adl_drift/lang/java/resolution/overloads.py +1275 -0
- package/adl_drift/lang/java/resolution/scopes.py +1111 -0
- package/adl_drift/lang/java/resolution/types.py +2947 -0
- package/adl_drift/lang/java/resolver.py +1401 -0
- package/adl_drift/lang/java/scopes.py +69 -0
- package/adl_drift/lang/java/taint.py +500 -0
- package/adl_drift/lang/java/type_provenance.py +342 -0
- package/adl_drift/lang/python/__init__.py +8 -0
- package/adl_drift/lang/python/backend.py +94 -0
- package/adl_drift/lang/python/cfg.py +537 -0
- package/adl_drift/lang/python/extractor.py +454 -0
- package/adl_drift/lang/python/parser.py +41 -0
- package/adl_drift/lang/python/resolver.py +1789 -0
- package/adl_drift/lang/python/taint.py +365 -0
- package/adl_drift/lang/registry.py +117 -0
- package/adl_drift/parser/__init__.py +1 -0
- package/adl_drift/parser/cache.py +38 -0
- package/adl_drift/parser/core.py +33 -0
- package/adl_drift/parser/extractor.py +17 -0
- package/adl_drift/parser/literal_eval.py +326 -0
- package/adl_drift/scanner/__init__.py +1 -0
- package/adl_drift/scanner/discovery.py +338 -0
- package/adl_drift/scanner/incremental.py +261 -0
- package/adl_drift/scanner/parallel.py +224 -0
- package/adl_drift/server/__init__.py +1 -0
- package/adl_drift/server/handlers.py +2849 -0
- package/adl_drift/server/mcp_server.py +127 -0
- package/adl_drift/server/mcp_stdio.py +335 -0
- package/adl_drift/server/protocol.py +71 -0
- package/adl_drift/server/static/index.html +696 -0
- package/adl_drift/server/web.py +350 -0
- package/adl_drift/utils/__init__.py +1 -0
- package/adl_drift/utils/config.py +77 -0
- package/adl_drift/utils/fswalk.py +52 -0
- package/adl_drift/utils/types.py +345 -0
- package/bin/adl-drift.mjs +35 -0
- package/bin/adl-toolkit.mjs +488 -0
- package/package.json +29 -4
- package/pyproject.toml +49 -0
- package/scripts/install_toolkit.py +3584 -0
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<title>ADL Drift Graph</title>
|
|
6
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
7
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
8
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" rel="stylesheet" />
|
|
9
|
+
<script src="https://unpkg.com/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
|
|
10
|
+
<style>
|
|
11
|
+
:root {
|
|
12
|
+
--canvas: #ffffff;
|
|
13
|
+
--canvas-soft: #fafafa;
|
|
14
|
+
--canvas-night: #1c1c1c;
|
|
15
|
+
--ink: #171717;
|
|
16
|
+
--ink-mute: #707070;
|
|
17
|
+
--ink-mute-2: #9a9a9a;
|
|
18
|
+
--ink-faint: #b2b2b2;
|
|
19
|
+
--hairline: #dfdfdf;
|
|
20
|
+
--hairline-strong: #c7c7c7;
|
|
21
|
+
--hairline-cool: #ededed;
|
|
22
|
+
--primary: #3ecf8e;
|
|
23
|
+
--primary-deep: #24b47e;
|
|
24
|
+
--on-primary: #171717;
|
|
25
|
+
--on-dark: #ffffff;
|
|
26
|
+
--shadow-1: 0 1px 3px rgba(0,0,0,0.06);
|
|
27
|
+
--shadow-2: 0 8px 24px rgba(0,0,0,0.08);
|
|
28
|
+
}
|
|
29
|
+
* { box-sizing: border-box; }
|
|
30
|
+
html, body {
|
|
31
|
+
margin: 0; padding: 0; height: 100%;
|
|
32
|
+
font-family: "Inter", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
33
|
+
font-size: 14px; font-weight: 400; line-height: 1.5;
|
|
34
|
+
background: var(--canvas); color: var(--ink);
|
|
35
|
+
}
|
|
36
|
+
#app { display: flex; height: 100vh; }
|
|
37
|
+
#net {
|
|
38
|
+
flex: 1; background: var(--canvas-soft);
|
|
39
|
+
background-image:
|
|
40
|
+
radial-gradient(circle, var(--hairline-cool) 1px, transparent 1px);
|
|
41
|
+
background-size: 24px 24px;
|
|
42
|
+
padding-top: 56px;
|
|
43
|
+
}
|
|
44
|
+
#side {
|
|
45
|
+
width: 380px; background: var(--canvas);
|
|
46
|
+
border-left: 1px solid var(--hairline);
|
|
47
|
+
padding: 24px; overflow-y: auto;
|
|
48
|
+
}
|
|
49
|
+
#side .empty { color: var(--ink-mute); font-size: 13px; }
|
|
50
|
+
|
|
51
|
+
#nav {
|
|
52
|
+
position: absolute; top: 0; left: 0; right: 380px; height: 56px;
|
|
53
|
+
background: var(--canvas); border-bottom: 1px solid var(--hairline);
|
|
54
|
+
padding: 0 24px; display: flex; align-items: center; gap: 12px;
|
|
55
|
+
z-index: 10;
|
|
56
|
+
}
|
|
57
|
+
#brand {
|
|
58
|
+
font-weight: 500; font-size: 16px; letter-spacing: -0.2px;
|
|
59
|
+
display: flex; align-items: center; gap: 8px;
|
|
60
|
+
}
|
|
61
|
+
#brand .dot { width: 8px; height: 8px; border-radius: 9999px; background: var(--primary); }
|
|
62
|
+
#nav-spacer { flex: 1; }
|
|
63
|
+
#search {
|
|
64
|
+
background: var(--canvas); color: var(--ink);
|
|
65
|
+
border: 1px solid var(--hairline); border-radius: 6px;
|
|
66
|
+
padding: 8px 12px; font-size: 14px; font-family: inherit;
|
|
67
|
+
min-width: 280px; outline: none;
|
|
68
|
+
}
|
|
69
|
+
#search:focus { border-color: var(--ink); }
|
|
70
|
+
.btn {
|
|
71
|
+
font-family: inherit; font-size: 14px; font-weight: 500;
|
|
72
|
+
padding: 8px 16px; border-radius: 6px; cursor: pointer;
|
|
73
|
+
border: 1px solid var(--hairline-strong); background: var(--canvas);
|
|
74
|
+
color: var(--ink); line-height: 1; height: 36px;
|
|
75
|
+
}
|
|
76
|
+
.btn:hover { background: var(--canvas-soft); }
|
|
77
|
+
.btn-primary {
|
|
78
|
+
background: var(--primary); color: var(--on-primary);
|
|
79
|
+
border-color: var(--primary);
|
|
80
|
+
}
|
|
81
|
+
.btn-primary:hover { background: var(--primary-deep); border-color: var(--primary-deep); }
|
|
82
|
+
.btn-sm { padding: 6px 10px; height: auto; font-size: 13px; }
|
|
83
|
+
.btn-danger { color: #b24040; }
|
|
84
|
+
|
|
85
|
+
#search-results {
|
|
86
|
+
position: absolute; top: 64px; left: 24px; z-index: 11;
|
|
87
|
+
background: var(--canvas); border: 1px solid var(--hairline);
|
|
88
|
+
border-radius: 8px; box-shadow: var(--shadow-2);
|
|
89
|
+
max-height: 360px; overflow-y: auto; display: none; min-width: 320px;
|
|
90
|
+
}
|
|
91
|
+
#search-results .item {
|
|
92
|
+
padding: 8px 12px; cursor: pointer; font-size: 13px;
|
|
93
|
+
border-bottom: 1px solid var(--hairline-cool);
|
|
94
|
+
}
|
|
95
|
+
#search-results .item:last-child { border-bottom: none; }
|
|
96
|
+
#search-results .item:hover { background: var(--canvas-soft); }
|
|
97
|
+
#search-results .item .type {
|
|
98
|
+
font-size: 11px; color: var(--ink-mute);
|
|
99
|
+
text-transform: lowercase; margin-left: 6px;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#status {
|
|
103
|
+
position: absolute; bottom: 12px; left: 24px;
|
|
104
|
+
font-size: 12px; color: var(--ink-mute);
|
|
105
|
+
background: var(--canvas); padding: 6px 12px;
|
|
106
|
+
border: 1px solid var(--hairline); border-radius: 6px;
|
|
107
|
+
box-shadow: var(--shadow-1);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
#banner {
|
|
111
|
+
position: absolute; top: 64px; left: 50%; transform: translateX(-50%);
|
|
112
|
+
z-index: 11; background: #fff7e6; border: 1px solid #f0c674;
|
|
113
|
+
color: #6b4500; padding: 8px 16px; border-radius: 6px;
|
|
114
|
+
font-size: 13px; box-shadow: var(--shadow-1); display: none;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.legend {
|
|
118
|
+
display: flex; gap: 16px; align-items: center;
|
|
119
|
+
font-size: 12px; color: var(--ink-mute);
|
|
120
|
+
}
|
|
121
|
+
.legend span.dot {
|
|
122
|
+
display: inline-block; width: 10px; height: 10px;
|
|
123
|
+
border-radius: 9999px; vertical-align: middle; margin-right: 6px;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
#side h3 {
|
|
127
|
+
margin: 0 0 4px 0; font-size: 22px; font-weight: 500;
|
|
128
|
+
letter-spacing: -0.3px; line-height: 1.2; color: var(--ink);
|
|
129
|
+
word-break: break-all;
|
|
130
|
+
}
|
|
131
|
+
#side .qualified {
|
|
132
|
+
font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
|
|
133
|
+
font-size: 12px; color: var(--ink-mute);
|
|
134
|
+
word-break: break-all; margin-bottom: 16px;
|
|
135
|
+
}
|
|
136
|
+
#side .meta {
|
|
137
|
+
border-top: 1px solid var(--hairline-cool); padding-top: 12px;
|
|
138
|
+
margin-bottom: 12px;
|
|
139
|
+
}
|
|
140
|
+
#side .meta .row {
|
|
141
|
+
display: flex; gap: 12px; margin-bottom: 6px; font-size: 13px;
|
|
142
|
+
}
|
|
143
|
+
#side .meta .label {
|
|
144
|
+
color: var(--ink-mute); min-width: 96px; flex-shrink: 0;
|
|
145
|
+
}
|
|
146
|
+
#side .meta .value { color: var(--ink); word-break: break-all; }
|
|
147
|
+
#side .meta .pill {
|
|
148
|
+
display: inline-block; padding: 2px 8px; border-radius: 9999px;
|
|
149
|
+
font-size: 11px; font-weight: 500; background: var(--canvas-soft);
|
|
150
|
+
border: 1px solid var(--hairline); color: var(--ink);
|
|
151
|
+
}
|
|
152
|
+
#side .meta .pill.primary { background: var(--primary); color: var(--on-primary); border-color: var(--primary); }
|
|
153
|
+
#side .code {
|
|
154
|
+
background: var(--canvas-night); color: var(--on-dark);
|
|
155
|
+
font-family: ui-monospace, Menlo, Monaco, Consolas, monospace;
|
|
156
|
+
font-size: 13px; line-height: 1.5;
|
|
157
|
+
padding: 12px 14px; border-radius: 6px;
|
|
158
|
+
white-space: pre-wrap; word-break: break-word;
|
|
159
|
+
margin-top: 4px;
|
|
160
|
+
}
|
|
161
|
+
#side .section-label {
|
|
162
|
+
font-size: 11px; font-weight: 500; text-transform: uppercase;
|
|
163
|
+
letter-spacing: 0.5px; color: var(--ink-mute);
|
|
164
|
+
margin: 16px 0 6px 0;
|
|
165
|
+
}
|
|
166
|
+
#side .actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 16px; }
|
|
167
|
+
</style>
|
|
168
|
+
</head>
|
|
169
|
+
<body>
|
|
170
|
+
<div id="app">
|
|
171
|
+
<div id="net"></div>
|
|
172
|
+
<div id="side"><p class="empty">Click a node for details. Double-click to expand children. Right-click to hide neighbors.</p></div>
|
|
173
|
+
</div>
|
|
174
|
+
|
|
175
|
+
<div id="nav">
|
|
176
|
+
<div id="brand"><span class="dot"></span>ADL Drift Graph</div>
|
|
177
|
+
<input id="search" placeholder="Search by qualified name..." />
|
|
178
|
+
<button id="btn-reset" class="btn">Reset</button>
|
|
179
|
+
<button id="btn-fit" class="btn">Fit</button>
|
|
180
|
+
<button id="btn-stats" class="btn btn-primary">Stats</button>
|
|
181
|
+
<div id="nav-spacer"></div>
|
|
182
|
+
<span class="legend">
|
|
183
|
+
<span><span class="dot" style="background: var(--ink)"></span>module</span>
|
|
184
|
+
<span><span class="dot" style="background: var(--ink-mute)"></span>class</span>
|
|
185
|
+
<span><span class="dot" style="background: var(--primary)"></span>function</span>
|
|
186
|
+
<span><span class="dot" style="background: #e8a13a"></span>reads var</span>
|
|
187
|
+
<span><span class="dot" style="background: #2aa7a0"></span>type edge</span>
|
|
188
|
+
<span><span class="dot" style="background: #d96aa8"></span>instantiates</span>
|
|
189
|
+
<span><span class="dot" style="background: #4f8cf7"></span>inherits/overrides</span>
|
|
190
|
+
<span><span class="dot" style="background: #4fb787"></span>implements</span>
|
|
191
|
+
<span><span class="dot" style="background: #b06aa8"></span>dynamic attr</span>
|
|
192
|
+
</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div id="search-results"></div>
|
|
195
|
+
<div id="banner"></div>
|
|
196
|
+
<div id="status">loading...</div>
|
|
197
|
+
|
|
198
|
+
<script>
|
|
199
|
+
// ---- visual constants ----
|
|
200
|
+
const NODE_CAP = 1000;
|
|
201
|
+
// 10-color cycle for community coloring; fall back to type color when cid<0.
|
|
202
|
+
const COMMUNITY_PALETTE = [
|
|
203
|
+
"#3ecf8e", "#4f8cf7", "#e07a5f", "#a16ae8", "#f2b134",
|
|
204
|
+
"#56c7d3", "#d96b9b", "#7ec96b", "#b58a4d", "#8b8fa3",
|
|
205
|
+
];
|
|
206
|
+
const TYPE_COLOR = {
|
|
207
|
+
MODULE: { bg: "#171717", fg: "#ffffff", border: "#171717" },
|
|
208
|
+
CLASS: { bg: "#707070", fg: "#ffffff", border: "#707070" },
|
|
209
|
+
FUNCTION: { bg: "#3ecf8e", fg: "#171717", border: "#3ecf8e" },
|
|
210
|
+
VARIABLE: { bg: "#9a9a9a", fg: "#171717", border: "#9a9a9a" },
|
|
211
|
+
IMPORT: { bg: "#b2b2b2", fg: "#171717", border: "#b2b2b2" },
|
|
212
|
+
};
|
|
213
|
+
const TYPE_SHAPE = {
|
|
214
|
+
MODULE: "box", // rounded rect
|
|
215
|
+
CLASS: "diamond",
|
|
216
|
+
FUNCTION: "dot",
|
|
217
|
+
VARIABLE: "dot",
|
|
218
|
+
IMPORT: "dot",
|
|
219
|
+
};
|
|
220
|
+
const BASE_SIZE = { MODULE: 18, CLASS: 14, FUNCTION: 10, VARIABLE: 8, IMPORT: 8 };
|
|
221
|
+
|
|
222
|
+
// Physics: slow continuous drift in idle. `maxVelocity` capped low and
|
|
223
|
+
// `damping` raised so nodes ooze rather than fly. `minVelocity` left at the
|
|
224
|
+
// default so the solver doesn't auto-halt — we want gentle motion, not a
|
|
225
|
+
// frozen canvas.
|
|
226
|
+
const PHYSICS = {
|
|
227
|
+
enabled: true,
|
|
228
|
+
solver: "forceAtlas2Based",
|
|
229
|
+
forceAtlas2Based: {
|
|
230
|
+
gravitationalConstant: -60,
|
|
231
|
+
centralGravity: 0.005,
|
|
232
|
+
springLength: 120,
|
|
233
|
+
springConstant: 0.05,
|
|
234
|
+
damping: 0.85,
|
|
235
|
+
avoidOverlap: 0.6,
|
|
236
|
+
},
|
|
237
|
+
stabilization: { enabled: true, iterations: 1500, fit: true },
|
|
238
|
+
minVelocity: 0.1,
|
|
239
|
+
maxVelocity: 4,
|
|
240
|
+
adaptiveTimestep: true,
|
|
241
|
+
timestep: 0.3,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
// ---- vis.js setup ----
|
|
245
|
+
const nodes = new vis.DataSet();
|
|
246
|
+
const edges = new vis.DataSet();
|
|
247
|
+
const container = document.getElementById("net");
|
|
248
|
+
|
|
249
|
+
const network = new vis.Network(container, { nodes, edges }, {
|
|
250
|
+
nodes: {
|
|
251
|
+
shape: "dot",
|
|
252
|
+
borderWidth: 1,
|
|
253
|
+
font: { face: "Inter", size: 11, color: "#171717" },
|
|
254
|
+
shadow: false,
|
|
255
|
+
// Disable hover/selection-driven visual mutations that read as "movement"
|
|
256
|
+
chosen: { node: false, label: false },
|
|
257
|
+
scaling: { label: { enabled: false } },
|
|
258
|
+
},
|
|
259
|
+
edges: {
|
|
260
|
+
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
|
261
|
+
smooth: { type: "continuous", roundness: 0.2 },
|
|
262
|
+
color: { color: "#dfdfdf", highlight: "#171717" },
|
|
263
|
+
chosen: false,
|
|
264
|
+
},
|
|
265
|
+
interaction: {
|
|
266
|
+
hover: true,
|
|
267
|
+
hoverConnectedEdges: false,
|
|
268
|
+
selectConnectedEdges: false,
|
|
269
|
+
multiselect: false,
|
|
270
|
+
tooltipDelay: 200,
|
|
271
|
+
zoomView: true,
|
|
272
|
+
dragView: true,
|
|
273
|
+
dragNodes: true,
|
|
274
|
+
},
|
|
275
|
+
physics: PHYSICS,
|
|
276
|
+
layout: { improvedLayout: false },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
function nodeColor(d) {
|
|
280
|
+
const cid = (d.community != null) ? d.community : -1;
|
|
281
|
+
if (cid >= 0) {
|
|
282
|
+
const c = COMMUNITY_PALETTE[cid % COMMUNITY_PALETTE.length];
|
|
283
|
+
return { background: c, border: c, highlight: { background: c, border: "#171717" } };
|
|
284
|
+
}
|
|
285
|
+
const t = TYPE_COLOR[d.type] || TYPE_COLOR.FUNCTION;
|
|
286
|
+
return { background: t.bg, border: t.border, highlight: { background: t.bg, border: "#171717" } };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function nodeSize(d) {
|
|
290
|
+
const base = BASE_SIZE[d.type] || 10;
|
|
291
|
+
// log-scale degree so hubs are visibly bigger but extreme outliers don't dominate
|
|
292
|
+
const deg = Math.max(0, d.degree || 0);
|
|
293
|
+
return base + Math.min(24, Math.log2(deg + 1) * 4);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function nodeFontHidden(d) {
|
|
297
|
+
// Hide label for low-degree, non-module nodes; show on hover via title.
|
|
298
|
+
if (d.type === "MODULE") return false;
|
|
299
|
+
return (d.degree || 0) < 3;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function toVisNode(d) {
|
|
303
|
+
const color = nodeColor(d);
|
|
304
|
+
const size = nodeSize(d);
|
|
305
|
+
const isEntry = !!d.is_entry_point;
|
|
306
|
+
return {
|
|
307
|
+
id: d.id,
|
|
308
|
+
label: nodeFontHidden(d) ? " " : d.name,
|
|
309
|
+
title: d.id + (d.signature ? "\n" + d.signature : ""),
|
|
310
|
+
shape: TYPE_SHAPE[d.type] || "dot",
|
|
311
|
+
size,
|
|
312
|
+
color,
|
|
313
|
+
borderWidth: isEntry ? 3 : 1,
|
|
314
|
+
borderWidthSelected: isEntry ? 4 : 2,
|
|
315
|
+
font: {
|
|
316
|
+
face: "Inter",
|
|
317
|
+
size: d.type === "MODULE" ? 12 : 11,
|
|
318
|
+
color: d.type === "MODULE" ? "#ffffff" : "#171717",
|
|
319
|
+
strokeWidth: d.type === "MODULE" ? 0 : 3,
|
|
320
|
+
strokeColor: "#fafafa",
|
|
321
|
+
},
|
|
322
|
+
_raw: d,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function edgeStyle(d) {
|
|
327
|
+
const conf = (d.confidence == null) ? 1.0 : d.confidence;
|
|
328
|
+
const type = d.type;
|
|
329
|
+
if (type === "CALLS" || type === "AWAITS" || type === "INSTANTIATES") {
|
|
330
|
+
if (conf >= 0.85) return { dashes: false, color: type === "INSTANTIATES" ? "#d96aa8" : "#3ecf8e", width: 2, opacity: 0.85 };
|
|
331
|
+
if (conf >= 0.6) return { dashes: [6, 4], color: "#707070", width: 1, opacity: 0.7 };
|
|
332
|
+
return { dashes: [3, 6], color: "#b2b2b2", width: 1, opacity: 0.4 };
|
|
333
|
+
}
|
|
334
|
+
if (type === "CONTAINS") return { dashes: false, color: "#dfdfdf", width: 1, opacity: 0.55 };
|
|
335
|
+
if (type === "INHERITS") return { dashes: false, color: "#4f8cf7", width: 1.5, opacity: 0.7 };
|
|
336
|
+
if (type === "IMPLEMENTS") return { dashes: [7, 3], color: "#4fb787", width: 1.5, opacity: 0.7 };
|
|
337
|
+
if (type === "DECORATED_BY") return { dashes: [2, 4], color: "#a16ae8", width: 1, opacity: 0.6 };
|
|
338
|
+
if (type === "IMPORTS") return { dashes: [4, 4], color: "#9a9a9a", width: 1, opacity: 0.5 };
|
|
339
|
+
if (type === "REFERENCES") return { dashes: [1, 3], color: "#e8a13a", width: 1, opacity: 0.65 }; // variable read
|
|
340
|
+
if (type === "TYPE_OF") return { dashes: [8, 3], color: "#2aa7a0", width: 1, opacity: 0.6 }; // param type
|
|
341
|
+
if (type === "RETURNS") return { dashes: [8, 3, 2, 3], color: "#2aa7a0", width: 1, opacity: 0.6 }; // return type
|
|
342
|
+
if (type === "OVERRIDES") return { dashes: [10, 4], color: "#4f8cf7", width: 1.5, opacity: 0.65 }; // method override
|
|
343
|
+
if (type === "DYNAMIC_ATTR") return { dashes: [3, 5], color: "#b06aa8", width: 1, opacity: 0.5 }; // open attr universe (P2.14)
|
|
344
|
+
return { dashes: false, color: "#dfdfdf", width: 1, opacity: 0.5 };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function toVisEdge(d) {
|
|
348
|
+
const s = edgeStyle(d);
|
|
349
|
+
return {
|
|
350
|
+
id: `${d.source}->${d.target}:${d.type}`,
|
|
351
|
+
from: d.source,
|
|
352
|
+
to: d.target,
|
|
353
|
+
arrows: { to: { enabled: true, scaleFactor: 0.5 } },
|
|
354
|
+
color: { color: s.color, opacity: s.opacity, highlight: "#171717" },
|
|
355
|
+
width: s.width,
|
|
356
|
+
dashes: s.dashes,
|
|
357
|
+
label: undefined,
|
|
358
|
+
_type: d.type,
|
|
359
|
+
_confidence: d.confidence,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---- physics lifecycle ----
|
|
364
|
+
// Initial: ON. After stabilization: OFF + pin node positions. Topology change:
|
|
365
|
+
// unpin + warm 800ms + repin. Keeps the canvas dead-still on hover / selection
|
|
366
|
+
// / version poll.
|
|
367
|
+
let stabilized = false;
|
|
368
|
+
let warmTimer = null;
|
|
369
|
+
|
|
370
|
+
function freezeAllNodes(freeze) {
|
|
371
|
+
const updates = [];
|
|
372
|
+
nodes.forEach((n) => updates.push({ id: n.id, fixed: { x: !!freeze, y: !!freeze } }));
|
|
373
|
+
if (updates.length) nodes.update(updates);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
network.on("stabilizationIterationsDone", () => {
|
|
377
|
+
stabilized = true;
|
|
378
|
+
// Keep physics ON — capped maxVelocity + high damping gives slow drift
|
|
379
|
+
// without violent motion. Nodes are not pinned in idle.
|
|
380
|
+
setStatus("layout stabilized — gentle drift");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
network.on("stabilized", () => {
|
|
384
|
+
if (stabilized) return;
|
|
385
|
+
stabilized = true;
|
|
386
|
+
setStatus("layout stabilized — gentle drift");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
function warmPhysics(ms = 800, freeIds = null) {
|
|
390
|
+
// Physics is always ON now (capped, slow). Nothing to warm. New nodes
|
|
391
|
+
// settle naturally under the same low-velocity solver. Kept as a no-op
|
|
392
|
+
// shim so callers don't need to change.
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ---- state + API ----
|
|
397
|
+
const side = document.getElementById("side");
|
|
398
|
+
const status = document.getElementById("status");
|
|
399
|
+
const banner = document.getElementById("banner");
|
|
400
|
+
const nodeData = new Map(); // id -> raw payload
|
|
401
|
+
const expanded = new Set();
|
|
402
|
+
|
|
403
|
+
function setStatus(m) { status.textContent = m; }
|
|
404
|
+
function showBanner(m) { banner.textContent = m; banner.style.display = "block"; }
|
|
405
|
+
function hideBanner() { banner.style.display = "none"; }
|
|
406
|
+
|
|
407
|
+
async function api(p) {
|
|
408
|
+
const r = await fetch(p);
|
|
409
|
+
if (!r.ok) throw new Error(await r.text());
|
|
410
|
+
return r.json();
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function shortId(id) { return id.length < 40 ? id : "..." + id.slice(-37); }
|
|
414
|
+
function escapeHtml(s) {
|
|
415
|
+
return String(s).replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]));
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function addPayload(payload) {
|
|
419
|
+
const newIds = [];
|
|
420
|
+
let edgeAdded = false;
|
|
421
|
+
for (const n of payload.nodes || []) {
|
|
422
|
+
if (nodeData.has(n.id)) continue;
|
|
423
|
+
if (nodes.length >= NODE_CAP) {
|
|
424
|
+
showBanner(`Repo too large — node cap ${NODE_CAP} hit. Use search or hide.`);
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
nodeData.set(n.id, n);
|
|
428
|
+
nodes.add(toVisNode(n));
|
|
429
|
+
newIds.push(n.id);
|
|
430
|
+
}
|
|
431
|
+
for (const e of payload.edges || []) {
|
|
432
|
+
if (!nodeData.has(e.source) || !nodeData.has(e.target)) continue;
|
|
433
|
+
const eid = `${e.source}->${e.target}:${e.type}`;
|
|
434
|
+
if (edges.get(eid)) continue;
|
|
435
|
+
edges.add(toVisEdge(e));
|
|
436
|
+
edgeAdded = true;
|
|
437
|
+
}
|
|
438
|
+
if (newIds.length) {
|
|
439
|
+
// Settle just the new arrivals — existing pinned nodes stay put.
|
|
440
|
+
warmPhysics(800, newIds);
|
|
441
|
+
} else if (edgeAdded) {
|
|
442
|
+
// Edges only (no new nodes) — no need to move anything.
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function loadRoots() {
|
|
447
|
+
setStatus("loading roots...");
|
|
448
|
+
hideBanner();
|
|
449
|
+
nodes.clear();
|
|
450
|
+
edges.clear();
|
|
451
|
+
nodeData.clear();
|
|
452
|
+
expanded.clear();
|
|
453
|
+
stabilized = false;
|
|
454
|
+
network.setOptions({ physics: PHYSICS });
|
|
455
|
+
const data = await api("/api/roots");
|
|
456
|
+
if ((data.nodes || []).length > NODE_CAP) {
|
|
457
|
+
showBanner(`Repo too large (${data.nodes.length} modules > ${NODE_CAP}) — use search or lazy expand.`);
|
|
458
|
+
}
|
|
459
|
+
addPayload(data);
|
|
460
|
+
setStatus(`${data.nodes.length} modules · double-click to expand`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function expandChildren(nodeId) {
|
|
464
|
+
setStatus(`expanding ${shortId(nodeId)}...`);
|
|
465
|
+
const data = await api(`/api/node/${encodeURIComponent(nodeId)}/children`);
|
|
466
|
+
addPayload(data);
|
|
467
|
+
expanded.add(nodeId);
|
|
468
|
+
setStatus(`+${data.nodes.length} children`);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function expandNeighbors(nodeId, direction) {
|
|
472
|
+
setStatus(`expanding ${direction}...`);
|
|
473
|
+
const data = await api(`/api/node/${encodeURIComponent(nodeId)}/neighbors?direction=${direction}`);
|
|
474
|
+
addPayload(data);
|
|
475
|
+
setStatus(`+${data.nodes.length} ${direction}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function hideRelated(nodeId) {
|
|
479
|
+
if (!nodeData.has(nodeId)) return;
|
|
480
|
+
const connected = network.getConnectedNodes(nodeId);
|
|
481
|
+
let n = 0;
|
|
482
|
+
for (const nid of connected) {
|
|
483
|
+
if (nid === nodeId) continue;
|
|
484
|
+
nodes.remove(nid);
|
|
485
|
+
nodeData.delete(nid);
|
|
486
|
+
n++;
|
|
487
|
+
}
|
|
488
|
+
expanded.delete(nodeId);
|
|
489
|
+
setStatus(`hid ${n} neighbors`);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function collapseSubtree(nodeId) {
|
|
493
|
+
if (!nodeData.has(nodeId)) return;
|
|
494
|
+
// walk CONTAINS-downstream from nodeId; remove descendants that have no
|
|
495
|
+
// other parent outside the subtree.
|
|
496
|
+
const all = nodes.getIds();
|
|
497
|
+
const sub = new Set([nodeId]);
|
|
498
|
+
let grew = true;
|
|
499
|
+
while (grew) {
|
|
500
|
+
grew = false;
|
|
501
|
+
edges.forEach((e) => {
|
|
502
|
+
if (e._type !== "CONTAINS") return;
|
|
503
|
+
if (sub.has(e.from) && !sub.has(e.to)) { sub.add(e.to); grew = true; }
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
sub.delete(nodeId);
|
|
507
|
+
let removed = 0;
|
|
508
|
+
for (const nid of sub) {
|
|
509
|
+
let extParent = false;
|
|
510
|
+
edges.forEach((e) => {
|
|
511
|
+
if (e._type !== "CONTAINS") return;
|
|
512
|
+
if (e.to === nid && !sub.has(e.from) && e.from !== nodeId) extParent = true;
|
|
513
|
+
});
|
|
514
|
+
if (!extParent) {
|
|
515
|
+
nodes.remove(nid);
|
|
516
|
+
nodeData.delete(nid);
|
|
517
|
+
removed++;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
expanded.delete(nodeId);
|
|
521
|
+
setStatus(`collapsed ${removed} nodes`);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function renderDetails(d) {
|
|
525
|
+
if (!d) {
|
|
526
|
+
side.innerHTML = `<p class="empty">no data</p>`;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const parts = [];
|
|
530
|
+
parts.push(`<h3>${escapeHtml(d.name)}</h3>`);
|
|
531
|
+
parts.push(`<div class="qualified">${escapeHtml(d.id)}</div>`);
|
|
532
|
+
|
|
533
|
+
parts.push(`<div class="meta">`);
|
|
534
|
+
parts.push(`<div class="row"><span class="label">Type</span><span class="value"><span class="pill ${d.type === 'FUNCTION' ? 'primary' : ''}">${d.type.toLowerCase()}</span></span></div>`);
|
|
535
|
+
if (d.module) parts.push(`<div class="row"><span class="label">Module</span><span class="value">${escapeHtml(d.module)}</span></div>`);
|
|
536
|
+
if (d.file) parts.push(`<div class="row"><span class="label">File</span><span class="value">${escapeHtml(d.file)}:${d.line}</span></div>`);
|
|
537
|
+
if (d.is_async) parts.push(`<div class="row"><span class="label">Async</span><span class="value"><span class="pill">async</span></span></div>`);
|
|
538
|
+
if (d.is_entry_point) parts.push(`<div class="row"><span class="label">Entry point</span><span class="value"><span class="pill primary">yes</span></span></div>`);
|
|
539
|
+
if (d.community != null && d.community >= 0) parts.push(`<div class="row"><span class="label">Community</span><span class="value">#${d.community}</span></div>`);
|
|
540
|
+
if (d.degree != null) parts.push(`<div class="row"><span class="label">Degree</span><span class="value">${d.degree}</span></div>`);
|
|
541
|
+
if (d.decorators && d.decorators.length) parts.push(`<div class="row"><span class="label">Decorators</span><span class="value">${d.decorators.map(escapeHtml).join(", ")}</span></div>`);
|
|
542
|
+
parts.push(`<div class="row"><span class="label">Calls</span><span class="value">${d.calls_count || 0} out · ${d.called_by_count || 0} in</span></div>`);
|
|
543
|
+
if (d.children_count != null) parts.push(`<div class="row"><span class="label">Children</span><span class="value">${d.children_count}</span></div>`);
|
|
544
|
+
parts.push(`</div>`);
|
|
545
|
+
|
|
546
|
+
if (d.signature) {
|
|
547
|
+
parts.push(`<div class="section-label">Signature</div>`);
|
|
548
|
+
parts.push(`<div class="code">${escapeHtml(d.signature)}</div>`);
|
|
549
|
+
}
|
|
550
|
+
if (d.docstring) {
|
|
551
|
+
parts.push(`<div class="section-label">Docstring</div>`);
|
|
552
|
+
parts.push(`<div class="code">${escapeHtml(d.docstring)}</div>`);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
parts.push(`<div class="section-label">Actions</div>`);
|
|
556
|
+
parts.push(`<div class="actions">
|
|
557
|
+
<button class="btn btn-sm btn-primary" data-act="expand">Expand</button>
|
|
558
|
+
<button class="btn btn-sm" data-act="callees">Callees</button>
|
|
559
|
+
<button class="btn btn-sm" data-act="callers">Callers</button>
|
|
560
|
+
<button class="btn btn-sm btn-danger" data-act="collapse">Collapse</button>
|
|
561
|
+
<button class="btn btn-sm btn-danger" data-act="hide">Hide</button>
|
|
562
|
+
</div>`);
|
|
563
|
+
side.innerHTML = parts.join("");
|
|
564
|
+
side.querySelectorAll("[data-act]").forEach((b) => {
|
|
565
|
+
b.addEventListener("click", () => {
|
|
566
|
+
const act = b.getAttribute("data-act");
|
|
567
|
+
if (act === "expand") expandChildren(d.id);
|
|
568
|
+
else if (act === "callees") expandNeighbors(d.id, "callees");
|
|
569
|
+
else if (act === "callers") expandNeighbors(d.id, "callers");
|
|
570
|
+
else if (act === "collapse") collapseSubtree(d.id);
|
|
571
|
+
else if (act === "hide") hideRelated(d.id);
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
network.on("click", (evt) => {
|
|
577
|
+
if (evt.nodes.length) renderDetails(nodeData.get(evt.nodes[0]));
|
|
578
|
+
});
|
|
579
|
+
network.on("doubleClick", async (evt) => {
|
|
580
|
+
if (!evt.nodes.length) return;
|
|
581
|
+
const id = evt.nodes[0];
|
|
582
|
+
if (expanded.has(id)) collapseSubtree(id);
|
|
583
|
+
else await expandChildren(id);
|
|
584
|
+
});
|
|
585
|
+
network.on("oncontext", (evt) => {
|
|
586
|
+
evt.event.preventDefault();
|
|
587
|
+
const id = network.getNodeAt(evt.pointer.DOM);
|
|
588
|
+
if (id) hideRelated(id);
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
document.getElementById("btn-reset").addEventListener("click", loadRoots);
|
|
592
|
+
document.getElementById("btn-fit").addEventListener("click", () => network.fit({ animation: { duration: 300 } }));
|
|
593
|
+
document.getElementById("btn-stats").addEventListener("click", async () => {
|
|
594
|
+
const s = await api("/api/stats");
|
|
595
|
+
side.innerHTML =
|
|
596
|
+
`<h3>Graph stats</h3>
|
|
597
|
+
<div class="qualified">${s.total_nodes} nodes · ${s.total_edges} edges</div>
|
|
598
|
+
<div class="section-label">By type</div>
|
|
599
|
+
<div class="code">${escapeHtml(JSON.stringify(s, null, 2))}</div>`;
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
const searchInput = document.getElementById("search");
|
|
603
|
+
const searchResults = document.getElementById("search-results");
|
|
604
|
+
let searchTimer = null;
|
|
605
|
+
searchInput.addEventListener("input", () => {
|
|
606
|
+
clearTimeout(searchTimer);
|
|
607
|
+
const q = searchInput.value.trim();
|
|
608
|
+
if (!q) { searchResults.style.display = "none"; return; }
|
|
609
|
+
searchTimer = setTimeout(async () => {
|
|
610
|
+
const r = await api(`/api/search?q=${encodeURIComponent(q)}`);
|
|
611
|
+
if (!r.matches.length) {
|
|
612
|
+
searchResults.innerHTML = `<div class="item">no match</div>`;
|
|
613
|
+
} else {
|
|
614
|
+
searchResults.innerHTML = r.matches.map((m) =>
|
|
615
|
+
`<div class="item" data-id="${escapeHtml(m.id)}">${escapeHtml(m.id)}<span class="type">${m.type.toLowerCase()}</span></div>`
|
|
616
|
+
).join("");
|
|
617
|
+
}
|
|
618
|
+
searchResults.style.display = "block";
|
|
619
|
+
}, 200);
|
|
620
|
+
});
|
|
621
|
+
document.addEventListener("click", (e) => {
|
|
622
|
+
if (!searchResults.contains(e.target) && e.target !== searchInput) {
|
|
623
|
+
searchResults.style.display = "none";
|
|
624
|
+
}
|
|
625
|
+
});
|
|
626
|
+
searchResults.addEventListener("click", async (e) => {
|
|
627
|
+
const div = e.target.closest(".item[data-id]");
|
|
628
|
+
if (!div) return;
|
|
629
|
+
const id = div.getAttribute("data-id");
|
|
630
|
+
searchResults.style.display = "none";
|
|
631
|
+
searchInput.value = "";
|
|
632
|
+
const detail = await api(`/api/node/${encodeURIComponent(id)}`);
|
|
633
|
+
if (!nodeData.has(id)) {
|
|
634
|
+
nodeData.set(id, detail);
|
|
635
|
+
nodes.add(toVisNode(detail));
|
|
636
|
+
warmPhysics(800);
|
|
637
|
+
}
|
|
638
|
+
network.focus(id, { scale: 1.4, animation: { duration: 400 } });
|
|
639
|
+
network.selectNodes([id]);
|
|
640
|
+
renderDetails(detail);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// ---- version polling (data-only refresh; never touches physics) ----
|
|
644
|
+
let currentVersion = null;
|
|
645
|
+
async function pollVersion() {
|
|
646
|
+
try {
|
|
647
|
+
const v = await api("/api/version");
|
|
648
|
+
const key = `${v.mtime}:${v.files}`;
|
|
649
|
+
if (currentVersion && currentVersion !== key) {
|
|
650
|
+
setStatus("files changed — refreshing...");
|
|
651
|
+
let topologyChanged = false;
|
|
652
|
+
const visible = Array.from(nodeData.keys());
|
|
653
|
+
for (const id of visible) {
|
|
654
|
+
try {
|
|
655
|
+
const fresh = await api(`/api/node/${encodeURIComponent(id)}`);
|
|
656
|
+
nodeData.set(id, fresh);
|
|
657
|
+
// Diff-only update: change border to emerald flash, leave position
|
|
658
|
+
// (`fixed`, `x`, `y`) untouched so the node doesn't drift.
|
|
659
|
+
const existing = nodes.get(id);
|
|
660
|
+
const baseColor = existing && existing.color ? existing.color : {};
|
|
661
|
+
nodes.update({
|
|
662
|
+
id,
|
|
663
|
+
borderWidth: 3,
|
|
664
|
+
color: { ...baseColor, border: "#3ecf8e" },
|
|
665
|
+
});
|
|
666
|
+
setTimeout(() => {
|
|
667
|
+
const cur = nodeData.get(id);
|
|
668
|
+
if (!cur) return;
|
|
669
|
+
const restored = toVisNode(cur);
|
|
670
|
+
// Preserve pinned state; only restore borderWidth + color.
|
|
671
|
+
nodes.update({
|
|
672
|
+
id,
|
|
673
|
+
borderWidth: restored.borderWidth,
|
|
674
|
+
color: restored.color,
|
|
675
|
+
});
|
|
676
|
+
}, 3000);
|
|
677
|
+
} catch (e) {
|
|
678
|
+
nodes.remove(id);
|
|
679
|
+
nodeData.delete(id);
|
|
680
|
+
topologyChanged = true;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
setStatus(`refreshed ${visible.length} nodes`);
|
|
684
|
+
if (topologyChanged) warmPhysics(800);
|
|
685
|
+
}
|
|
686
|
+
currentVersion = key;
|
|
687
|
+
} catch (e) {
|
|
688
|
+
setStatus("poll error: " + e.message);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
setInterval(pollVersion, 2000);
|
|
692
|
+
|
|
693
|
+
loadRoots().catch((e) => setStatus("error: " + e.message));
|
|
694
|
+
</script>
|
|
695
|
+
</body>
|
|
696
|
+
</html>
|