@composed-di/observability 0.5.0-alpha → 0.6.0-alpha
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/dist/cli.js.map +1 -1
- package/dist/dashboardClient.d.ts +19 -11
- package/dist/dashboardClient.d.ts.map +1 -1
- package/dist/dashboardClient.js +30 -11
- package/dist/dashboardClient.js.map +1 -1
- package/dist/dashboardEventListener.d.ts +27 -8
- package/dist/dashboardEventListener.d.ts.map +1 -1
- package/dist/dashboardEventListener.js +38 -16
- package/dist/dashboardEventListener.js.map +1 -1
- package/dist/dashboardHtml.d.ts +6 -0
- package/dist/dashboardHtml.d.ts.map +1 -1
- package/dist/dashboardHtml.js +618 -98
- package/dist/dashboardHtml.js.map +1 -1
- package/dist/dashboardInstrumentation.d.ts +42 -0
- package/dist/dashboardInstrumentation.d.ts.map +1 -0
- package/dist/dashboardInstrumentation.js +116 -0
- package/dist/dashboardInstrumentation.js.map +1 -0
- package/dist/dashboardServer.d.ts +14 -8
- package/dist/dashboardServer.d.ts.map +1 -1
- package/dist/dashboardServer.js +54 -14
- package/dist/dashboardServer.js.map +1 -1
- package/dist/events.d.ts +20 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/moduleGraph.d.ts.map +1 -1
- package/dist/moduleGraph.js.map +1 -1
- package/package.json +24 -23
- package/src/cli.ts +19 -19
- package/src/dashboardClient.ts +96 -83
- package/src/dashboardHtml.ts +620 -100
- package/src/dashboardInstrumentation.ts +151 -0
- package/src/dashboardServer.ts +185 -148
- package/src/events.ts +55 -35
- package/src/index.ts +5 -5
- package/src/moduleGraph.ts +9 -9
- package/src/dashboardEventListener.ts +0 -104
package/dist/dashboardHtml.js
CHANGED
|
@@ -9,6 +9,12 @@ exports.renderDashboardHtml = renderDashboardHtml;
|
|
|
9
9
|
* dependency ranks, dependencies on the left), a stat-tile row, a services
|
|
10
10
|
* table, and an event log. Service state is shown as a status glyph + word,
|
|
11
11
|
* never color alone.
|
|
12
|
+
*
|
|
13
|
+
* Visual language: warm paper surfaces, ink text, hairline rules. The orange
|
|
14
|
+
* accent is reserved for live activity (call flashes, active edges,
|
|
15
|
+
* selection); blue means initializing, green means ready. Display type is
|
|
16
|
+
* Poppins and editorial notes are Lora — both resolve to local fonts only
|
|
17
|
+
* (Arial / Georgia fallbacks), keeping the no-CDN contract.
|
|
12
18
|
*/
|
|
13
19
|
function renderDashboardHtml() {
|
|
14
20
|
return HTML;
|
|
@@ -21,147 +27,229 @@ const HTML = `<!doctype html>
|
|
|
21
27
|
<title>composed-di dashboard</title>
|
|
22
28
|
<style>
|
|
23
29
|
:root {
|
|
24
|
-
--page: #
|
|
25
|
-
--surface: #
|
|
26
|
-
--ink: #
|
|
27
|
-
--ink-secondary: #
|
|
28
|
-
--ink-muted: #
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
--
|
|
34
|
-
--
|
|
30
|
+
--page: #faf9f5;
|
|
31
|
+
--surface: #fffefb;
|
|
32
|
+
--ink: #141413;
|
|
33
|
+
--ink-secondary: #5c5a53;
|
|
34
|
+
--ink-muted: #87857c;
|
|
35
|
+
--ink-faint: #b0aea5;
|
|
36
|
+
--grid: #e8e6dc;
|
|
37
|
+
--edge: #d6d3c8;
|
|
38
|
+
--border: rgba(20, 20, 19, 0.10);
|
|
39
|
+
--accent: #d97757;
|
|
40
|
+
--good: #788c5d;
|
|
41
|
+
--busy: #6a9bcc;
|
|
42
|
+
--critical: #b8442f;
|
|
43
|
+
--shadow: 0 1px 2px rgba(20, 20, 19, 0.04);
|
|
44
|
+
--overlay-shadow: -14px 0 32px rgba(20, 20, 19, 0.14);
|
|
45
|
+
--font-display: "Poppins", "Avenir Next", "Segoe UI", Arial, sans-serif;
|
|
46
|
+
--font-serif: "Lora", Georgia, "Times New Roman", serif;
|
|
47
|
+
--font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
35
48
|
}
|
|
36
49
|
/* Dark values apply via the system preference, or ?theme=dark|light. */
|
|
37
50
|
@media (prefers-color-scheme: dark) {
|
|
38
51
|
:root:not([data-theme="light"]) {
|
|
39
|
-
--page: #
|
|
40
|
-
--surface: #
|
|
41
|
-
--ink: #
|
|
42
|
-
--ink-secondary: #
|
|
43
|
-
--ink-muted: #
|
|
44
|
-
--
|
|
45
|
-
--
|
|
46
|
-
--
|
|
52
|
+
--page: #141413;
|
|
53
|
+
--surface: #1e1d1a;
|
|
54
|
+
--ink: #faf9f5;
|
|
55
|
+
--ink-secondary: #b0aea5;
|
|
56
|
+
--ink-muted: #8c8a80;
|
|
57
|
+
--ink-faint: #57554e;
|
|
58
|
+
--grid: #2c2b27;
|
|
59
|
+
--edge: #3a382f;
|
|
60
|
+
--border: rgba(250, 249, 245, 0.10);
|
|
61
|
+
--accent: #d97757;
|
|
62
|
+
--good: #8fa571;
|
|
63
|
+
--busy: #7ba3ce;
|
|
64
|
+
--critical: #dd5f4b;
|
|
65
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
|
|
66
|
+
--overlay-shadow: -14px 0 32px rgba(0, 0, 0, 0.45);
|
|
47
67
|
}
|
|
48
68
|
}
|
|
49
69
|
:root[data-theme="dark"] {
|
|
50
|
-
--page: #
|
|
51
|
-
--surface: #
|
|
52
|
-
--ink: #
|
|
53
|
-
--ink-secondary: #
|
|
54
|
-
--ink-muted: #
|
|
55
|
-
--
|
|
56
|
-
--
|
|
57
|
-
--
|
|
70
|
+
--page: #141413;
|
|
71
|
+
--surface: #1e1d1a;
|
|
72
|
+
--ink: #faf9f5;
|
|
73
|
+
--ink-secondary: #b0aea5;
|
|
74
|
+
--ink-muted: #8c8a80;
|
|
75
|
+
--ink-faint: #57554e;
|
|
76
|
+
--grid: #2c2b27;
|
|
77
|
+
--edge: #3a382f;
|
|
78
|
+
--border: rgba(250, 249, 245, 0.10);
|
|
79
|
+
--accent: #d97757;
|
|
80
|
+
--good: #8fa571;
|
|
81
|
+
--busy: #7ba3ce;
|
|
82
|
+
--critical: #dd5f4b;
|
|
83
|
+
--shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
|
|
84
|
+
--overlay-shadow: -14px 0 32px rgba(0, 0, 0, 0.45);
|
|
58
85
|
}
|
|
59
86
|
* { box-sizing: border-box; }
|
|
60
87
|
body {
|
|
61
88
|
margin: 0;
|
|
62
89
|
background: var(--page);
|
|
90
|
+
background-image: radial-gradient(1100px 380px at 75% -120px,
|
|
91
|
+
color-mix(in srgb, var(--accent) 6%, transparent), transparent 70%);
|
|
63
92
|
color: var(--ink);
|
|
64
|
-
font: 14px/1.
|
|
93
|
+
font: 14px/1.5 var(--font-display);
|
|
94
|
+
}
|
|
95
|
+
/* Brand rule: a single orange hairline crowning the page. */
|
|
96
|
+
body::before {
|
|
97
|
+
content: "";
|
|
98
|
+
position: fixed;
|
|
99
|
+
top: 0; left: 0; right: 0;
|
|
100
|
+
height: 3px;
|
|
101
|
+
background: var(--accent);
|
|
102
|
+
z-index: 30;
|
|
65
103
|
}
|
|
104
|
+
::selection { background: color-mix(in srgb, var(--accent) 28%, transparent); }
|
|
105
|
+
|
|
106
|
+
@keyframes rise {
|
|
107
|
+
from { opacity: 0; transform: translateY(6px); }
|
|
108
|
+
}
|
|
109
|
+
header, .tiles, main > * { animation: rise 0.5s cubic-bezier(0.2, 0.7, 0.3, 1) backwards; }
|
|
110
|
+
.tiles { animation-delay: 0.06s; }
|
|
111
|
+
main > :nth-child(1) { animation-delay: 0.12s; }
|
|
112
|
+
main > :nth-child(2) { animation-delay: 0.18s; }
|
|
113
|
+
|
|
66
114
|
header {
|
|
67
115
|
display: flex;
|
|
68
|
-
align-items:
|
|
69
|
-
gap:
|
|
70
|
-
padding:
|
|
116
|
+
align-items: center;
|
|
117
|
+
gap: 10px;
|
|
118
|
+
padding: 20px 24px 0;
|
|
119
|
+
}
|
|
120
|
+
header .mark { flex: none; }
|
|
121
|
+
header h1 {
|
|
122
|
+
font-size: 17px;
|
|
123
|
+
font-weight: 600;
|
|
124
|
+
letter-spacing: -0.01em;
|
|
125
|
+
margin: 0;
|
|
126
|
+
}
|
|
127
|
+
header .sub {
|
|
128
|
+
color: var(--ink-muted);
|
|
129
|
+
font-family: var(--font-serif);
|
|
130
|
+
font-style: italic;
|
|
131
|
+
font-size: 13px;
|
|
71
132
|
}
|
|
72
|
-
header h1 { font-size: 16px; font-weight: 650; margin: 0; }
|
|
73
|
-
header .sub { color: var(--ink-muted); font-size: 12px; }
|
|
74
133
|
#conn {
|
|
75
134
|
margin-left: auto;
|
|
76
135
|
font-size: 12px;
|
|
77
136
|
color: var(--ink-secondary);
|
|
78
137
|
border: 1px solid var(--border);
|
|
138
|
+
background: var(--surface);
|
|
79
139
|
border-radius: 999px;
|
|
80
|
-
padding:
|
|
140
|
+
padding: 3px 12px;
|
|
81
141
|
}
|
|
82
|
-
#conn.live::before {
|
|
83
|
-
|
|
142
|
+
#conn.live::before {
|
|
143
|
+
content: "\\25CF ";
|
|
144
|
+
color: var(--good);
|
|
145
|
+
animation: breathe 2.4s ease-in-out infinite;
|
|
146
|
+
}
|
|
147
|
+
#conn.down::before { content: "\\25CF "; color: var(--critical); }
|
|
148
|
+
@keyframes breathe { 50% { opacity: 0.35; } }
|
|
84
149
|
|
|
85
150
|
.tiles {
|
|
86
151
|
display: flex;
|
|
87
152
|
flex-wrap: wrap;
|
|
88
153
|
gap: 12px;
|
|
89
|
-
padding: 14px
|
|
154
|
+
padding: 18px 24px 14px;
|
|
90
155
|
}
|
|
91
156
|
.tile {
|
|
92
157
|
background: var(--surface);
|
|
93
158
|
border: 1px solid var(--border);
|
|
94
|
-
border-radius:
|
|
95
|
-
|
|
96
|
-
|
|
159
|
+
border-radius: 10px;
|
|
160
|
+
box-shadow: var(--shadow);
|
|
161
|
+
padding: 12px 18px 14px;
|
|
162
|
+
min-width: 148px;
|
|
97
163
|
}
|
|
98
164
|
.tile .label {
|
|
99
|
-
font-size:
|
|
100
|
-
letter-spacing: 0.
|
|
165
|
+
font-size: 10.5px;
|
|
166
|
+
letter-spacing: 0.08em;
|
|
101
167
|
text-transform: uppercase;
|
|
168
|
+
font-weight: 500;
|
|
102
169
|
color: var(--ink-muted);
|
|
103
170
|
}
|
|
104
|
-
.tile .value {
|
|
105
|
-
|
|
171
|
+
.tile .value {
|
|
172
|
+
font-size: 26px;
|
|
173
|
+
font-weight: 600;
|
|
174
|
+
letter-spacing: -0.01em;
|
|
175
|
+
margin-top: 2px;
|
|
176
|
+
font-variant-numeric: tabular-nums;
|
|
177
|
+
}
|
|
178
|
+
.tile .value.bad { color: var(--critical); }
|
|
179
|
+
.tile .value small { font-size: 13px; font-weight: 400; color: var(--ink-muted); }
|
|
106
180
|
|
|
107
181
|
main {
|
|
108
182
|
display: grid;
|
|
109
183
|
grid-template-columns: 1fr 340px;
|
|
110
|
-
gap:
|
|
111
|
-
padding: 0
|
|
184
|
+
gap: 14px;
|
|
185
|
+
padding: 0 24px 24px;
|
|
112
186
|
align-items: start;
|
|
113
187
|
}
|
|
114
188
|
.panel {
|
|
115
189
|
background: var(--surface);
|
|
116
190
|
border: 1px solid var(--border);
|
|
117
|
-
border-radius:
|
|
191
|
+
border-radius: 10px;
|
|
192
|
+
box-shadow: var(--shadow);
|
|
118
193
|
overflow: hidden;
|
|
119
194
|
}
|
|
120
195
|
.panel h2 {
|
|
121
|
-
font-size:
|
|
122
|
-
letter-spacing: 0.
|
|
196
|
+
font-size: 11px;
|
|
197
|
+
letter-spacing: 0.08em;
|
|
123
198
|
text-transform: uppercase;
|
|
124
199
|
color: var(--ink-muted);
|
|
125
200
|
font-weight: 600;
|
|
126
201
|
margin: 0;
|
|
127
|
-
padding:
|
|
202
|
+
padding: 12px 16px;
|
|
128
203
|
border-bottom: 1px solid var(--grid);
|
|
129
204
|
}
|
|
130
205
|
#graph-panel svg { display: block; width: 100%; }
|
|
131
|
-
#graph-empty {
|
|
206
|
+
#graph-empty {
|
|
207
|
+
padding: 40px 32px;
|
|
208
|
+
color: var(--ink-muted);
|
|
209
|
+
text-align: center;
|
|
210
|
+
font-family: var(--font-serif);
|
|
211
|
+
font-style: italic;
|
|
212
|
+
font-size: 14px;
|
|
213
|
+
}
|
|
132
214
|
|
|
133
215
|
.legend {
|
|
134
216
|
display: flex;
|
|
135
217
|
flex-wrap: wrap;
|
|
136
|
-
gap:
|
|
137
|
-
padding: 10px
|
|
218
|
+
gap: 16px;
|
|
219
|
+
padding: 10px 16px;
|
|
138
220
|
border-top: 1px solid var(--grid);
|
|
139
221
|
font-size: 12px;
|
|
140
222
|
color: var(--ink-secondary);
|
|
141
223
|
}
|
|
142
224
|
.legend .glyph { font-size: 11px; }
|
|
225
|
+
.legend .hint {
|
|
226
|
+
margin-left: auto;
|
|
227
|
+
color: var(--ink-muted);
|
|
228
|
+
font-family: var(--font-serif);
|
|
229
|
+
font-style: italic;
|
|
230
|
+
}
|
|
143
231
|
|
|
144
232
|
/* Graph marks */
|
|
145
233
|
.edge {
|
|
146
234
|
fill: none;
|
|
147
|
-
stroke: var(--
|
|
235
|
+
stroke: var(--edge);
|
|
148
236
|
stroke-width: 1.5;
|
|
149
237
|
transition: stroke 0.45s ease;
|
|
150
238
|
}
|
|
151
239
|
.edge.active { stroke: var(--accent); stroke-width: 2.5; transition: none; }
|
|
152
240
|
.node rect {
|
|
153
241
|
fill: var(--surface);
|
|
154
|
-
stroke: var(--ink-
|
|
242
|
+
stroke: var(--ink-faint);
|
|
155
243
|
stroke-width: 1.5;
|
|
156
|
-
rx:
|
|
244
|
+
rx: 8;
|
|
157
245
|
}
|
|
158
246
|
.node text { fill: var(--ink); font-size: 13px; font-weight: 600; }
|
|
159
247
|
.node .status { fill: var(--ink-secondary); font-size: 11px; font-weight: 400; }
|
|
160
248
|
.node .glyph { font-size: 10px; }
|
|
161
|
-
.node.pending rect { stroke: var(--ink-
|
|
249
|
+
.node.pending rect { stroke: var(--ink-faint); stroke-dasharray: 4 3; }
|
|
162
250
|
.node.pending .glyph { fill: var(--ink-muted); }
|
|
163
|
-
.node.initializing rect { stroke: var(--
|
|
164
|
-
.node.initializing .glyph { fill: var(--
|
|
251
|
+
.node.initializing rect { stroke: var(--busy); animation: pulse 1s ease-in-out infinite; }
|
|
252
|
+
.node.initializing .glyph { fill: var(--busy); }
|
|
165
253
|
.node.ready rect { stroke: var(--good); }
|
|
166
254
|
.node.ready .glyph { fill: var(--good); }
|
|
167
255
|
.node.error rect { stroke: var(--critical); stroke-width: 2; }
|
|
@@ -169,62 +257,209 @@ const HTML = `<!doctype html>
|
|
|
169
257
|
.node.disposed rect { stroke: var(--grid); }
|
|
170
258
|
.node.disposed .glyph { fill: var(--ink-muted); }
|
|
171
259
|
.node.disposed text { fill: var(--ink-muted); }
|
|
172
|
-
.node
|
|
260
|
+
.node { cursor: pointer; }
|
|
261
|
+
.node.flash rect {
|
|
262
|
+
stroke: var(--accent);
|
|
263
|
+
stroke-width: 2.5;
|
|
264
|
+
fill: color-mix(in srgb, var(--accent) 7%, var(--surface));
|
|
265
|
+
}
|
|
266
|
+
.node.selected rect { stroke: var(--accent); stroke-width: 2.5; }
|
|
267
|
+
.node:focus-visible { outline: none; }
|
|
268
|
+
.node:focus-visible rect { stroke: var(--accent); }
|
|
173
269
|
@keyframes pulse { 50% { stroke-opacity: 0.35; } }
|
|
174
270
|
|
|
175
271
|
/* Tables */
|
|
176
272
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
177
|
-
th, td { text-align: left; padding:
|
|
273
|
+
th, td { text-align: left; padding: 7px 16px; border-top: 1px solid var(--grid); }
|
|
178
274
|
thead th {
|
|
179
275
|
border-top: none;
|
|
180
|
-
font-size:
|
|
181
|
-
letter-spacing: 0.
|
|
276
|
+
font-size: 10.5px;
|
|
277
|
+
letter-spacing: 0.08em;
|
|
182
278
|
text-transform: uppercase;
|
|
183
279
|
color: var(--ink-muted);
|
|
184
280
|
font-weight: 600;
|
|
185
281
|
}
|
|
186
|
-
td.num, th.num { text-align: right;
|
|
282
|
+
td.num, th.num { text-align: right; }
|
|
283
|
+
td.num { font-family: var(--font-mono); font-size: 12px; font-variant-numeric: tabular-nums; }
|
|
284
|
+
td.err { color: var(--critical); font-weight: 600; }
|
|
187
285
|
td .glyph { font-size: 10px; }
|
|
188
286
|
|
|
189
|
-
/*
|
|
287
|
+
/* Clickable service rows + per-method drill-down */
|
|
288
|
+
tr.svc-row { cursor: pointer; }
|
|
289
|
+
tr.svc-row:hover { background: color-mix(in srgb, var(--accent) 6%, transparent); }
|
|
290
|
+
tr.svc-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
291
|
+
tr.svc-row.selected { background: color-mix(in srgb, var(--accent) 10%, transparent); }
|
|
292
|
+
tr.svc-row .caret {
|
|
293
|
+
display: inline-block;
|
|
294
|
+
width: 14px;
|
|
295
|
+
color: var(--ink-faint);
|
|
296
|
+
font-size: 10px;
|
|
297
|
+
}
|
|
298
|
+
tr.method-detail > td { padding: 0; background: var(--page); }
|
|
299
|
+
.methods table { font-size: 12px; }
|
|
300
|
+
.methods thead th { font-size: 10px; }
|
|
301
|
+
.methods th:first-child, .methods td:first-child { padding-left: 46px; }
|
|
302
|
+
.methods .empty {
|
|
303
|
+
padding: 8px 16px 10px 46px;
|
|
304
|
+
color: var(--ink-muted);
|
|
305
|
+
font-family: var(--font-serif);
|
|
306
|
+
font-style: italic;
|
|
307
|
+
font-size: 12px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/* Event log (shared row styles with the inspector's call history) */
|
|
311
|
+
#log, .insp-body { scrollbar-width: thin; scrollbar-color: var(--grid) transparent; }
|
|
190
312
|
#log { max-height: 520px; overflow-y: auto; }
|
|
191
|
-
|
|
313
|
+
.log .row {
|
|
192
314
|
display: flex;
|
|
193
315
|
gap: 8px;
|
|
194
|
-
padding: 5px
|
|
316
|
+
padding: 5px 16px;
|
|
195
317
|
border-top: 1px solid var(--grid);
|
|
196
318
|
font-size: 12px;
|
|
197
319
|
align-items: baseline;
|
|
198
320
|
}
|
|
199
|
-
#log .
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
321
|
+
#log .row { animation: row-in 0.35s ease backwards; }
|
|
322
|
+
@keyframes row-in { from { opacity: 0; transform: translateX(4px); } }
|
|
323
|
+
.log .time {
|
|
324
|
+
color: var(--ink-muted);
|
|
325
|
+
font-family: var(--font-mono);
|
|
326
|
+
font-size: 11px;
|
|
327
|
+
white-space: nowrap;
|
|
328
|
+
}
|
|
329
|
+
.log .name { flex: 1; color: var(--ink); word-break: break-all; }
|
|
330
|
+
.log .dur {
|
|
331
|
+
color: var(--ink-secondary);
|
|
332
|
+
font-family: var(--font-mono);
|
|
333
|
+
font-size: 11px;
|
|
334
|
+
white-space: nowrap;
|
|
335
|
+
}
|
|
336
|
+
.log .row.error { box-shadow: inset 2px 0 0 var(--critical); }
|
|
337
|
+
.log .row.error .name { color: var(--critical); }
|
|
338
|
+
.log .row.lifecycle .name { color: var(--ink-secondary); }
|
|
339
|
+
.log .err-msg { color: var(--critical); }
|
|
340
|
+
.log .detail {
|
|
341
|
+
font-family: var(--font-mono);
|
|
342
|
+
font-size: 11px;
|
|
343
|
+
color: var(--ink-secondary);
|
|
344
|
+
word-break: break-all;
|
|
345
|
+
margin-top: 1px;
|
|
346
|
+
}
|
|
347
|
+
.log .detail .k { color: var(--ink-muted); }
|
|
348
|
+
|
|
349
|
+
/* Inspector side panel */
|
|
350
|
+
#inspector {
|
|
351
|
+
position: fixed;
|
|
352
|
+
top: 0;
|
|
353
|
+
right: 0;
|
|
354
|
+
bottom: 0;
|
|
355
|
+
width: 380px;
|
|
356
|
+
max-width: 92vw;
|
|
357
|
+
background: var(--surface);
|
|
358
|
+
border-left: 1px solid var(--border);
|
|
359
|
+
box-shadow: var(--overlay-shadow);
|
|
360
|
+
transform: translateX(100%);
|
|
361
|
+
visibility: hidden;
|
|
362
|
+
transition: transform 0.2s ease, visibility 0.2s;
|
|
363
|
+
z-index: 20;
|
|
364
|
+
display: flex;
|
|
365
|
+
flex-direction: column;
|
|
366
|
+
}
|
|
367
|
+
#inspector.open { transform: none; visibility: visible; }
|
|
368
|
+
.insp-head {
|
|
369
|
+
display: flex;
|
|
370
|
+
align-items: flex-start;
|
|
371
|
+
gap: 10px;
|
|
372
|
+
padding: 16px;
|
|
373
|
+
border-bottom: 1px solid var(--grid);
|
|
374
|
+
}
|
|
375
|
+
.insp-head .insp-name { font-size: 16px; font-weight: 600; letter-spacing: -0.01em; word-break: break-all; }
|
|
376
|
+
.insp-head .insp-status { font-size: 12px; color: var(--ink-secondary); margin-top: 2px; }
|
|
377
|
+
.insp-head .insp-status .glyph { font-size: 10px; }
|
|
378
|
+
#insp-close {
|
|
379
|
+
margin-left: auto;
|
|
380
|
+
background: none;
|
|
381
|
+
border: 1px solid var(--border);
|
|
382
|
+
border-radius: 999px;
|
|
383
|
+
color: var(--ink-secondary);
|
|
384
|
+
font: inherit;
|
|
385
|
+
font-size: 14px;
|
|
386
|
+
line-height: 1;
|
|
387
|
+
padding: 5px 9px;
|
|
388
|
+
cursor: pointer;
|
|
389
|
+
transition: color 0.15s ease, border-color 0.15s ease;
|
|
390
|
+
}
|
|
391
|
+
#insp-close:hover { color: var(--ink); border-color: var(--ink-faint); }
|
|
392
|
+
#insp-close:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
393
|
+
.insp-body { flex: 1; overflow-y: auto; padding-bottom: 12px; }
|
|
394
|
+
.insp-body h3 {
|
|
395
|
+
font-size: 10.5px;
|
|
396
|
+
letter-spacing: 0.08em;
|
|
397
|
+
text-transform: uppercase;
|
|
398
|
+
color: var(--ink-muted);
|
|
399
|
+
font-weight: 600;
|
|
400
|
+
margin: 0;
|
|
401
|
+
padding: 16px 16px 4px;
|
|
402
|
+
}
|
|
403
|
+
#insp-summary { padding-top: 10px; }
|
|
404
|
+
.i-row {
|
|
405
|
+
display: flex;
|
|
406
|
+
justify-content: space-between;
|
|
407
|
+
padding: 2px 16px;
|
|
408
|
+
font-size: 12px;
|
|
409
|
+
color: var(--ink-secondary);
|
|
410
|
+
}
|
|
411
|
+
.i-row b {
|
|
412
|
+
color: var(--ink);
|
|
413
|
+
font-weight: 500;
|
|
414
|
+
font-family: var(--font-mono);
|
|
415
|
+
font-size: 11.5px;
|
|
416
|
+
}
|
|
417
|
+
#insp-methods table { font-size: 12px; }
|
|
418
|
+
#insp-methods thead th { font-size: 10px; }
|
|
419
|
+
.empty-note {
|
|
420
|
+
padding: 4px 16px 6px;
|
|
421
|
+
color: var(--ink-muted);
|
|
422
|
+
font-family: var(--font-serif);
|
|
423
|
+
font-style: italic;
|
|
424
|
+
font-size: 12px;
|
|
425
|
+
}
|
|
205
426
|
|
|
206
427
|
#tooltip {
|
|
207
428
|
position: fixed;
|
|
208
429
|
display: none;
|
|
209
430
|
background: var(--surface);
|
|
210
431
|
border: 1px solid var(--border);
|
|
211
|
-
border-radius:
|
|
212
|
-
box-shadow: 0
|
|
213
|
-
padding:
|
|
432
|
+
border-radius: 8px;
|
|
433
|
+
box-shadow: 0 6px 20px rgba(20, 20, 19, 0.18);
|
|
434
|
+
padding: 10px 12px;
|
|
214
435
|
font-size: 12px;
|
|
215
436
|
pointer-events: none;
|
|
216
437
|
z-index: 10;
|
|
217
438
|
max-width: 280px;
|
|
218
439
|
}
|
|
219
|
-
#tooltip .t-name { font-weight:
|
|
440
|
+
#tooltip .t-name { font-weight: 600; margin-bottom: 4px; }
|
|
220
441
|
#tooltip .t-row { display: flex; justify-content: space-between; gap: 16px; color: var(--ink-secondary); }
|
|
221
|
-
#tooltip .t-row b {
|
|
442
|
+
#tooltip .t-row b {
|
|
443
|
+
color: var(--ink);
|
|
444
|
+
font-weight: 500;
|
|
445
|
+
font-family: var(--font-mono);
|
|
446
|
+
font-size: 11.5px;
|
|
447
|
+
}
|
|
222
448
|
|
|
223
449
|
@media (max-width: 900px) { main { grid-template-columns: 1fr; } }
|
|
450
|
+
@media (prefers-reduced-motion: reduce) {
|
|
451
|
+
*, *::before, *::after { animation: none !important; transition: none !important; }
|
|
452
|
+
}
|
|
224
453
|
</style>
|
|
225
454
|
</head>
|
|
226
455
|
<body>
|
|
227
456
|
<header>
|
|
457
|
+
<svg class="mark" width="22" height="22" viewBox="0 0 22 22" aria-hidden="true">
|
|
458
|
+
<path d="M7 6.5 L15 11 M7 15.5 L15 11" stroke="var(--ink-faint)" stroke-width="1.5" fill="none"/>
|
|
459
|
+
<circle cx="6" cy="6" r="3" fill="#6a9bcc"/>
|
|
460
|
+
<circle cx="6" cy="16" r="3" fill="#788c5d"/>
|
|
461
|
+
<circle cx="16" cy="11" r="3.5" fill="#d97757"/>
|
|
462
|
+
</svg>
|
|
228
463
|
<h1>composed-di</h1>
|
|
229
464
|
<span class="sub">service dashboard</span>
|
|
230
465
|
<span id="conn" class="down">connecting…</span>
|
|
@@ -243,15 +478,15 @@ const HTML = `<!doctype html>
|
|
|
243
478
|
<h2>Dependency graph</h2>
|
|
244
479
|
<div id="graph-host"><div id="graph-empty">Waiting for module…</div></div>
|
|
245
480
|
<div class="legend">
|
|
246
|
-
<span><span class="glyph" style="color:var(--
|
|
247
|
-
<span><span class="glyph" style="color:var(--warning)">◐</span> initializing</span>
|
|
481
|
+
<span><span class="glyph" style="color:var(--busy)">◐</span> initializing</span>
|
|
248
482
|
<span><span class="glyph" style="color:var(--good)">●</span> ready</span>
|
|
249
483
|
<span><span class="glyph" style="color:var(--critical)">✕</span> error</span>
|
|
250
484
|
<span><span class="glyph" style="color:var(--ink-muted)">◌</span> disposed</span>
|
|
251
485
|
<span><span class="glyph" style="color:var(--accent)">━</span> edge / node flash = live method call</span>
|
|
486
|
+
<span class="hint">click a node to inspect its calls</span>
|
|
252
487
|
</div>
|
|
253
488
|
</div>
|
|
254
|
-
<div class="panel" style="margin-top:
|
|
489
|
+
<div class="panel" style="margin-top:14px">
|
|
255
490
|
<h2>Services</h2>
|
|
256
491
|
<table>
|
|
257
492
|
<thead><tr>
|
|
@@ -265,9 +500,25 @@ const HTML = `<!doctype html>
|
|
|
265
500
|
</div>
|
|
266
501
|
<div class="panel">
|
|
267
502
|
<h2>Event log</h2>
|
|
268
|
-
<div id="log"></div>
|
|
503
|
+
<div id="log" class="log"></div>
|
|
269
504
|
</div>
|
|
270
505
|
</main>
|
|
506
|
+
<aside id="inspector" aria-label="Service inspector">
|
|
507
|
+
<div class="insp-head">
|
|
508
|
+
<div>
|
|
509
|
+
<div class="insp-name" id="insp-name"></div>
|
|
510
|
+
<div class="insp-status" id="insp-status"></div>
|
|
511
|
+
</div>
|
|
512
|
+
<button id="insp-close" aria-label="Close inspector" title="Close (Esc)">✕</button>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="insp-body">
|
|
515
|
+
<div id="insp-summary"></div>
|
|
516
|
+
<h3>Methods</h3>
|
|
517
|
+
<div id="insp-methods"></div>
|
|
518
|
+
<h3>Call history</h3>
|
|
519
|
+
<div id="insp-history" class="log"></div>
|
|
520
|
+
</div>
|
|
521
|
+
</aside>
|
|
271
522
|
<div id="tooltip"></div>
|
|
272
523
|
|
|
273
524
|
<script>
|
|
@@ -288,8 +539,12 @@ const HTML = `<!doctype html>
|
|
|
288
539
|
};
|
|
289
540
|
var NODE_W = 176, NODE_H = 56, COL_W = 248, ROW_H = 84, PAD = 24;
|
|
290
541
|
var MAX_LOG_ROWS = 200;
|
|
542
|
+
var MAX_HISTORY = 100; /* completed calls kept per service for the inspector */
|
|
291
543
|
|
|
292
544
|
var state = { nodes: [], edges: [], services: {} };
|
|
545
|
+
var selectedService = null; /* service whose table row is expanded */
|
|
546
|
+
var inspected = null; /* service shown in the inspector side panel */
|
|
547
|
+
var callHistory = {}; /* service name -> completed calls, newest first */
|
|
293
548
|
var nodeEls = {}; /* service name -> { group, statusText, glyph } */
|
|
294
549
|
var edgeEls = {}; /* "from\\u0000to" -> path element */
|
|
295
550
|
var flashTimers = {};
|
|
@@ -310,9 +565,15 @@ const HTML = `<!doctype html>
|
|
|
310
565
|
|
|
311
566
|
/* ---------- graph layout: rank = longest path to a leaf ---------- */
|
|
312
567
|
|
|
313
|
-
|
|
568
|
+
/** Only services whose initialization has at least started are drawn. */
|
|
569
|
+
function isShown(name) {
|
|
570
|
+
var stats = state.services[name];
|
|
571
|
+
return !!stats && stats.status !== 'pending';
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function computeRanks(nodes) {
|
|
314
575
|
var deps = {};
|
|
315
|
-
|
|
576
|
+
nodes.forEach(function (n) { deps[n.name] = []; });
|
|
316
577
|
state.edges.forEach(function (e) {
|
|
317
578
|
if (deps[e.from] && e.to in deps) deps[e.from].push(e.to);
|
|
318
579
|
});
|
|
@@ -327,7 +588,7 @@ const HTML = `<!doctype html>
|
|
|
327
588
|
ranks[name] = r;
|
|
328
589
|
return r;
|
|
329
590
|
}
|
|
330
|
-
|
|
591
|
+
nodes.forEach(function (n) { rankOf(n.name); });
|
|
331
592
|
return ranks;
|
|
332
593
|
}
|
|
333
594
|
|
|
@@ -336,18 +597,21 @@ const HTML = `<!doctype html>
|
|
|
336
597
|
host.textContent = '';
|
|
337
598
|
nodeEls = {};
|
|
338
599
|
edgeEls = {};
|
|
339
|
-
|
|
600
|
+
var shown = state.nodes.filter(function (n) { return isShown(n.name); });
|
|
601
|
+
if (!shown.length) {
|
|
340
602
|
var empty = document.createElement('div');
|
|
341
603
|
empty.id = 'graph-empty';
|
|
342
|
-
empty.textContent =
|
|
604
|
+
empty.textContent = state.nodes.length
|
|
605
|
+
? 'No services initialized yet \\u2014 nodes appear when initialization starts.'
|
|
606
|
+
: 'No services yet \\u2014 waiting for an application to connect.';
|
|
343
607
|
host.appendChild(empty);
|
|
344
608
|
return;
|
|
345
609
|
}
|
|
346
610
|
|
|
347
|
-
var ranks = computeRanks();
|
|
611
|
+
var ranks = computeRanks(shown);
|
|
348
612
|
var columns = {};
|
|
349
613
|
var maxRank = 0;
|
|
350
|
-
|
|
614
|
+
shown.forEach(function (n) {
|
|
351
615
|
var r = ranks[n.name];
|
|
352
616
|
maxRank = Math.max(maxRank, r);
|
|
353
617
|
(columns[r] = columns[r] || []).push(n.name);
|
|
@@ -408,7 +672,7 @@ const HTML = `<!doctype html>
|
|
|
408
672
|
edgeEls[e.from + '\\u0000' + e.to] = path;
|
|
409
673
|
});
|
|
410
674
|
|
|
411
|
-
|
|
675
|
+
shown.forEach(function (n) {
|
|
412
676
|
var p = pos[n.name];
|
|
413
677
|
var g = svgEl('g');
|
|
414
678
|
g.setAttribute('transform', 'translate(' + p.x + ' ' + p.y + ')');
|
|
@@ -416,7 +680,7 @@ const HTML = `<!doctype html>
|
|
|
416
680
|
var rect = svgEl('rect');
|
|
417
681
|
rect.setAttribute('width', NODE_W);
|
|
418
682
|
rect.setAttribute('height', NODE_H);
|
|
419
|
-
rect.setAttribute('rx',
|
|
683
|
+
rect.setAttribute('rx', 8);
|
|
420
684
|
g.appendChild(rect);
|
|
421
685
|
|
|
422
686
|
var label = svgEl('text');
|
|
@@ -437,8 +701,18 @@ const HTML = `<!doctype html>
|
|
|
437
701
|
status.setAttribute('y', 42);
|
|
438
702
|
g.appendChild(status);
|
|
439
703
|
|
|
704
|
+
g.setAttribute('tabindex', '0');
|
|
705
|
+
g.setAttribute('role', 'button');
|
|
706
|
+
g.setAttribute('aria-label', 'Inspect ' + n.name);
|
|
440
707
|
g.addEventListener('mousemove', function (ev) { showTooltip(n.name, ev); });
|
|
441
708
|
g.addEventListener('mouseleave', hideTooltip);
|
|
709
|
+
g.addEventListener('click', function () { inspect(n.name); });
|
|
710
|
+
g.addEventListener('keydown', function (ev) {
|
|
711
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
712
|
+
ev.preventDefault();
|
|
713
|
+
inspect(n.name);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
442
716
|
|
|
443
717
|
svg.appendChild(g);
|
|
444
718
|
nodeEls[n.name] = { group: g, statusText: status, glyph: glyph };
|
|
@@ -453,7 +727,9 @@ const HTML = `<!doctype html>
|
|
|
453
727
|
var stats = state.services[name];
|
|
454
728
|
if (!el || !stats) return;
|
|
455
729
|
var flashing = el.group.classList.contains('flash');
|
|
456
|
-
el.group.setAttribute('class', 'node ' + stats.status
|
|
730
|
+
el.group.setAttribute('class', 'node ' + stats.status
|
|
731
|
+
+ (flashing ? ' flash' : '')
|
|
732
|
+
+ (name === inspected ? ' selected' : ''));
|
|
457
733
|
el.glyph.textContent = GLYPHS[stats.status] || '';
|
|
458
734
|
var line = stats.status;
|
|
459
735
|
if (stats.calls > 0) {
|
|
@@ -533,24 +809,43 @@ const HTML = `<!doctype html>
|
|
|
533
809
|
$('tile-calls').textContent = String(calls);
|
|
534
810
|
$('tile-avg').textContent = calls > 0 ? fmtMs(callMs / calls) + ' ms' : '–';
|
|
535
811
|
$('tile-errors').textContent = String(errors);
|
|
812
|
+
$('tile-errors').className = 'value' + (errors > 0 ? ' bad' : '');
|
|
536
813
|
|
|
537
814
|
var tbody = $('service-rows');
|
|
538
815
|
tbody.textContent = '';
|
|
539
816
|
names.sort().forEach(function (n) {
|
|
540
817
|
var s = state.services[n];
|
|
818
|
+
var selected = n === selectedService;
|
|
541
819
|
var tr = document.createElement('tr');
|
|
820
|
+
tr.className = 'svc-row' + (selected ? ' selected' : '');
|
|
821
|
+
tr.tabIndex = 0;
|
|
822
|
+
tr.setAttribute('role', 'button');
|
|
823
|
+
tr.setAttribute('aria-expanded', selected ? 'true' : 'false');
|
|
824
|
+
tr.addEventListener('click', function () { toggleService(n); });
|
|
825
|
+
tr.addEventListener('keydown', function (ev) {
|
|
826
|
+
if (ev.key === 'Enter' || ev.key === ' ') {
|
|
827
|
+
ev.preventDefault();
|
|
828
|
+
toggleService(n);
|
|
829
|
+
}
|
|
830
|
+
});
|
|
542
831
|
function cell(text, cls) {
|
|
543
832
|
var td = document.createElement('td');
|
|
544
833
|
if (cls) td.className = cls;
|
|
545
834
|
td.textContent = text;
|
|
546
835
|
return td;
|
|
547
836
|
}
|
|
548
|
-
|
|
837
|
+
var nameCell = cell('');
|
|
838
|
+
var caret = document.createElement('span');
|
|
839
|
+
caret.className = 'caret';
|
|
840
|
+
caret.textContent = selected ? '\\u25BE' : '\\u25B8'; /* ▾ / ▸ */
|
|
841
|
+
nameCell.appendChild(caret);
|
|
842
|
+
nameCell.appendChild(document.createTextNode(n));
|
|
843
|
+
tr.appendChild(nameCell);
|
|
549
844
|
var status = cell('');
|
|
550
845
|
var glyph = document.createElement('span');
|
|
551
846
|
glyph.className = 'glyph';
|
|
552
847
|
glyph.textContent = GLYPHS[s.status] + ' ';
|
|
553
|
-
var color = { ready: 'var(--good)', initializing: 'var(--
|
|
848
|
+
var color = { ready: 'var(--good)', initializing: 'var(--busy)', error: 'var(--critical)' }[s.status];
|
|
554
849
|
glyph.style.color = color || 'var(--ink-muted)';
|
|
555
850
|
status.appendChild(glyph);
|
|
556
851
|
status.appendChild(document.createTextNode(s.status));
|
|
@@ -558,11 +853,222 @@ const HTML = `<!doctype html>
|
|
|
558
853
|
tr.appendChild(cell(fmtMs(s.initMs), 'num'));
|
|
559
854
|
tr.appendChild(cell(String(s.calls), 'num'));
|
|
560
855
|
tr.appendChild(cell(s.calls > 0 ? fmtMs(s.totalCallMs / s.calls) : '–', 'num'));
|
|
561
|
-
tr.appendChild(cell(String(s.errors), 'num'));
|
|
856
|
+
tr.appendChild(cell(String(s.errors), 'num' + (s.errors > 0 ? ' err' : '')));
|
|
562
857
|
tbody.appendChild(tr);
|
|
858
|
+
if (selected) tbody.appendChild(methodDetailRow(s));
|
|
563
859
|
});
|
|
564
860
|
}
|
|
565
861
|
|
|
862
|
+
/* ---------- per-method drill-down ---------- */
|
|
863
|
+
|
|
864
|
+
function toggleService(name) {
|
|
865
|
+
selectedService = selectedService === name ? null : name;
|
|
866
|
+
renderSummary();
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/** A per-method stats table for a service, or null when it has none. */
|
|
870
|
+
function methodsTable(methods) {
|
|
871
|
+
var names = Object.keys(methods);
|
|
872
|
+
if (!names.length) return null;
|
|
873
|
+
names.sort(function (a, b) {
|
|
874
|
+
return methods[b].calls - methods[a].calls || a.localeCompare(b);
|
|
875
|
+
});
|
|
876
|
+
var table = document.createElement('table');
|
|
877
|
+
var thead = document.createElement('thead');
|
|
878
|
+
var headRow = document.createElement('tr');
|
|
879
|
+
[['Method'], ['Calls', 'num'], ['Avg ms', 'num'], ['Last ms', 'num'], ['Errors', 'num']]
|
|
880
|
+
.forEach(function (h) {
|
|
881
|
+
var th = document.createElement('th');
|
|
882
|
+
if (h[1]) th.className = h[1];
|
|
883
|
+
th.textContent = h[0];
|
|
884
|
+
headRow.appendChild(th);
|
|
885
|
+
});
|
|
886
|
+
thead.appendChild(headRow);
|
|
887
|
+
table.appendChild(thead);
|
|
888
|
+
|
|
889
|
+
var body = document.createElement('tbody');
|
|
890
|
+
names.forEach(function (m) {
|
|
891
|
+
var stat = methods[m];
|
|
892
|
+
var row = document.createElement('tr');
|
|
893
|
+
function cell(text, cls) {
|
|
894
|
+
var c = document.createElement('td');
|
|
895
|
+
if (cls) c.className = cls;
|
|
896
|
+
c.textContent = text;
|
|
897
|
+
return c;
|
|
898
|
+
}
|
|
899
|
+
row.appendChild(cell(m));
|
|
900
|
+
row.appendChild(cell(String(stat.calls), 'num'));
|
|
901
|
+
row.appendChild(cell(fmtMs(stat.totalMs / stat.calls), 'num'));
|
|
902
|
+
row.appendChild(cell(fmtMs(stat.lastMs), 'num'));
|
|
903
|
+
row.appendChild(cell(String(stat.errors), 'num' + (stat.errors > 0 ? ' err' : '')));
|
|
904
|
+
body.appendChild(row);
|
|
905
|
+
});
|
|
906
|
+
table.appendChild(body);
|
|
907
|
+
return table;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** The expanded row under a selected service: its method-call breakdown. */
|
|
911
|
+
function methodDetailRow(stats) {
|
|
912
|
+
var tr = document.createElement('tr');
|
|
913
|
+
tr.className = 'method-detail';
|
|
914
|
+
var td = document.createElement('td');
|
|
915
|
+
td.colSpan = 6;
|
|
916
|
+
var table = methodsTable(stats.methods || {});
|
|
917
|
+
if (table) {
|
|
918
|
+
var wrap = document.createElement('div');
|
|
919
|
+
wrap.className = 'methods';
|
|
920
|
+
wrap.appendChild(table);
|
|
921
|
+
td.appendChild(wrap);
|
|
922
|
+
} else {
|
|
923
|
+
var empty = document.createElement('div');
|
|
924
|
+
empty.className = 'empty';
|
|
925
|
+
empty.textContent = 'No method calls yet.';
|
|
926
|
+
td.appendChild(empty);
|
|
927
|
+
}
|
|
928
|
+
tr.appendChild(td);
|
|
929
|
+
return tr;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/* ---------- inspector side panel ---------- */
|
|
933
|
+
|
|
934
|
+
/** Opens the panel for a service; same service (or null) closes it. */
|
|
935
|
+
function inspect(name) {
|
|
936
|
+
inspected = (name === null || inspected === name) ? null : name;
|
|
937
|
+
renderInspector();
|
|
938
|
+
Object.keys(nodeEls).forEach(function (n) { applyNodeState(n); });
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function renderInspector() {
|
|
942
|
+
var panel = $('inspector');
|
|
943
|
+
var stats = inspected ? state.services[inspected] : null;
|
|
944
|
+
if (!stats) {
|
|
945
|
+
inspected = null;
|
|
946
|
+
panel.classList.remove('open');
|
|
947
|
+
return;
|
|
948
|
+
}
|
|
949
|
+
panel.classList.add('open');
|
|
950
|
+
$('insp-name').textContent = inspected;
|
|
951
|
+
|
|
952
|
+
var status = $('insp-status');
|
|
953
|
+
status.textContent = '';
|
|
954
|
+
var glyph = document.createElement('span');
|
|
955
|
+
glyph.className = 'glyph';
|
|
956
|
+
glyph.textContent = GLYPHS[stats.status] + ' ';
|
|
957
|
+
var color = { ready: 'var(--good)', initializing: 'var(--busy)', error: 'var(--critical)' }[stats.status];
|
|
958
|
+
glyph.style.color = color || 'var(--ink-muted)';
|
|
959
|
+
status.appendChild(glyph);
|
|
960
|
+
status.appendChild(document.createTextNode(stats.status));
|
|
961
|
+
|
|
962
|
+
var summary = $('insp-summary');
|
|
963
|
+
summary.textContent = '';
|
|
964
|
+
var avg = stats.calls > 0 ? stats.totalCallMs / stats.calls : null;
|
|
965
|
+
[
|
|
966
|
+
['Init', stats.initMs == null ? '–' : fmtMs(stats.initMs) + ' ms'],
|
|
967
|
+
['Calls', String(stats.calls)],
|
|
968
|
+
['Avg call', avg == null ? '–' : fmtMs(avg) + ' ms'],
|
|
969
|
+
['Errors', String(stats.errors)]
|
|
970
|
+
].forEach(function (pair) {
|
|
971
|
+
var row = document.createElement('div');
|
|
972
|
+
row.className = 'i-row';
|
|
973
|
+
var k = document.createElement('span');
|
|
974
|
+
k.textContent = pair[0];
|
|
975
|
+
var v = document.createElement('b');
|
|
976
|
+
v.textContent = pair[1];
|
|
977
|
+
row.appendChild(k);
|
|
978
|
+
row.appendChild(v);
|
|
979
|
+
summary.appendChild(row);
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
var methodsHost = $('insp-methods');
|
|
983
|
+
methodsHost.textContent = '';
|
|
984
|
+
var table = methodsTable(stats.methods || {});
|
|
985
|
+
if (table) methodsHost.appendChild(table);
|
|
986
|
+
else methodsHost.appendChild(emptyNote('No method calls yet.'));
|
|
987
|
+
|
|
988
|
+
var historyHost = $('insp-history');
|
|
989
|
+
historyHost.textContent = '';
|
|
990
|
+
var entries = callHistory[inspected] || [];
|
|
991
|
+
if (!entries.length) {
|
|
992
|
+
historyHost.appendChild(emptyNote('No calls recorded yet.'));
|
|
993
|
+
} else {
|
|
994
|
+
entries.forEach(function (entry) {
|
|
995
|
+
historyHost.appendChild(historyRow(entry));
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function emptyNote(text) {
|
|
1001
|
+
var div = document.createElement('div');
|
|
1002
|
+
div.className = 'empty-note';
|
|
1003
|
+
div.textContent = text;
|
|
1004
|
+
return div;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function historyRow(entry) {
|
|
1008
|
+
var row = document.createElement('div');
|
|
1009
|
+
row.className = 'row' + (entry.error ? ' error' : '');
|
|
1010
|
+
|
|
1011
|
+
var time = document.createElement('span');
|
|
1012
|
+
time.className = 'time';
|
|
1013
|
+
time.textContent = fmtTime(entry.time);
|
|
1014
|
+
row.appendChild(time);
|
|
1015
|
+
|
|
1016
|
+
var name = document.createElement('span');
|
|
1017
|
+
name.className = 'name';
|
|
1018
|
+
var text = entry.method;
|
|
1019
|
+
if (entry.parentService && entry.parentService !== inspected) {
|
|
1020
|
+
text = entry.parentService + ' \\u2192 ' + text;
|
|
1021
|
+
}
|
|
1022
|
+
name.textContent = text;
|
|
1023
|
+
/* "[]" (no arguments) and "undefined" (void return) carry no signal. */
|
|
1024
|
+
if (entry.args != null && entry.args !== '[]') {
|
|
1025
|
+
name.appendChild(detailLine('args', entry.args));
|
|
1026
|
+
}
|
|
1027
|
+
if (entry.result != null && entry.result !== 'undefined') {
|
|
1028
|
+
name.appendChild(detailLine('\\u2192', entry.result));
|
|
1029
|
+
}
|
|
1030
|
+
if (entry.error) {
|
|
1031
|
+
var err = document.createElement('div');
|
|
1032
|
+
err.className = 'err-msg';
|
|
1033
|
+
err.textContent = '\\u2715 ' + entry.error;
|
|
1034
|
+
name.appendChild(err);
|
|
1035
|
+
}
|
|
1036
|
+
row.appendChild(name);
|
|
1037
|
+
|
|
1038
|
+
var dur = document.createElement('span');
|
|
1039
|
+
dur.className = 'dur';
|
|
1040
|
+
dur.textContent = fmtMs(entry.durationMs) + ' ms';
|
|
1041
|
+
row.appendChild(dur);
|
|
1042
|
+
return row;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function detailLine(label, text) {
|
|
1046
|
+
var div = document.createElement('div');
|
|
1047
|
+
div.className = 'detail';
|
|
1048
|
+
var k = document.createElement('span');
|
|
1049
|
+
k.className = 'k';
|
|
1050
|
+
k.textContent = label + ' ';
|
|
1051
|
+
div.appendChild(k);
|
|
1052
|
+
div.appendChild(document.createTextNode(text));
|
|
1053
|
+
return div;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/** Keeps the per-service history the inspector shows, newest first. */
|
|
1057
|
+
function recordCall(wire) {
|
|
1058
|
+
if (wire.kind !== 'call' || wire.span.type !== 'end' || !wire.service) return;
|
|
1059
|
+
var list = callHistory[wire.service] = callHistory[wire.service] || [];
|
|
1060
|
+
list.unshift({
|
|
1061
|
+
method: wire.method,
|
|
1062
|
+
parentService: wire.parentService,
|
|
1063
|
+
time: wire.span.time,
|
|
1064
|
+
durationMs: wire.span.durationMs,
|
|
1065
|
+
error: wire.span.error,
|
|
1066
|
+
args: wire.args,
|
|
1067
|
+
result: wire.span.result != null ? wire.span.result : null
|
|
1068
|
+
});
|
|
1069
|
+
if (list.length > MAX_HISTORY) list.length = MAX_HISTORY;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
566
1072
|
/* ---------- event log ---------- */
|
|
567
1073
|
|
|
568
1074
|
function logEvent(wire) {
|
|
@@ -610,10 +1116,13 @@ const HTML = `<!doctype html>
|
|
|
610
1116
|
|
|
611
1117
|
function applyEvent(wire) {
|
|
612
1118
|
if (wire.service && wire.stats) {
|
|
613
|
-
var
|
|
1119
|
+
var wasDrawn = wire.service in nodeEls;
|
|
614
1120
|
state.services[wire.service] = wire.stats;
|
|
615
|
-
if (
|
|
616
|
-
|
|
1121
|
+
if (isShown(wire.service) !== wasDrawn) {
|
|
1122
|
+
renderGraph(); /* the node just became visible (or vanished) */
|
|
1123
|
+
} else {
|
|
1124
|
+
applyNodeState(wire.service);
|
|
1125
|
+
}
|
|
617
1126
|
}
|
|
618
1127
|
if (wire.kind === 'call' && wire.service) {
|
|
619
1128
|
var el = nodeEls[wire.service];
|
|
@@ -622,8 +1131,10 @@ const HTML = `<!doctype html>
|
|
|
622
1131
|
flash(edgeEls[wire.parentService + '\\u0000' + wire.service], 'e:' + wire.parentService + '>' + wire.service);
|
|
623
1132
|
}
|
|
624
1133
|
}
|
|
1134
|
+
recordCall(wire);
|
|
625
1135
|
logEvent(wire);
|
|
626
1136
|
renderSummary();
|
|
1137
|
+
if (inspected && wire.service === inspected) renderInspector();
|
|
627
1138
|
}
|
|
628
1139
|
|
|
629
1140
|
function connect() {
|
|
@@ -643,8 +1154,12 @@ const HTML = `<!doctype html>
|
|
|
643
1154
|
state.nodes = snap.nodes;
|
|
644
1155
|
state.edges = snap.edges;
|
|
645
1156
|
state.services = snap.services;
|
|
1157
|
+
callHistory = {};
|
|
1158
|
+
/* recent is oldest-first; recordCall unshifts, so newest ends up first */
|
|
1159
|
+
snap.recent.forEach(recordCall);
|
|
646
1160
|
renderGraph();
|
|
647
1161
|
renderSummary();
|
|
1162
|
+
renderInspector(); /* refreshes, or closes if the service is gone */
|
|
648
1163
|
$('log').textContent = '';
|
|
649
1164
|
snap.recent.forEach(logEvent);
|
|
650
1165
|
});
|
|
@@ -653,6 +1168,11 @@ const HTML = `<!doctype html>
|
|
|
653
1168
|
});
|
|
654
1169
|
}
|
|
655
1170
|
|
|
1171
|
+
$('insp-close').addEventListener('click', function () { inspect(null); });
|
|
1172
|
+
document.addEventListener('keydown', function (ev) {
|
|
1173
|
+
if (ev.key === 'Escape') inspect(null);
|
|
1174
|
+
});
|
|
1175
|
+
|
|
656
1176
|
connect();
|
|
657
1177
|
})();
|
|
658
1178
|
</script>
|