@composed-di/observability 0.5.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.
Files changed (41) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +50 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/dashboardClient.d.ts +67 -0
  6. package/dist/dashboardClient.d.ts.map +1 -0
  7. package/dist/dashboardClient.js +168 -0
  8. package/dist/dashboardClient.js.map +1 -0
  9. package/dist/dashboardEventListener.d.ts +32 -0
  10. package/dist/dashboardEventListener.d.ts.map +1 -0
  11. package/dist/dashboardEventListener.js +88 -0
  12. package/dist/dashboardEventListener.js.map +1 -0
  13. package/dist/dashboardHtml.d.ts +11 -0
  14. package/dist/dashboardHtml.d.ts.map +1 -0
  15. package/dist/dashboardHtml.js +662 -0
  16. package/dist/dashboardHtml.js.map +1 -0
  17. package/dist/dashboardServer.d.ts +99 -0
  18. package/dist/dashboardServer.d.ts.map +1 -0
  19. package/dist/dashboardServer.js +360 -0
  20. package/dist/dashboardServer.js.map +1 -0
  21. package/dist/events.d.ts +80 -0
  22. package/dist/events.d.ts.map +1 -0
  23. package/dist/events.js +7 -0
  24. package/dist/events.js.map +1 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +22 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/moduleGraph.d.ts +13 -0
  30. package/dist/moduleGraph.d.ts.map +1 -0
  31. package/dist/moduleGraph.js +25 -0
  32. package/dist/moduleGraph.js.map +1 -0
  33. package/package.json +47 -0
  34. package/src/cli.ts +42 -0
  35. package/src/dashboardClient.ts +189 -0
  36. package/src/dashboardEventListener.ts +104 -0
  37. package/src/dashboardHtml.ts +659 -0
  38. package/src/dashboardServer.ts +391 -0
  39. package/src/events.ts +94 -0
  40. package/src/index.ts +5 -0
  41. package/src/moduleGraph.ts +37 -0
@@ -0,0 +1,662 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderDashboardHtml = renderDashboardHtml;
4
+ /**
5
+ * The dashboard page, served at "/". Self-contained: no build step, no CDN.
6
+ *
7
+ * Connects to "/events" (Server-Sent Events), receives a full snapshot, then
8
+ * live span events. Renders the dependency graph as SVG (services laid out in
9
+ * dependency ranks, dependencies on the left), a stat-tile row, a services
10
+ * table, and an event log. Service state is shown as a status glyph + word,
11
+ * never color alone.
12
+ */
13
+ function renderDashboardHtml() {
14
+ return HTML;
15
+ }
16
+ const HTML = `<!doctype html>
17
+ <html lang="en">
18
+ <head>
19
+ <meta charset="utf-8">
20
+ <meta name="viewport" content="width=device-width, initial-scale=1">
21
+ <title>composed-di dashboard</title>
22
+ <style>
23
+ :root {
24
+ --page: #f9f9f7;
25
+ --surface: #fcfcfb;
26
+ --ink: #0b0b0b;
27
+ --ink-secondary: #52514e;
28
+ --ink-muted: #898781;
29
+ --grid: #e1e0d9;
30
+ --border: rgba(11, 11, 11, 0.10);
31
+ --accent: #2a78d6;
32
+ --good: #0ca30c;
33
+ --warning: #fab219;
34
+ --critical: #d03b3b;
35
+ }
36
+ /* Dark values apply via the system preference, or ?theme=dark|light. */
37
+ @media (prefers-color-scheme: dark) {
38
+ :root:not([data-theme="light"]) {
39
+ --page: #0d0d0d;
40
+ --surface: #1a1a19;
41
+ --ink: #ffffff;
42
+ --ink-secondary: #c3c2b7;
43
+ --ink-muted: #898781;
44
+ --grid: #2c2c2a;
45
+ --border: rgba(255, 255, 255, 0.10);
46
+ --accent: #3987e5;
47
+ }
48
+ }
49
+ :root[data-theme="dark"] {
50
+ --page: #0d0d0d;
51
+ --surface: #1a1a19;
52
+ --ink: #ffffff;
53
+ --ink-secondary: #c3c2b7;
54
+ --ink-muted: #898781;
55
+ --grid: #2c2c2a;
56
+ --border: rgba(255, 255, 255, 0.10);
57
+ --accent: #3987e5;
58
+ }
59
+ * { box-sizing: border-box; }
60
+ body {
61
+ margin: 0;
62
+ background: var(--page);
63
+ color: var(--ink);
64
+ font: 14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif;
65
+ }
66
+ header {
67
+ display: flex;
68
+ align-items: baseline;
69
+ gap: 12px;
70
+ padding: 16px 20px 0;
71
+ }
72
+ header h1 { font-size: 16px; font-weight: 650; margin: 0; }
73
+ header .sub { color: var(--ink-muted); font-size: 12px; }
74
+ #conn {
75
+ margin-left: auto;
76
+ font-size: 12px;
77
+ color: var(--ink-secondary);
78
+ border: 1px solid var(--border);
79
+ border-radius: 999px;
80
+ padding: 2px 10px;
81
+ }
82
+ #conn.live::before { content: "● "; color: var(--good); }
83
+ #conn.down::before { content: "● "; color: var(--critical); }
84
+
85
+ .tiles {
86
+ display: flex;
87
+ flex-wrap: wrap;
88
+ gap: 12px;
89
+ padding: 14px 20px;
90
+ }
91
+ .tile {
92
+ background: var(--surface);
93
+ border: 1px solid var(--border);
94
+ border-radius: 8px;
95
+ padding: 10px 16px;
96
+ min-width: 132px;
97
+ }
98
+ .tile .label {
99
+ font-size: 11px;
100
+ letter-spacing: 0.06em;
101
+ text-transform: uppercase;
102
+ color: var(--ink-muted);
103
+ }
104
+ .tile .value { font-size: 24px; font-weight: 650; margin-top: 2px; }
105
+ .tile .value small { font-size: 13px; font-weight: 400; color: var(--ink-secondary); }
106
+
107
+ main {
108
+ display: grid;
109
+ grid-template-columns: 1fr 340px;
110
+ gap: 12px;
111
+ padding: 0 20px 20px;
112
+ align-items: start;
113
+ }
114
+ .panel {
115
+ background: var(--surface);
116
+ border: 1px solid var(--border);
117
+ border-radius: 8px;
118
+ overflow: hidden;
119
+ }
120
+ .panel h2 {
121
+ font-size: 12px;
122
+ letter-spacing: 0.06em;
123
+ text-transform: uppercase;
124
+ color: var(--ink-muted);
125
+ font-weight: 600;
126
+ margin: 0;
127
+ padding: 10px 14px;
128
+ border-bottom: 1px solid var(--grid);
129
+ }
130
+ #graph-panel svg { display: block; width: 100%; }
131
+ #graph-empty { padding: 32px; color: var(--ink-muted); text-align: center; }
132
+
133
+ .legend {
134
+ display: flex;
135
+ flex-wrap: wrap;
136
+ gap: 14px;
137
+ padding: 10px 14px;
138
+ border-top: 1px solid var(--grid);
139
+ font-size: 12px;
140
+ color: var(--ink-secondary);
141
+ }
142
+ .legend .glyph { font-size: 11px; }
143
+
144
+ /* Graph marks */
145
+ .edge {
146
+ fill: none;
147
+ stroke: var(--grid);
148
+ stroke-width: 1.5;
149
+ transition: stroke 0.45s ease;
150
+ }
151
+ .edge.active { stroke: var(--accent); stroke-width: 2.5; transition: none; }
152
+ .node rect {
153
+ fill: var(--surface);
154
+ stroke: var(--ink-muted);
155
+ stroke-width: 1.5;
156
+ rx: 6;
157
+ }
158
+ .node text { fill: var(--ink); font-size: 13px; font-weight: 600; }
159
+ .node .status { fill: var(--ink-secondary); font-size: 11px; font-weight: 400; }
160
+ .node .glyph { font-size: 10px; }
161
+ .node.pending rect { stroke: var(--ink-muted); stroke-dasharray: 4 3; }
162
+ .node.pending .glyph { fill: var(--ink-muted); }
163
+ .node.initializing rect { stroke: var(--warning); animation: pulse 1s ease-in-out infinite; }
164
+ .node.initializing .glyph { fill: var(--warning); }
165
+ .node.ready rect { stroke: var(--good); }
166
+ .node.ready .glyph { fill: var(--good); }
167
+ .node.error rect { stroke: var(--critical); stroke-width: 2; }
168
+ .node.error .glyph { fill: var(--critical); }
169
+ .node.disposed rect { stroke: var(--grid); }
170
+ .node.disposed .glyph { fill: var(--ink-muted); }
171
+ .node.disposed text { fill: var(--ink-muted); }
172
+ .node.flash rect { stroke: var(--accent); stroke-width: 2.5; }
173
+ @keyframes pulse { 50% { stroke-opacity: 0.35; } }
174
+
175
+ /* Tables */
176
+ table { width: 100%; border-collapse: collapse; font-size: 13px; }
177
+ th, td { text-align: left; padding: 6px 14px; border-top: 1px solid var(--grid); }
178
+ thead th {
179
+ border-top: none;
180
+ font-size: 11px;
181
+ letter-spacing: 0.06em;
182
+ text-transform: uppercase;
183
+ color: var(--ink-muted);
184
+ font-weight: 600;
185
+ }
186
+ td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
187
+ td .glyph { font-size: 10px; }
188
+
189
+ /* Event log */
190
+ #log { max-height: 520px; overflow-y: auto; }
191
+ #log .row {
192
+ display: flex;
193
+ gap: 8px;
194
+ padding: 5px 14px;
195
+ border-top: 1px solid var(--grid);
196
+ font-size: 12px;
197
+ align-items: baseline;
198
+ }
199
+ #log .time { color: var(--ink-muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
200
+ #log .name { flex: 1; color: var(--ink); word-break: break-all; }
201
+ #log .dur { color: var(--ink-secondary); font-variant-numeric: tabular-nums; white-space: nowrap; }
202
+ #log .row.error .name { color: var(--critical); }
203
+ #log .row.lifecycle .name { color: var(--ink-secondary); }
204
+ #log .err-msg { color: var(--critical); }
205
+
206
+ #tooltip {
207
+ position: fixed;
208
+ display: none;
209
+ background: var(--surface);
210
+ border: 1px solid var(--border);
211
+ border-radius: 6px;
212
+ box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
213
+ padding: 8px 12px;
214
+ font-size: 12px;
215
+ pointer-events: none;
216
+ z-index: 10;
217
+ max-width: 280px;
218
+ }
219
+ #tooltip .t-name { font-weight: 650; margin-bottom: 4px; }
220
+ #tooltip .t-row { display: flex; justify-content: space-between; gap: 16px; color: var(--ink-secondary); }
221
+ #tooltip .t-row b { color: var(--ink); font-weight: 550; font-variant-numeric: tabular-nums; }
222
+
223
+ @media (max-width: 900px) { main { grid-template-columns: 1fr; } }
224
+ </style>
225
+ </head>
226
+ <body>
227
+ <header>
228
+ <h1>composed-di</h1>
229
+ <span class="sub">service dashboard</span>
230
+ <span id="conn" class="down">connecting…</span>
231
+ </header>
232
+
233
+ <div class="tiles">
234
+ <div class="tile"><div class="label">Services ready</div><div class="value" id="tile-ready">–</div></div>
235
+ <div class="tile"><div class="label">Method calls</div><div class="value" id="tile-calls">–</div></div>
236
+ <div class="tile"><div class="label">Avg call</div><div class="value" id="tile-avg">–</div></div>
237
+ <div class="tile"><div class="label">Errors</div><div class="value" id="tile-errors">–</div></div>
238
+ </div>
239
+
240
+ <main>
241
+ <div>
242
+ <div class="panel" id="graph-panel">
243
+ <h2>Dependency graph</h2>
244
+ <div id="graph-host"><div id="graph-empty">Waiting for module…</div></div>
245
+ <div class="legend">
246
+ <span><span class="glyph" style="color:var(--ink-muted)">○</span> pending</span>
247
+ <span><span class="glyph" style="color:var(--warning)">◐</span> initializing</span>
248
+ <span><span class="glyph" style="color:var(--good)">●</span> ready</span>
249
+ <span><span class="glyph" style="color:var(--critical)">✕</span> error</span>
250
+ <span><span class="glyph" style="color:var(--ink-muted)">◌</span> disposed</span>
251
+ <span><span class="glyph" style="color:var(--accent)">━</span> edge / node flash = live method call</span>
252
+ </div>
253
+ </div>
254
+ <div class="panel" style="margin-top:12px">
255
+ <h2>Services</h2>
256
+ <table>
257
+ <thead><tr>
258
+ <th>Service</th><th>Status</th>
259
+ <th class="num">Init ms</th><th class="num">Calls</th>
260
+ <th class="num">Avg ms</th><th class="num">Errors</th>
261
+ </tr></thead>
262
+ <tbody id="service-rows"></tbody>
263
+ </table>
264
+ </div>
265
+ </div>
266
+ <div class="panel">
267
+ <h2>Event log</h2>
268
+ <div id="log"></div>
269
+ </div>
270
+ </main>
271
+ <div id="tooltip"></div>
272
+
273
+ <script>
274
+ (function () {
275
+ 'use strict';
276
+
277
+ var theme = new URLSearchParams(location.search).get('theme');
278
+ if (theme === 'dark' || theme === 'light') {
279
+ document.documentElement.setAttribute('data-theme', theme);
280
+ }
281
+
282
+ var GLYPHS = {
283
+ pending: '\\u25CB', /* ○ */
284
+ initializing: '\\u25D0', /* ◐ */
285
+ ready: '\\u25CF', /* ● */
286
+ error: '\\u2715', /* ✕ */
287
+ disposed: '\\u25CC' /* ◌ */
288
+ };
289
+ var NODE_W = 176, NODE_H = 56, COL_W = 248, ROW_H = 84, PAD = 24;
290
+ var MAX_LOG_ROWS = 200;
291
+
292
+ var state = { nodes: [], edges: [], services: {} };
293
+ var nodeEls = {}; /* service name -> { group, statusText, glyph } */
294
+ var edgeEls = {}; /* "from\\u0000to" -> path element */
295
+ var flashTimers = {};
296
+
297
+ function $(id) { return document.getElementById(id); }
298
+ function svgEl(tag) { return document.createElementNS('http://www.w3.org/2000/svg', tag); }
299
+ function fmtMs(ms) {
300
+ if (ms == null) return '–';
301
+ if (ms < 1) return ms.toFixed(2);
302
+ if (ms < 100) return ms.toFixed(1);
303
+ return String(Math.round(ms));
304
+ }
305
+ function fmtTime(epoch) {
306
+ var d = new Date(epoch);
307
+ function p(n, w) { return String(n).padStart(w, '0'); }
308
+ return p(d.getHours(), 2) + ':' + p(d.getMinutes(), 2) + ':' + p(d.getSeconds(), 2) + '.' + p(d.getMilliseconds(), 3);
309
+ }
310
+
311
+ /* ---------- graph layout: rank = longest path to a leaf ---------- */
312
+
313
+ function computeRanks() {
314
+ var deps = {};
315
+ state.nodes.forEach(function (n) { deps[n.name] = []; });
316
+ state.edges.forEach(function (e) {
317
+ if (deps[e.from] && e.to in deps) deps[e.from].push(e.to);
318
+ });
319
+ var ranks = {}, visiting = {};
320
+ function rankOf(name) {
321
+ if (name in ranks) return ranks[name];
322
+ if (visiting[name]) return 0; /* cycle guard; modules validate anyway */
323
+ visiting[name] = true;
324
+ var r = 0;
325
+ deps[name].forEach(function (d) { r = Math.max(r, rankOf(d) + 1); });
326
+ visiting[name] = false;
327
+ ranks[name] = r;
328
+ return r;
329
+ }
330
+ state.nodes.forEach(function (n) { rankOf(n.name); });
331
+ return ranks;
332
+ }
333
+
334
+ function renderGraph() {
335
+ var host = $('graph-host');
336
+ host.textContent = '';
337
+ nodeEls = {};
338
+ edgeEls = {};
339
+ if (!state.nodes.length) {
340
+ var empty = document.createElement('div');
341
+ empty.id = 'graph-empty';
342
+ empty.textContent = 'No services yet \\u2014 waiting for an application to connect.';
343
+ host.appendChild(empty);
344
+ return;
345
+ }
346
+
347
+ var ranks = computeRanks();
348
+ var columns = {};
349
+ var maxRank = 0;
350
+ state.nodes.forEach(function (n) {
351
+ var r = ranks[n.name];
352
+ maxRank = Math.max(maxRank, r);
353
+ (columns[r] = columns[r] || []).push(n.name);
354
+ });
355
+ Object.keys(columns).forEach(function (r) { columns[r].sort(); });
356
+
357
+ var tallest = 0;
358
+ Object.keys(columns).forEach(function (r) { tallest = Math.max(tallest, columns[r].length); });
359
+ var width = PAD * 2 + maxRank * COL_W + NODE_W;
360
+ var height = PAD * 2 + (tallest - 1) * ROW_H + NODE_H;
361
+
362
+ var pos = {};
363
+ Object.keys(columns).forEach(function (r) {
364
+ var names = columns[r];
365
+ var colH = (names.length - 1) * ROW_H + NODE_H;
366
+ var y0 = PAD + (height - PAD * 2 - colH) / 2;
367
+ names.forEach(function (name, i) {
368
+ pos[name] = { x: PAD + Number(r) * COL_W, y: y0 + i * ROW_H };
369
+ });
370
+ });
371
+
372
+ var svg = svgEl('svg');
373
+ svg.setAttribute('viewBox', '0 0 ' + width + ' ' + height);
374
+ svg.setAttribute('role', 'img');
375
+ svg.setAttribute('aria-label', 'Service dependency graph');
376
+
377
+ /* Edges first, under the nodes. Arrow points at the dependency. */
378
+ var defs = svgEl('defs');
379
+ var marker = svgEl('marker');
380
+ marker.setAttribute('id', 'arrow');
381
+ marker.setAttribute('viewBox', '0 0 8 8');
382
+ marker.setAttribute('refX', '7');
383
+ marker.setAttribute('refY', '4');
384
+ marker.setAttribute('markerWidth', '7');
385
+ marker.setAttribute('markerHeight', '7');
386
+ marker.setAttribute('orient', 'auto-start-reverse');
387
+ var arrowPath = svgEl('path');
388
+ arrowPath.setAttribute('d', 'M 0 0 L 8 4 L 0 8 z');
389
+ arrowPath.setAttribute('fill', 'context-stroke');
390
+ marker.appendChild(arrowPath);
391
+ defs.appendChild(marker);
392
+ svg.appendChild(defs);
393
+
394
+ state.edges.forEach(function (e) {
395
+ var from = pos[e.from], to = pos[e.to];
396
+ if (!from || !to) return;
397
+ var x1 = from.x, y1 = from.y + NODE_H / 2;
398
+ var x2 = to.x + NODE_W + 6, y2 = to.y + NODE_H / 2;
399
+ var mid = (x1 - x2) / 2;
400
+ var path = svgEl('path');
401
+ path.setAttribute('class', 'edge');
402
+ path.setAttribute('marker-end', 'url(#arrow)');
403
+ path.setAttribute('d',
404
+ 'M ' + x1 + ' ' + y1 +
405
+ ' C ' + (x1 - mid) + ' ' + y1 + ' ' + (x2 + mid) + ' ' + y2 +
406
+ ' ' + x2 + ' ' + y2);
407
+ svg.appendChild(path);
408
+ edgeEls[e.from + '\\u0000' + e.to] = path;
409
+ });
410
+
411
+ state.nodes.forEach(function (n) {
412
+ var p = pos[n.name];
413
+ var g = svgEl('g');
414
+ g.setAttribute('transform', 'translate(' + p.x + ' ' + p.y + ')');
415
+
416
+ var rect = svgEl('rect');
417
+ rect.setAttribute('width', NODE_W);
418
+ rect.setAttribute('height', NODE_H);
419
+ rect.setAttribute('rx', 6);
420
+ g.appendChild(rect);
421
+
422
+ var label = svgEl('text');
423
+ label.setAttribute('x', 12);
424
+ label.setAttribute('y', 23);
425
+ label.textContent = n.name.length > 20 ? n.name.slice(0, 19) + '…' : n.name;
426
+ g.appendChild(label);
427
+
428
+ var glyph = svgEl('text');
429
+ glyph.setAttribute('class', 'glyph');
430
+ glyph.setAttribute('x', 12);
431
+ glyph.setAttribute('y', 42);
432
+ g.appendChild(glyph);
433
+
434
+ var status = svgEl('text');
435
+ status.setAttribute('class', 'status');
436
+ status.setAttribute('x', 26);
437
+ status.setAttribute('y', 42);
438
+ g.appendChild(status);
439
+
440
+ g.addEventListener('mousemove', function (ev) { showTooltip(n.name, ev); });
441
+ g.addEventListener('mouseleave', hideTooltip);
442
+
443
+ svg.appendChild(g);
444
+ nodeEls[n.name] = { group: g, statusText: status, glyph: glyph };
445
+ applyNodeState(n.name);
446
+ });
447
+
448
+ host.appendChild(svg);
449
+ }
450
+
451
+ function applyNodeState(name) {
452
+ var el = nodeEls[name];
453
+ var stats = state.services[name];
454
+ if (!el || !stats) return;
455
+ var flashing = el.group.classList.contains('flash');
456
+ el.group.setAttribute('class', 'node ' + stats.status + (flashing ? ' flash' : ''));
457
+ el.glyph.textContent = GLYPHS[stats.status] || '';
458
+ var line = stats.status;
459
+ if (stats.calls > 0) {
460
+ line += ' \\u00B7 ' + stats.calls + ' call' + (stats.calls === 1 ? '' : 's');
461
+ } else if (stats.status === 'ready' && stats.initMs != null) {
462
+ line += ' \\u00B7 init ' + fmtMs(stats.initMs) + 'ms';
463
+ }
464
+ el.statusText.textContent = line;
465
+ }
466
+
467
+ function flash(el, key) {
468
+ if (!el) return;
469
+ el.classList.add('active');
470
+ el.classList.add('flash');
471
+ clearTimeout(flashTimers[key]);
472
+ flashTimers[key] = setTimeout(function () {
473
+ el.classList.remove('active');
474
+ el.classList.remove('flash');
475
+ }, 450);
476
+ }
477
+
478
+ /* ---------- tooltip ---------- */
479
+
480
+ function showTooltip(name, ev) {
481
+ var stats = state.services[name];
482
+ if (!stats) return;
483
+ var tip = $('tooltip');
484
+ var avg = stats.calls > 0 ? stats.totalCallMs / stats.calls : null;
485
+ tip.textContent = '';
486
+ var title = document.createElement('div');
487
+ title.className = 't-name';
488
+ title.textContent = name;
489
+ tip.appendChild(title);
490
+ [
491
+ ['Status', stats.status],
492
+ ['Init', stats.initMs == null ? '–' : fmtMs(stats.initMs) + ' ms'],
493
+ ['Calls', String(stats.calls)],
494
+ ['Avg call', avg == null ? '–' : fmtMs(avg) + ' ms'],
495
+ ['Errors', String(stats.errors)]
496
+ ].forEach(function (pair) {
497
+ var row = document.createElement('div');
498
+ row.className = 't-row';
499
+ var k = document.createElement('span');
500
+ k.textContent = pair[0];
501
+ var v = document.createElement('b');
502
+ v.textContent = pair[1];
503
+ row.appendChild(k);
504
+ row.appendChild(v);
505
+ tip.appendChild(row);
506
+ });
507
+ tip.style.display = 'block';
508
+ var x = ev.clientX + 14, y = ev.clientY + 14;
509
+ if (x + tip.offsetWidth > window.innerWidth - 8) x = ev.clientX - tip.offsetWidth - 14;
510
+ if (y + tip.offsetHeight > window.innerHeight - 8) y = ev.clientY - tip.offsetHeight - 14;
511
+ tip.style.left = x + 'px';
512
+ tip.style.top = y + 'px';
513
+ }
514
+ function hideTooltip() { $('tooltip').style.display = 'none'; }
515
+
516
+ /* ---------- tiles + table ---------- */
517
+
518
+ function renderSummary() {
519
+ var names = Object.keys(state.services);
520
+ var ready = 0, calls = 0, errors = 0, callMs = 0;
521
+ names.forEach(function (n) {
522
+ var s = state.services[n];
523
+ if (s.status === 'ready') ready++;
524
+ calls += s.calls;
525
+ errors += s.errors;
526
+ callMs += s.totalCallMs;
527
+ });
528
+ $('tile-ready').innerHTML = '';
529
+ $('tile-ready').appendChild(document.createTextNode(ready + ''));
530
+ var total = document.createElement('small');
531
+ total.textContent = ' / ' + names.length;
532
+ $('tile-ready').appendChild(total);
533
+ $('tile-calls').textContent = String(calls);
534
+ $('tile-avg').textContent = calls > 0 ? fmtMs(callMs / calls) + ' ms' : '–';
535
+ $('tile-errors').textContent = String(errors);
536
+
537
+ var tbody = $('service-rows');
538
+ tbody.textContent = '';
539
+ names.sort().forEach(function (n) {
540
+ var s = state.services[n];
541
+ var tr = document.createElement('tr');
542
+ function cell(text, cls) {
543
+ var td = document.createElement('td');
544
+ if (cls) td.className = cls;
545
+ td.textContent = text;
546
+ return td;
547
+ }
548
+ tr.appendChild(cell(n));
549
+ var status = cell('');
550
+ var glyph = document.createElement('span');
551
+ glyph.className = 'glyph';
552
+ glyph.textContent = GLYPHS[s.status] + ' ';
553
+ var color = { ready: 'var(--good)', initializing: 'var(--warning)', error: 'var(--critical)' }[s.status];
554
+ glyph.style.color = color || 'var(--ink-muted)';
555
+ status.appendChild(glyph);
556
+ status.appendChild(document.createTextNode(s.status));
557
+ tr.appendChild(status);
558
+ tr.appendChild(cell(fmtMs(s.initMs), 'num'));
559
+ tr.appendChild(cell(String(s.calls), 'num'));
560
+ tr.appendChild(cell(s.calls > 0 ? fmtMs(s.totalCallMs / s.calls) : '–', 'num'));
561
+ tr.appendChild(cell(String(s.errors), 'num'));
562
+ tbody.appendChild(tr);
563
+ });
564
+ }
565
+
566
+ /* ---------- event log ---------- */
567
+
568
+ function logEvent(wire) {
569
+ var span = wire.span;
570
+ var isEnd = span.type === 'end';
571
+ /* Log every completed span, plus initialize starts (they can be slow). */
572
+ if (!isEnd && wire.kind !== 'initialize') return;
573
+
574
+ var row = document.createElement('div');
575
+ row.className = 'row'
576
+ + (isEnd && span.error ? ' error' : '')
577
+ + (wire.kind !== 'call' ? ' lifecycle' : '');
578
+
579
+ var time = document.createElement('span');
580
+ time.className = 'time';
581
+ time.textContent = fmtTime(span.time);
582
+ row.appendChild(time);
583
+
584
+ var name = document.createElement('span');
585
+ name.className = 'name';
586
+ var text = (wire.service != null ? wire.service + '.' : '') + wire.method;
587
+ if (wire.parentService && wire.parentService !== wire.service) {
588
+ text = wire.parentService + ' \\u2192 ' + text;
589
+ }
590
+ name.textContent = text;
591
+ if (isEnd && span.error) {
592
+ var err = document.createElement('div');
593
+ err.className = 'err-msg';
594
+ err.textContent = '\\u2715 ' + span.error;
595
+ name.appendChild(err);
596
+ }
597
+ row.appendChild(name);
598
+
599
+ var dur = document.createElement('span');
600
+ dur.className = 'dur';
601
+ dur.textContent = isEnd ? fmtMs(span.durationMs) + ' ms' : 'started…';
602
+ row.appendChild(dur);
603
+
604
+ var log = $('log');
605
+ log.insertBefore(row, log.firstChild);
606
+ while (log.children.length > MAX_LOG_ROWS) log.removeChild(log.lastChild);
607
+ }
608
+
609
+ /* ---------- live updates ---------- */
610
+
611
+ function applyEvent(wire) {
612
+ if (wire.service && wire.stats) {
613
+ var known = wire.service in state.services;
614
+ state.services[wire.service] = wire.stats;
615
+ if (!known) renderGraph();
616
+ applyNodeState(wire.service);
617
+ }
618
+ if (wire.kind === 'call' && wire.service) {
619
+ var el = nodeEls[wire.service];
620
+ if (el) flash(el.group, 'n:' + wire.service);
621
+ if (wire.parentService && wire.parentService !== wire.service) {
622
+ flash(edgeEls[wire.parentService + '\\u0000' + wire.service], 'e:' + wire.parentService + '>' + wire.service);
623
+ }
624
+ }
625
+ logEvent(wire);
626
+ renderSummary();
627
+ }
628
+
629
+ function connect() {
630
+ var es = new EventSource('/events');
631
+ es.addEventListener('open', function () {
632
+ var conn = $('conn');
633
+ conn.className = 'live';
634
+ conn.textContent = 'live';
635
+ });
636
+ es.addEventListener('error', function () {
637
+ var conn = $('conn');
638
+ conn.className = 'down';
639
+ conn.textContent = 'reconnecting…';
640
+ });
641
+ es.addEventListener('snapshot', function (ev) {
642
+ var snap = JSON.parse(ev.data);
643
+ state.nodes = snap.nodes;
644
+ state.edges = snap.edges;
645
+ state.services = snap.services;
646
+ renderGraph();
647
+ renderSummary();
648
+ $('log').textContent = '';
649
+ snap.recent.forEach(logEvent);
650
+ });
651
+ es.addEventListener('span', function (ev) {
652
+ applyEvent(JSON.parse(ev.data));
653
+ });
654
+ }
655
+
656
+ connect();
657
+ })();
658
+ </script>
659
+ </body>
660
+ </html>
661
+ `;
662
+ //# sourceMappingURL=dashboardHtml.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboardHtml.js","sourceRoot":"","sources":["../src/dashboardHtml.ts"],"names":[],"mappings":";;AASA,kDAEC;AAXD;;;;;;;;GAQG;AACH,SAAgB,mBAAmB;IACjC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,IAAI,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqoBZ,CAAC"}