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