@composed-di/observability 0.5.0-alpha → 0.7.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 (40) hide show
  1. package/dist/cli.js.map +1 -1
  2. package/dist/dashboardClient.d.ts +17 -11
  3. package/dist/dashboardClient.d.ts.map +1 -1
  4. package/dist/dashboardClient.js +28 -11
  5. package/dist/dashboardClient.js.map +1 -1
  6. package/dist/dashboardEventListener.d.ts +27 -8
  7. package/dist/dashboardEventListener.d.ts.map +1 -1
  8. package/dist/dashboardEventListener.js +38 -16
  9. package/dist/dashboardEventListener.js.map +1 -1
  10. package/dist/dashboardHtml.d.ts +6 -0
  11. package/dist/dashboardHtml.d.ts.map +1 -1
  12. package/dist/dashboardHtml.js +618 -98
  13. package/dist/dashboardHtml.js.map +1 -1
  14. package/dist/dashboardInstrumentation.d.ts +44 -0
  15. package/dist/dashboardInstrumentation.d.ts.map +1 -0
  16. package/dist/dashboardInstrumentation.js +119 -0
  17. package/dist/dashboardInstrumentation.js.map +1 -0
  18. package/dist/dashboardServer.d.ts +12 -8
  19. package/dist/dashboardServer.d.ts.map +1 -1
  20. package/dist/dashboardServer.js +52 -14
  21. package/dist/dashboardServer.js.map +1 -1
  22. package/dist/events.d.ts +20 -1
  23. package/dist/events.d.ts.map +1 -1
  24. package/dist/events.js +1 -1
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -1
  28. package/dist/index.js.map +1 -1
  29. package/dist/moduleGraph.d.ts.map +1 -1
  30. package/dist/moduleGraph.js.map +1 -1
  31. package/package.json +24 -23
  32. package/src/cli.ts +19 -19
  33. package/src/dashboardClient.ts +94 -83
  34. package/src/dashboardHtml.ts +620 -100
  35. package/src/dashboardInstrumentation.ts +153 -0
  36. package/src/dashboardServer.ts +183 -148
  37. package/src/events.ts +55 -35
  38. package/src/index.ts +5 -5
  39. package/src/moduleGraph.ts +9 -9
  40. package/src/dashboardEventListener.ts +0 -104
@@ -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: #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;
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: #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;
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: #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;
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.45 system-ui, -apple-system, "Segoe UI", sans-serif;
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: baseline;
69
- gap: 12px;
70
- padding: 16px 20px 0;
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: 2px 10px;
140
+ padding: 3px 12px;
81
141
  }
82
- #conn.live::before { content: "● "; color: var(--good); }
83
- #conn.down::before { content: ""; color: var(--critical); }
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 20px;
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: 8px;
95
- padding: 10px 16px;
96
- min-width: 132px;
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: 11px;
100
- letter-spacing: 0.06em;
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 { font-size: 24px; font-weight: 650; margin-top: 2px; }
105
- .tile .value small { font-size: 13px; font-weight: 400; color: var(--ink-secondary); }
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: 12px;
111
- padding: 0 20px 20px;
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: 8px;
191
+ border-radius: 10px;
192
+ box-shadow: var(--shadow);
118
193
  overflow: hidden;
119
194
  }
120
195
  .panel h2 {
121
- font-size: 12px;
122
- letter-spacing: 0.06em;
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: 10px 14px;
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 { padding: 32px; color: var(--ink-muted); text-align: center; }
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: 14px;
137
- padding: 10px 14px;
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(--grid);
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-muted);
242
+ stroke: var(--ink-faint);
155
243
  stroke-width: 1.5;
156
- rx: 6;
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-muted); stroke-dasharray: 4 3; }
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(--warning); animation: pulse 1s ease-in-out infinite; }
164
- .node.initializing .glyph { fill: var(--warning); }
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.flash rect { stroke: var(--accent); stroke-width: 2.5; }
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: 6px 14px; border-top: 1px solid var(--grid); }
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: 11px;
181
- letter-spacing: 0.06em;
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; font-variant-numeric: tabular-nums; }
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
- /* Event log */
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
- #log .row {
313
+ .log .row {
192
314
  display: flex;
193
315
  gap: 8px;
194
- padding: 5px 14px;
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 .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); }
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: 6px;
212
- box-shadow: 0 4px 14px rgba(0, 0, 0, 0.25);
213
- padding: 8px 12px;
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: 650; margin-bottom: 4px; }
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 { color: var(--ink); font-weight: 550; font-variant-numeric: tabular-nums; }
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(--ink-muted)">○</span> pending</span>
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:12px">
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)">&#x2715;</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
- function computeRanks() {
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
- state.nodes.forEach(function (n) { deps[n.name] = []; });
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
- state.nodes.forEach(function (n) { rankOf(n.name); });
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
- if (!state.nodes.length) {
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 = 'No services yet \\u2014 waiting for an application to connect.';
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
- state.nodes.forEach(function (n) {
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
- state.nodes.forEach(function (n) {
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', 6);
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 + (flashing ? ' flash' : ''));
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
- tr.appendChild(cell(n));
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(--warning)', error: 'var(--critical)' }[s.status];
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 known = wire.service in state.services;
1119
+ var wasDrawn = wire.service in nodeEls;
614
1120
  state.services[wire.service] = wire.stats;
615
- if (!known) renderGraph();
616
- applyNodeState(wire.service);
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>