proteum 2.1.0 → 2.1.2

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 (95) hide show
  1. package/AGENTS.md +44 -98
  2. package/README.md +143 -10
  3. package/agents/framework/AGENTS.md +146 -886
  4. package/agents/project/AGENTS.md +73 -127
  5. package/agents/project/client/AGENTS.md +22 -93
  6. package/agents/project/client/pages/AGENTS.md +24 -26
  7. package/agents/project/server/routes/AGENTS.md +10 -8
  8. package/agents/project/server/services/AGENTS.md +22 -159
  9. package/agents/project/tests/AGENTS.md +11 -8
  10. package/cli/app/config.ts +7 -20
  11. package/cli/bin.js +8 -0
  12. package/cli/commands/command.ts +243 -0
  13. package/cli/commands/commandLocalRunner.js +198 -0
  14. package/cli/commands/create.ts +5 -0
  15. package/cli/commands/deploy/web.ts +1 -2
  16. package/cli/commands/dev.ts +98 -2
  17. package/cli/commands/doctor.ts +8 -74
  18. package/cli/commands/explain.ts +8 -186
  19. package/cli/commands/init.ts +2 -94
  20. package/cli/commands/trace.ts +228 -0
  21. package/cli/compiler/artifacts/commands.ts +217 -0
  22. package/cli/compiler/artifacts/manifest.ts +35 -21
  23. package/cli/compiler/artifacts/services.ts +300 -1
  24. package/cli/compiler/client/index.ts +43 -8
  25. package/cli/compiler/common/commands.ts +175 -0
  26. package/cli/compiler/common/index.ts +1 -1
  27. package/cli/compiler/common/proteumManifest.ts +15 -114
  28. package/cli/compiler/index.ts +25 -2
  29. package/cli/compiler/server/index.ts +31 -6
  30. package/cli/index.ts +1 -4
  31. package/cli/paths.ts +16 -1
  32. package/cli/presentation/commands.ts +104 -14
  33. package/cli/presentation/devSession.ts +22 -3
  34. package/cli/presentation/proteum_logo_400x400_square_icon.txt +400 -0
  35. package/cli/runtime/commands.ts +121 -4
  36. package/cli/scaffold/index.ts +720 -0
  37. package/cli/scaffold/templates.ts +344 -0
  38. package/cli/scaffold/types.ts +26 -0
  39. package/cli/tsconfig.json +4 -1
  40. package/cli/utils/check.ts +1 -1
  41. package/client/app/component.tsx +13 -9
  42. package/client/dev/profiler/index.tsx +2511 -0
  43. package/client/dev/profiler/noop.tsx +5 -0
  44. package/client/dev/profiler/runtime.noop.ts +116 -0
  45. package/client/dev/profiler/runtime.ts +840 -0
  46. package/client/services/router/components/router.tsx +30 -2
  47. package/client/services/router/index.tsx +27 -3
  48. package/client/services/router/request/api.ts +133 -17
  49. package/commands/proteum/diagnostics.ts +11 -0
  50. package/common/dev/commands.ts +50 -0
  51. package/common/dev/diagnostics.ts +298 -0
  52. package/common/dev/profiler.ts +92 -0
  53. package/common/dev/proteumManifest.ts +135 -0
  54. package/common/dev/requestTrace.ts +115 -0
  55. package/common/env/proteumEnv.ts +284 -0
  56. package/common/router/index.ts +4 -22
  57. package/docs/dev-commands.md +93 -0
  58. package/docs/diagnostics.md +88 -0
  59. package/docs/request-tracing.md +132 -0
  60. package/eslint.js +11 -6
  61. package/package.json +3 -3
  62. package/server/app/commands.ts +35 -370
  63. package/server/app/commandsManager.ts +393 -0
  64. package/server/app/container/config.ts +11 -49
  65. package/server/app/container/console/index.ts +2 -3
  66. package/server/app/container/index.ts +5 -2
  67. package/server/app/container/trace/index.ts +364 -0
  68. package/server/app/devCommands.ts +192 -0
  69. package/server/app/devDiagnostics.ts +53 -0
  70. package/server/app/index.ts +29 -6
  71. package/server/index.ts +0 -1
  72. package/server/services/auth/index.ts +525 -61
  73. package/server/services/auth/router/index.ts +106 -7
  74. package/server/services/cron/CronTask.ts +73 -5
  75. package/server/services/cron/index.ts +34 -11
  76. package/server/services/fetch/index.ts +3 -10
  77. package/server/services/prisma/index.ts +66 -4
  78. package/server/services/router/http/index.ts +173 -6
  79. package/server/services/router/index.ts +200 -12
  80. package/server/services/router/request/api.ts +30 -1
  81. package/server/services/router/response/index.ts +83 -10
  82. package/server/services/router/response/page/document.tsx +16 -0
  83. package/server/services/router/response/page/index.tsx +27 -1
  84. package/skills/clean-project-code/SKILL.md +7 -2
  85. package/test-results/.last-run.json +4 -0
  86. package/types/aliases.d.ts +6 -0
  87. package/types/global/utils.d.ts +7 -14
  88. package/Rte.zip +0 -0
  89. package/agents/project/agents.md.zip +0 -0
  90. package/doc/TODO.md +0 -71
  91. package/doc/front/router.md +0 -27
  92. package/doc/workspace/workspace.png +0 -0
  93. package/doc/workspace/workspace2.png +0 -0
  94. package/doc/workspace/workspace_26.01.22.png +0 -0
  95. package/server/services/router/http/session.ts.old +0 -40
@@ -0,0 +1,2511 @@
1
+ import React from 'react';
2
+
3
+ import {
4
+ buildDoctorBlocks,
5
+ buildExplainBlocks,
6
+ buildExplainSummaryItems,
7
+ explainSectionNames,
8
+ formatManifestLocation,
9
+ type THumanTextBlock,
10
+ } from '@common/dev/diagnostics';
11
+ import type { TDevCommandDefinition, TDevCommandExecution } from '@common/dev/commands';
12
+ import type {
13
+ TProfilerCronTask,
14
+ TProfilerNavigationSession,
15
+ TProfilerPanel,
16
+ TProfilerSessionTrace,
17
+ } from '@common/dev/profiler';
18
+ import type { TRequestTrace, TTraceCall, TTraceEventType, TTraceSummaryValue } from '@common/dev/requestTrace';
19
+
20
+ import { profilerRuntime } from './runtime';
21
+
22
+ const profilerStyles = `
23
+ .proteum-profiler {
24
+ --profiler-bg: #f3f5f8;
25
+ --profiler-bg-strong: #ffffff;
26
+ --profiler-bg-soft: #eef3f8;
27
+ --profiler-surface-hover: #eef4ff;
28
+ --profiler-surface-selected: #e1ecff;
29
+ --profiler-line: rgba(19, 32, 51, 0.1);
30
+ --profiler-line-strong: rgba(19, 32, 51, 0.18);
31
+ --profiler-text: #132033;
32
+ --profiler-muted: #627186;
33
+ --profiler-brand: #175fe6;
34
+ --profiler-ok: #15803d;
35
+ --profiler-warn: #b45309;
36
+ --profiler-error: #b91c1c;
37
+ --profiler-title-row-bg: #f1f3f5;
38
+ position: fixed;
39
+ inset-inline: 0;
40
+ bottom: 0;
41
+ z-index: 2147483000;
42
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
43
+ color: var(--profiler-text);
44
+ letter-spacing: 0.01em;
45
+ }
46
+
47
+ .proteum-profiler__bar,
48
+ .proteum-profiler__panel,
49
+ .proteum-profiler__handle {
50
+ position: relative;
51
+ box-sizing: border-box;
52
+ }
53
+
54
+ .proteum-profiler__bar::before,
55
+ .proteum-profiler__panel::before,
56
+ .proteum-profiler__handle::before {
57
+ display: none;
58
+ }
59
+
60
+ .proteum-profiler__bar {
61
+ display: flex;
62
+ align-items: center;
63
+ gap: 0;
64
+ min-height: 32px;
65
+ padding: 6px 10px calc(6px + env(safe-area-inset-bottom, 0px));
66
+ border-top: 1px solid var(--profiler-line-strong);
67
+ background: var(--profiler-bg-strong);
68
+ backdrop-filter: none;
69
+ box-shadow: none;
70
+ overflow-x: auto;
71
+ scrollbar-width: none;
72
+ }
73
+
74
+ .proteum-profiler__bar::-webkit-scrollbar,
75
+ .proteum-profiler__panelTabs::-webkit-scrollbar {
76
+ display: none;
77
+ }
78
+
79
+ .proteum-profiler__token {
80
+ flex: 0 0 auto;
81
+ display: inline-flex;
82
+ align-items: center;
83
+ gap: 6px;
84
+ min-height: 20px;
85
+ padding: 0 10px;
86
+ border: none;
87
+ border-inline-start: 1px solid var(--profiler-line);
88
+ background: transparent;
89
+ color: var(--profiler-muted);
90
+ font-size: 11px;
91
+ line-height: 1;
92
+ letter-spacing: 0.06em;
93
+ text-transform: uppercase;
94
+ cursor: pointer;
95
+ white-space: nowrap;
96
+ }
97
+
98
+ .proteum-profiler__token:first-child {
99
+ padding-inline-start: 0;
100
+ border-inline-start: none;
101
+ }
102
+
103
+ .proteum-profiler__token:hover {
104
+ color: var(--profiler-text);
105
+ background: var(--profiler-surface-hover);
106
+ }
107
+
108
+ .proteum-profiler__token--brand {
109
+ color: var(--profiler-brand);
110
+ font-weight: 700;
111
+ }
112
+
113
+ .proteum-profiler__token--ok {
114
+ color: var(--profiler-ok);
115
+ }
116
+
117
+ .proteum-profiler__token--warn {
118
+ color: var(--profiler-warn);
119
+ }
120
+
121
+ .proteum-profiler__token--error {
122
+ color: var(--profiler-error);
123
+ }
124
+
125
+ .proteum-profiler__spacer {
126
+ flex: 1 1 auto;
127
+ min-width: 16px;
128
+ }
129
+
130
+ .proteum-profiler__handle {
131
+ position: fixed;
132
+ right: 10px;
133
+ bottom: calc(10px + env(safe-area-inset-bottom, 0px));
134
+ display: inline-flex;
135
+ align-items: center;
136
+ gap: 10px;
137
+ min-height: 30px;
138
+ padding: 0 12px;
139
+ border: 1px solid var(--profiler-line-strong);
140
+ border-radius: 0;
141
+ background: var(--profiler-bg-strong);
142
+ backdrop-filter: none;
143
+ color: var(--profiler-brand);
144
+ box-shadow: none;
145
+ cursor: pointer;
146
+ font-size: 11px;
147
+ letter-spacing: 0.08em;
148
+ text-transform: uppercase;
149
+ }
150
+
151
+ .proteum-profiler__panel {
152
+ position: fixed;
153
+ inset-inline: 0;
154
+ bottom: calc(32px + env(safe-area-inset-bottom, 0px));
155
+ display: grid;
156
+ grid-template-rows: auto 1fr;
157
+ height: 50vh;
158
+ max-height: 50vh;
159
+ margin: 0;
160
+ border: 1px solid var(--profiler-line-strong);
161
+ border-bottom: none;
162
+ border-left: none;
163
+ border-right: none;
164
+ border-radius: 0;
165
+ background: var(--profiler-bg-strong);
166
+ backdrop-filter: none;
167
+ box-shadow: none;
168
+ overflow: hidden;
169
+ }
170
+
171
+ .proteum-profiler__panelHeader {
172
+ display: flex;
173
+ align-items: center;
174
+ justify-content: flex-start;
175
+ gap: 12px;
176
+ padding: 12px 14px 10px;
177
+ border-bottom: 1px solid var(--profiler-line);
178
+ min-width: 0;
179
+ background: var(--profiler-bg-strong);
180
+ }
181
+
182
+ .proteum-profiler__panelTabs {
183
+ display: flex;
184
+ align-items: center;
185
+ gap: 14px;
186
+ overflow: auto;
187
+ padding: 0;
188
+ border-bottom: none;
189
+ scrollbar-width: none;
190
+ flex: 1 1 auto;
191
+ min-width: 0;
192
+ }
193
+
194
+ .proteum-profiler__pill {
195
+ display: inline-flex;
196
+ align-items: center;
197
+ gap: 6px;
198
+ min-height: 20px;
199
+ padding: 0;
200
+ border: none;
201
+ border-bottom: 1px solid transparent;
202
+ background: transparent;
203
+ font-size: 11px;
204
+ color: var(--profiler-muted);
205
+ cursor: pointer;
206
+ white-space: nowrap;
207
+ letter-spacing: 0.05em;
208
+ text-transform: uppercase;
209
+ }
210
+
211
+ .proteum-profiler__pill:hover {
212
+ color: var(--profiler-text);
213
+ border-bottom-color: var(--profiler-line-strong);
214
+ }
215
+
216
+ .proteum-profiler__pill--active {
217
+ color: var(--profiler-brand);
218
+ border-bottom-color: var(--profiler-brand);
219
+ }
220
+
221
+ .proteum-profiler__pill:disabled {
222
+ opacity: 0.44;
223
+ cursor: default;
224
+ }
225
+
226
+ .proteum-profiler__select {
227
+ flex: 0 1 280px;
228
+ min-width: 160px;
229
+ height: 28px;
230
+ padding: 0 28px 0 10px;
231
+ border: 1px solid var(--profiler-line);
232
+ border-radius: 0;
233
+ background-color: var(--profiler-bg-strong);
234
+ background-image:
235
+ linear-gradient(45deg, transparent 50%, var(--profiler-muted) 50%),
236
+ linear-gradient(135deg, var(--profiler-muted) 50%, transparent 50%);
237
+ background-position: calc(100% - 14px) 11px, calc(100% - 9px) 11px;
238
+ background-repeat: no-repeat;
239
+ background-size: 5px 5px;
240
+ color: var(--profiler-text);
241
+ font: inherit;
242
+ font-size: 11px;
243
+ outline: none;
244
+ appearance: none;
245
+ }
246
+
247
+ .proteum-profiler__select option {
248
+ background: var(--profiler-bg-strong);
249
+ color: var(--profiler-text);
250
+ }
251
+
252
+ .proteum-profiler__panelBody {
253
+ overflow: auto;
254
+ height: 100%;
255
+ min-height: 0;
256
+ padding: 0;
257
+ background: transparent;
258
+ }
259
+
260
+ .proteum-profiler__metrics {
261
+ display: grid;
262
+ gap: 0;
263
+ padding: 10px 12px 0;
264
+ }
265
+
266
+ .proteum-profiler__metricRow {
267
+ display: grid;
268
+ grid-template-columns: minmax(104px, 140px) 1fr;
269
+ gap: 12px;
270
+ padding: 8px 0;
271
+ border-top: 1px solid var(--profiler-line);
272
+ align-items: start;
273
+ }
274
+
275
+ .proteum-profiler__metricRow:first-child {
276
+ border-top: none;
277
+ }
278
+
279
+ .proteum-profiler__metricLabel {
280
+ color: var(--profiler-muted);
281
+ font-size: 10px;
282
+ letter-spacing: 0.08em;
283
+ text-transform: uppercase;
284
+ }
285
+
286
+ .proteum-profiler__metricValue {
287
+ font-size: 12px;
288
+ line-height: 1.45;
289
+ word-break: break-word;
290
+ }
291
+
292
+ .proteum-profiler__section {
293
+ display: grid;
294
+ gap: 0;
295
+ padding: 0;
296
+ border-top: 1px solid var(--profiler-line);
297
+ }
298
+
299
+ .proteum-profiler__sectionHeader {
300
+ display: flex;
301
+ align-items: center;
302
+ justify-content: space-between;
303
+ gap: 12px;
304
+ padding: 8px 10px;
305
+ background: var(--profiler-title-row-bg);
306
+ }
307
+
308
+ .proteum-profiler__actions {
309
+ display: flex;
310
+ align-items: center;
311
+ gap: 10px;
312
+ flex-wrap: wrap;
313
+ }
314
+
315
+ .proteum-profiler__panelHeader .proteum-profiler__actions {
316
+ flex: 0 0 auto;
317
+ margin-left: auto;
318
+ }
319
+
320
+ .proteum-profiler__sectionTitle {
321
+ font-size: 11px;
322
+ font-weight: 700;
323
+ color: var(--profiler-brand);
324
+ letter-spacing: 0.08em;
325
+ text-transform: uppercase;
326
+ }
327
+
328
+ .proteum-profiler__list {
329
+ display: grid;
330
+ gap: 0;
331
+ }
332
+
333
+ .proteum-profiler__row {
334
+ display: grid;
335
+ gap: 6px;
336
+ padding: 10px 12px;
337
+ border-top: 1px solid var(--profiler-line);
338
+ border-radius: 0;
339
+ background: transparent;
340
+ box-shadow: none;
341
+ }
342
+
343
+ .proteum-profiler__row--interactive {
344
+ width: 100%;
345
+ appearance: none;
346
+ background: transparent;
347
+ border: none;
348
+ text-align: left;
349
+ color: inherit;
350
+ cursor: pointer;
351
+ }
352
+
353
+ .proteum-profiler__row--interactive:hover {
354
+ background: var(--profiler-surface-hover);
355
+ }
356
+
357
+ .proteum-profiler__row--selected {
358
+ background: var(--profiler-surface-selected);
359
+ }
360
+
361
+ .proteum-profiler__rowHeader {
362
+ display: flex;
363
+ align-items: flex-start;
364
+ justify-content: space-between;
365
+ gap: 10px;
366
+ font-size: 11px;
367
+ line-height: 1.45;
368
+ }
369
+
370
+ .proteum-profiler__mono {
371
+ font-family: inherit;
372
+ font-size: 11px;
373
+ line-height: 1.5;
374
+ }
375
+
376
+ .proteum-profiler__muted {
377
+ color: var(--profiler-muted);
378
+ }
379
+
380
+ .proteum-profiler__pre {
381
+ margin: 0;
382
+ white-space: pre-wrap;
383
+ word-break: break-word;
384
+ padding: 10px 0 0;
385
+ border: none;
386
+ border-top: 1px solid var(--profiler-line);
387
+ border-radius: 0;
388
+ background: transparent;
389
+ }
390
+
391
+ .proteum-profiler__jsonKey {
392
+ color: var(--profiler-brand);
393
+ }
394
+
395
+ .proteum-profiler__jsonString {
396
+ color: #0f766e;
397
+ }
398
+
399
+ .proteum-profiler__jsonNumber {
400
+ color: #b45309;
401
+ }
402
+
403
+ .proteum-profiler__jsonLiteral {
404
+ color: var(--profiler-error);
405
+ }
406
+
407
+ .proteum-profiler__detail {
408
+ display: grid;
409
+ gap: 10px;
410
+ padding: 10px 0 0;
411
+ border: none;
412
+ border-top: 1px solid var(--profiler-line);
413
+ border-radius: 0;
414
+ background: transparent;
415
+ }
416
+
417
+ .proteum-profiler__detailLine {
418
+ display: grid;
419
+ gap: 4px;
420
+ }
421
+
422
+ .proteum-profiler__detailLabel {
423
+ color: var(--profiler-muted);
424
+ font-size: 10px;
425
+ letter-spacing: 0.08em;
426
+ text-transform: uppercase;
427
+ }
428
+
429
+ .proteum-profiler__tags {
430
+ display: flex;
431
+ flex-wrap: wrap;
432
+ gap: 8px;
433
+ }
434
+
435
+ .proteum-profiler__tag {
436
+ display: inline-flex;
437
+ align-items: center;
438
+ min-height: 0;
439
+ padding: 0;
440
+ font-size: 11px;
441
+ color: var(--profiler-muted);
442
+ }
443
+
444
+ .proteum-profiler__tag::before,
445
+ .proteum-profiler__tag::after {
446
+ color: var(--profiler-line-strong);
447
+ }
448
+
449
+ .proteum-profiler__tag::before {
450
+ content: '[';
451
+ }
452
+
453
+ .proteum-profiler__tag::after {
454
+ content: ']';
455
+ }
456
+
457
+ .proteum-profiler__empty {
458
+ padding: 12px;
459
+ border-top: 1px solid var(--profiler-line);
460
+ color: var(--profiler-muted);
461
+ }
462
+
463
+ .proteum-profiler__requestWorkspace {
464
+ display: grid;
465
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
466
+ gap: 0;
467
+ align-items: stretch;
468
+ min-height: 100%;
469
+ height: 100%;
470
+ }
471
+
472
+ .proteum-profiler__splitView {
473
+ display: grid;
474
+ grid-template-columns: minmax(0, 1.35fr) minmax(320px, 420px);
475
+ gap: 0;
476
+ align-items: stretch;
477
+ min-height: 100%;
478
+ height: 100%;
479
+ }
480
+
481
+ .proteum-profiler__splitView--stacked {
482
+ min-height: 0;
483
+ height: auto;
484
+ }
485
+
486
+ .proteum-profiler__splitColumn {
487
+ display: grid;
488
+ gap: 0;
489
+ min-width: 0;
490
+ align-content: start;
491
+ }
492
+
493
+ .proteum-profiler__requestGroups {
494
+ display: grid;
495
+ gap: 0;
496
+ min-width: 0;
497
+ }
498
+
499
+ .proteum-profiler__requestGroup {
500
+ display: grid;
501
+ gap: 0;
502
+ }
503
+
504
+ .proteum-profiler__requestGroupHeader {
505
+ display: flex;
506
+ align-items: center;
507
+ justify-content: space-between;
508
+ gap: 12px;
509
+ padding: 8px 10px;
510
+ background: var(--profiler-title-row-bg);
511
+ }
512
+
513
+ .proteum-profiler__requestGroupCount {
514
+ color: var(--profiler-muted);
515
+ font-size: 10px;
516
+ letter-spacing: 0.08em;
517
+ text-transform: uppercase;
518
+ }
519
+
520
+ .proteum-profiler__sidebar {
521
+ position: sticky;
522
+ top: 0;
523
+ display: flex;
524
+ align-self: stretch;
525
+ height: 100%;
526
+ min-height: 0;
527
+ padding: 0;
528
+ border: none;
529
+ border-left: 1px solid var(--profiler-line);
530
+ border-radius: 0;
531
+ background: transparent;
532
+ box-shadow: none;
533
+ }
534
+
535
+ .proteum-profiler__sidebarScroller {
536
+ display: grid;
537
+ flex: 1 1 auto;
538
+ gap: 0;
539
+ align-content: start;
540
+ height: 100%;
541
+ min-height: 0;
542
+ overflow: auto;
543
+ overscroll-behavior: contain;
544
+ scrollbar-width: thin;
545
+ }
546
+
547
+ .proteum-profiler__titleRow {
548
+ padding: 8px 10px;
549
+ background: var(--profiler-title-row-bg);
550
+ }
551
+
552
+ .proteum-profiler__sidebarHeader {
553
+ display: grid;
554
+ gap: 6px;
555
+ padding: 10px 12px 0;
556
+ }
557
+
558
+ .proteum-profiler__sidebarEyebrow,
559
+ .proteum-profiler__sidebarSectionTitle {
560
+ color: var(--profiler-muted);
561
+ font-size: 10px;
562
+ letter-spacing: 0.08em;
563
+ text-transform: uppercase;
564
+ }
565
+
566
+ .proteum-profiler__sidebarTitle {
567
+ font-size: 13px;
568
+ line-height: 1.5;
569
+ word-break: break-word;
570
+ }
571
+
572
+ .proteum-profiler__sidebarSection {
573
+ display: grid;
574
+ gap: 6px;
575
+ padding: 10px 12px 0;
576
+ border-top: 1px solid var(--profiler-line);
577
+ }
578
+
579
+ .proteum-profiler__sidebarScroller > .proteum-profiler__metrics {
580
+ border-top: 1px solid var(--profiler-line);
581
+ }
582
+
583
+ .proteum-profiler__sidebarEmpty {
584
+ font-size: 12px;
585
+ color: var(--profiler-muted);
586
+ }
587
+
588
+ .proteum-profiler__timelineChart {
589
+ display: grid;
590
+ gap: 0;
591
+ }
592
+
593
+ .proteum-profiler__timelineChartMeta {
594
+ display: flex;
595
+ align-items: center;
596
+ justify-content: space-between;
597
+ gap: 12px;
598
+ padding: 10px 12px 0;
599
+ }
600
+
601
+ .proteum-profiler__timelineChartCanvas {
602
+ position: relative;
603
+ padding: 8px 12px 12px;
604
+ border-top: 1px solid var(--profiler-line);
605
+ }
606
+
607
+ .proteum-profiler__timelineChartCanvas > * {
608
+ height: 100%;
609
+ }
610
+
611
+ .proteum-profiler__timelineChartCanvas canvas {
612
+ display: block;
613
+ width: 100%;
614
+ height: 100%;
615
+ }
616
+
617
+ .proteum-profiler__timelineChartCanvas .apexcharts-canvas,
618
+ .proteum-profiler__timelineChartCanvas .apexcharts-svg {
619
+ background: transparent !important;
620
+ }
621
+
622
+ .proteum-profiler__traceEventRow {
623
+ --profiler-trace-depth: 0;
624
+ --profiler-trace-guide-opacity: 0;
625
+ --profiler-trace-indent: calc(var(--profiler-trace-depth) * 18px);
626
+ }
627
+
628
+ .proteum-profiler__traceEventRow .proteum-profiler__rowHeader,
629
+ .proteum-profiler__traceEventRow .proteum-profiler__tags {
630
+ padding-inline-start: var(--profiler-trace-indent);
631
+ }
632
+
633
+ .proteum-profiler__traceEventRow .proteum-profiler__rowHeader {
634
+ position: relative;
635
+ }
636
+
637
+ .proteum-profiler__traceEventRow .proteum-profiler__rowHeader::before {
638
+ content: '';
639
+ position: absolute;
640
+ left: max(0px, calc(var(--profiler-trace-indent) - 8px));
641
+ top: 3px;
642
+ bottom: 3px;
643
+ width: 1px;
644
+ background: var(--profiler-line-strong);
645
+ opacity: var(--profiler-trace-guide-opacity);
646
+ }
647
+
648
+ @media (max-width: 900px) {
649
+ .proteum-profiler__panel {
650
+ height: 50vh;
651
+ max-height: 50vh;
652
+ margin: 0;
653
+ border-left: 0;
654
+ border-right: 0;
655
+ border-radius: 0;
656
+ }
657
+
658
+ .proteum-profiler__bar {
659
+ padding-inline: 8px;
660
+ }
661
+
662
+ .proteum-profiler__panelHeader,
663
+ .proteum-profiler__panelTabs,
664
+ .proteum-profiler__panelBody {
665
+ padding-inline: 10px;
666
+ }
667
+
668
+ .proteum-profiler__panelTabs {
669
+ gap: 10px;
670
+ }
671
+
672
+ .proteum-profiler__metricRow {
673
+ grid-template-columns: minmax(90px, 110px) 1fr;
674
+ }
675
+
676
+ .proteum-profiler__select {
677
+ min-width: 132px;
678
+ }
679
+
680
+ .proteum-profiler__requestWorkspace {
681
+ grid-template-columns: 1fr;
682
+ min-height: 0;
683
+ height: auto;
684
+ }
685
+
686
+ .proteum-profiler__splitView {
687
+ grid-template-columns: 1fr;
688
+ min-height: 0;
689
+ height: auto;
690
+ }
691
+
692
+ .proteum-profiler__sidebar {
693
+ position: static;
694
+ height: auto;
695
+ min-height: 0;
696
+ border-left: none;
697
+ border-top: 1px solid var(--profiler-line);
698
+ }
699
+
700
+ .proteum-profiler__sidebarScroller {
701
+ height: auto;
702
+ max-height: none;
703
+ min-height: 0;
704
+ }
705
+
706
+ }
707
+ `;
708
+
709
+ type TSessionSummary = {
710
+ apiAsyncCount: number;
711
+ apiSyncCount: number;
712
+ errorCount: number;
713
+ primaryTrace?: TProfilerSessionTrace;
714
+ renderMs?: number;
715
+ routeLabel: string;
716
+ ssrPayloadBytes?: number;
717
+ statusLabel: string;
718
+ totalMs?: number;
719
+ };
720
+ type TApiRequestItem = {
721
+ id: string;
722
+ groupLabel: string;
723
+ durationMs?: number;
724
+ errorMessage?: string;
725
+ finishedAt?: string;
726
+ label?: string;
727
+ method: string;
728
+ path: string;
729
+ requestData?: TTraceSummaryValue;
730
+ result?: TTraceSummaryValue;
731
+ startedAt: string;
732
+ statusCode?: number;
733
+ statusLabel?: string;
734
+ tags: string[];
735
+ };
736
+ type TTimelineWaterfallEventItem = {
737
+ chartLabel: string;
738
+ color: string;
739
+ durationMs: number;
740
+ endMs: number;
741
+ endOffsetMs: number;
742
+ event: TRequestTrace['events'][number];
743
+ startMs: number;
744
+ startOffsetMs: number;
745
+ traceLabel: string;
746
+ };
747
+ type TProfilerState = ReturnType<typeof profilerRuntime.getState>;
748
+
749
+ const panelLabels: Record<TProfilerPanel, string> = {
750
+ summary: 'Summary',
751
+ timeline: 'Timeline',
752
+ routing: 'Routing',
753
+ auth: 'Auth',
754
+ controller: 'Controller',
755
+ ssr: 'SSR',
756
+ api: 'API',
757
+ errors: 'Errors',
758
+ explain: 'Explain',
759
+ doctor: 'Doctor',
760
+ commands: 'Commands',
761
+ cron: 'Cron',
762
+ };
763
+
764
+ const getSelectedSession = (sessions: TProfilerNavigationSession[], selectedSessionId?: string, currentSessionId?: string) =>
765
+ sessions.find((session) => session.id === selectedSessionId) ||
766
+ sessions.find((session) => session.id === currentSessionId) ||
767
+ sessions[sessions.length - 1];
768
+
769
+ const getSessionSelectorLabel = (session: TProfilerNavigationSession) => truncate(session.path || session.url || session.label, 56);
770
+ const truncate = (value: string, max = 96) => (value.length <= max ? value : `${value.slice(0, max)}...`);
771
+ const readNumber = (value: TTraceSummaryValue | undefined) => (typeof value === 'number' ? value : undefined);
772
+ const readString = (value: TTraceSummaryValue | undefined) => (typeof value === 'string' ? value : undefined);
773
+ const formatDuration = (value?: number) => (value === undefined ? 'pending' : `${Math.round(value)} ms`);
774
+ const formatBytes = (value?: number) => (value === undefined ? 'n/a' : `${(value / 1024).toFixed(value >= 1024 ? 1 : 2)} KB`);
775
+ const formatTimestamp = (value?: string) => {
776
+ if (!value) return 'never';
777
+ const date = new Date(value);
778
+ return Number.isNaN(date.valueOf()) ? value : date.toLocaleString();
779
+ };
780
+ const formatCronFrequency = (task: TProfilerCronTask) =>
781
+ task.frequency.kind === 'cron' ? task.frequency.value : `once at ${formatTimestamp(task.frequency.value)}`;
782
+ const formatStructuredValue = (value: unknown) => {
783
+ try {
784
+ return JSON.stringify(value, null, 2);
785
+ } catch (error) {
786
+ return String(value);
787
+ }
788
+ };
789
+
790
+ const renderSummaryValue = (value: TTraceSummaryValue | undefined): string => {
791
+ if (value === undefined) return '';
792
+ if (value === null) return 'null';
793
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
794
+ if (value.kind === 'undefined') return 'undefined';
795
+ if (value.kind === 'redacted') return `[redacted: ${value.reason}]`;
796
+ if (value.kind === 'error') return `${value.name}: ${value.message}`;
797
+ if (value.kind === 'array') return `Array(${value.length})`;
798
+ if (value.kind === 'object') return `${value.constructorName} { ${Object.keys(value.entries).join(', ')} }`;
799
+ if (value.kind === 'buffer') return `Buffer(${value.byteLength})`;
800
+ if ('value' in value) return String(value.value);
801
+ if ('size' in value) return String(value.size);
802
+ if ('name' in value) return value.name;
803
+ return JSON.stringify(value);
804
+ };
805
+
806
+ const toSummaryJsonValue = (value: TTraceSummaryValue | undefined): unknown => {
807
+ if (value === undefined) return 'undefined';
808
+ if (value === null) return null;
809
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return value;
810
+ if (value.kind === 'undefined') return 'undefined';
811
+ if (value.kind === 'redacted') return `[redacted: ${value.reason}]`;
812
+ if (value.kind === 'bigint') return `${value.value}n`;
813
+ if (value.kind === 'symbol') return value.value;
814
+ if (value.kind === 'function') return `[Function ${value.name}]`;
815
+ if (value.kind === 'date') return value.value;
816
+ if (value.kind === 'error') return { name: value.name, message: value.message, stack: value.stack };
817
+ if (value.kind === 'buffer') return `[Buffer ${value.byteLength} bytes]`;
818
+ if (value.kind === 'map') return `[Map(${value.size})]`;
819
+ if (value.kind === 'set') return `[Set(${value.size})]`;
820
+ if (value.kind === 'array') {
821
+ const items = value.items.map((item) => toSummaryJsonValue(item));
822
+ if (value.truncated) items.push(`... ${Math.max(0, value.length - value.items.length)} more item(s)`);
823
+ return items;
824
+ }
825
+
826
+ const objectValue: Record<string, unknown> = {};
827
+ for (const [key, entry] of Object.entries(value.entries)) objectValue[key] = toSummaryJsonValue(entry);
828
+ if (value.truncated) objectValue.__truncated = `${Math.max(0, value.keys.length - Object.keys(value.entries).length)} more key(s)`;
829
+ return objectValue;
830
+ };
831
+
832
+ const formatSummaryJson = (value: TTraceSummaryValue | undefined) => {
833
+ if (value === undefined) return 'undefined';
834
+ if (typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'undefined') return 'undefined';
835
+ return JSON.stringify(toSummaryJsonValue(value), null, 2);
836
+ };
837
+
838
+ const formatTraceEventDetailsJson = (details: Record<string, TTraceSummaryValue>) =>
839
+ JSON.stringify(
840
+ Object.fromEntries(Object.entries(details).map(([key, value]) => [key, toSummaryJsonValue(value)])),
841
+ null,
842
+ 2,
843
+ );
844
+
845
+ const renderHighlightedJson = (value: string) => {
846
+ const tokenPattern =
847
+ /"(?:\\u[0-9a-fA-F]{4}|\\[^u]|[^\\"])*"(?=\s*:)|"(?:\\u[0-9a-fA-F]{4}|\\[^u]|[^\\"])*"|\btrue\b|\bfalse\b|\bnull\b|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/g;
848
+ const parts: React.ReactNode[] = [];
849
+ let lastIndex = 0;
850
+ let match: RegExpExecArray | null;
851
+
852
+ while ((match = tokenPattern.exec(value))) {
853
+ const index = match.index;
854
+ const token = match[0];
855
+
856
+ if (index > lastIndex) parts.push(value.slice(lastIndex, index));
857
+
858
+ const trailing = value.slice(index + token.length);
859
+ const isKey = token.startsWith('"') && /^\s*:/.test(trailing);
860
+ const className = token.startsWith('"')
861
+ ? isKey
862
+ ? 'proteum-profiler__jsonKey'
863
+ : 'proteum-profiler__jsonString'
864
+ : token === 'true' || token === 'false' || token === 'null'
865
+ ? 'proteum-profiler__jsonLiteral'
866
+ : 'proteum-profiler__jsonNumber';
867
+
868
+ parts.push(
869
+ <span className={className} key={`json:${index}`}>
870
+ {token}
871
+ </span>,
872
+ );
873
+ lastIndex = index + token.length;
874
+ }
875
+
876
+ if (lastIndex < value.length) parts.push(value.slice(lastIndex));
877
+
878
+ return parts;
879
+ };
880
+
881
+ const formatSummaryLiteral = (value: TTraceSummaryValue | undefined, depth = 1): string => {
882
+ if (value === undefined) return '';
883
+ if (value === null) return 'null';
884
+ if (typeof value === 'string') return JSON.stringify(value);
885
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
886
+ if (depth <= 0) return renderSummaryValue(value);
887
+ if (value.kind === 'undefined') return 'undefined';
888
+ if (value.kind === 'redacted') return `[redacted: ${value.reason}]`;
889
+ if (value.kind === 'bigint') return `${value.value}n`;
890
+ if (value.kind === 'symbol') return value.value;
891
+ if (value.kind === 'function') return `[Function ${value.name}]`;
892
+ if (value.kind === 'date') return JSON.stringify(value.value);
893
+ if (value.kind === 'error') return `${value.name}(${JSON.stringify(value.message)})`;
894
+ if (value.kind === 'buffer') return `Buffer(${value.byteLength})`;
895
+ if (value.kind === 'map') return `Map(${value.size})`;
896
+ if (value.kind === 'set') return `Set(${value.size})`;
897
+ if (value.kind === 'array') {
898
+ const items = value.items.slice(0, 4).map((item) => formatSummaryLiteral(item, depth - 1));
899
+ if (value.truncated || value.length > items.length) items.push('...');
900
+ return `[${items.join(', ')}]`;
901
+ }
902
+
903
+ const entries = Object.entries(value.entries)
904
+ .slice(0, 4)
905
+ .map(([key, entry]) => `${key}: ${formatSummaryLiteral(entry, depth - 1)}`);
906
+ if (value.truncated || value.keys.length > entries.length) entries.push('...');
907
+ return `{ ${entries.join(', ')} }`;
908
+ };
909
+
910
+ const getApiReferenceName = (method: string, path: string, fallbackLabel?: string) => {
911
+ if (path.startsWith('/api/')) return path.slice('/api/'.length).split('/').filter(Boolean).join('.');
912
+
913
+ const rawName = `${method} ${path}`.trim();
914
+ if (rawName) return rawName;
915
+ return fallbackLabel || 'request';
916
+ };
917
+
918
+ const formatApiReference = (method: string, path: string, requestData?: TTraceSummaryValue, fallbackLabel?: string) => {
919
+ const args = formatSummaryLiteral(requestData, 1);
920
+ return `${getApiReferenceName(method, path, fallbackLabel)}(${truncate(args, 112)})`;
921
+ };
922
+
923
+ const formatProfilerRequestReference = ({
924
+ fallbackLabel,
925
+ method,
926
+ path,
927
+ requestData,
928
+ }: {
929
+ fallbackLabel?: string;
930
+ method?: string;
931
+ path?: string;
932
+ requestData?: TTraceSummaryValue;
933
+ }) => {
934
+ const safeMethod = method || '';
935
+ const safePath = path || '';
936
+
937
+ if (safePath.startsWith('/api/')) return formatApiReference(safeMethod, safePath, requestData, fallbackLabel);
938
+
939
+ const rawReference = `${safeMethod} ${safePath}`.trim();
940
+ return rawReference || fallbackLabel || 'request';
941
+ };
942
+
943
+ const getTraceRequestData = (trace: TRequestTrace | undefined) =>
944
+ trace?.events.find((event) => event.type === 'request.start')?.details.data;
945
+
946
+ const getTraceResultData = (trace: TRequestTrace | undefined) =>
947
+ [...findTraceEvents(trace, ['controller.result'])]
948
+ .reverse()
949
+ .find((event) => event.details.kind === 'json' && event.details.data !== undefined)?.details.data;
950
+
951
+ const getRequestStatusText = (statusCode?: number, statusLabel?: string) =>
952
+ statusCode !== undefined ? String(statusCode) : statusLabel || 'pending';
953
+
954
+ const findTraceEvents = (trace: TRequestTrace | undefined, eventTypes: string[]) =>
955
+ trace?.events.filter((event) => eventTypes.includes(event.type)) || [];
956
+
957
+ const traceEventDepths: Record<TTraceEventType, number> = {
958
+ 'request.start': 0,
959
+ 'request.user': 1,
960
+ 'auth.decode': 1,
961
+ 'auth.route': 1,
962
+ 'auth.check.start': 2,
963
+ 'auth.check.rule': 3,
964
+ 'auth.check.result': 2,
965
+ 'auth.session': 1,
966
+ 'resolve.start': 1,
967
+ 'resolve.controller-route': 2,
968
+ 'resolve.routes-evaluated': 1,
969
+ 'resolve.route-skip': 2,
970
+ 'resolve.route-match': 2,
971
+ 'resolve.not-found': 1,
972
+ 'controller.start': 2,
973
+ 'controller.result': 2,
974
+ 'setup.options': 3,
975
+ 'context.create': 3,
976
+ 'page.data': 3,
977
+ 'ssr.payload': 3,
978
+ 'render.start': 2,
979
+ 'render.end': 2,
980
+ 'response.send': 1,
981
+ 'request.finish': 0,
982
+ error: 0,
983
+ };
984
+
985
+ const getTraceEventDepth = (event: TRequestTrace['events'][number]) => traceEventDepths[event.type] ?? 0;
986
+
987
+ const authEventTypes: TTraceEventType[] = [
988
+ 'auth.decode',
989
+ 'auth.route',
990
+ 'auth.check.start',
991
+ 'auth.check.rule',
992
+ 'auth.check.result',
993
+ 'auth.session',
994
+ ];
995
+
996
+ const getSummary = (session: TProfilerNavigationSession): TSessionSummary => {
997
+ const primaryTrace =
998
+ session.traces.find((trace) => trace.kind === 'initial-root' && trace.trace) ||
999
+ session.traces.find((trace) => trace.kind === 'navigation-data' && trace.trace) ||
1000
+ session.traces.find((trace) => trace.trace);
1001
+ const trace = primaryTrace?.trace;
1002
+ const syncCalls = session.traces.flatMap((traceItem) =>
1003
+ traceItem.trace?.calls.filter((call) => call.origin === 'ssr-fetcher' || call.origin === 'api-batch-fetcher') || [],
1004
+ );
1005
+ const asyncCount = session.traces.filter((traceItem) => traceItem.kind === 'async').length;
1006
+ const errorCount =
1007
+ session.steps.filter((step) => step.status === 'error').length +
1008
+ session.traces.filter((traceItem) => traceItem.status === 'error').length +
1009
+ syncCalls.filter((call) => call.errorMessage || (call.statusCode !== undefined && call.statusCode >= 400)).length;
1010
+ const renderStart = trace?.events.find((event) => event.type === 'render.start');
1011
+ const renderEnd = trace?.events.find((event) => event.type === 'render.end');
1012
+ const localRender = [...session.steps].reverse().find((step) => step.label === 'Render' && step.durationMs !== undefined);
1013
+ const ssrPayload = trace?.events.find((event) => event.type === 'ssr.payload');
1014
+ const routeLabel = session.routeLabel || readString(renderStart?.details.routeId) || session.path;
1015
+
1016
+ return {
1017
+ apiAsyncCount: asyncCount,
1018
+ apiSyncCount: syncCalls.length,
1019
+ errorCount,
1020
+ primaryTrace,
1021
+ renderMs:
1022
+ renderStart && renderEnd
1023
+ ? Math.max(0, renderEnd.elapsedMs - renderStart.elapsedMs)
1024
+ : localRender?.durationMs,
1025
+ routeLabel,
1026
+ ssrPayloadBytes: readNumber(ssrPayload?.details.serializedBytes),
1027
+ statusLabel: session.kind === 'client-navigation' ? 'NAV' : trace ? `${trace.statusCode || 'pending'} ${trace.method}` : 'SSR',
1028
+ totalMs: session.kind === 'client-navigation' ? session.durationMs : trace?.durationMs ?? session.durationMs,
1029
+ };
1030
+ };
1031
+
1032
+ const StatusToken = ({ label, onClick, tone = 'ok' }: { label: string; onClick: () => void; tone?: 'ok' | 'warn' | 'error' }) => (
1033
+ <button className={`proteum-profiler__token proteum-profiler__token--${tone}`} onClick={onClick} type="button">
1034
+ {label}
1035
+ </button>
1036
+ );
1037
+
1038
+ const SummaryRow = ({ label, value }: { label: string; value: React.ReactNode }) => (
1039
+ <div className="proteum-profiler__metricRow">
1040
+ <div className="proteum-profiler__metricLabel">{label}</div>
1041
+ <div className="proteum-profiler__metricValue">{value}</div>
1042
+ </div>
1043
+ );
1044
+
1045
+ const JsonCodeBlock = ({ value }: { value: string }) => (
1046
+ <pre className="proteum-profiler__mono proteum-profiler__pre">{renderHighlightedJson(value)}</pre>
1047
+ );
1048
+
1049
+ const formatTraceCallDisplay = (call: TTraceCall) => {
1050
+ if (call.path.startsWith('/api/')) {
1051
+ return formatProfilerRequestReference({
1052
+ fallbackLabel: call.label,
1053
+ method: call.method,
1054
+ path: call.path,
1055
+ requestData: call.requestData,
1056
+ });
1057
+ }
1058
+
1059
+ const rawReference = `${call.method} ${call.path}`.trim();
1060
+ if (call.label && rawReference) return `${call.label} (${rawReference})`;
1061
+ return call.label || rawReference || 'request';
1062
+ };
1063
+
1064
+ const formatSessionTraceDisplay = (traceItem: TProfilerSessionTrace) => {
1065
+ if (traceItem.path.startsWith('/api/')) {
1066
+ return formatProfilerRequestReference({
1067
+ fallbackLabel: traceItem.label,
1068
+ method: traceItem.method,
1069
+ path: traceItem.path,
1070
+ requestData: getTraceRequestData(traceItem.trace),
1071
+ });
1072
+ }
1073
+
1074
+ return traceItem.label || formatProfilerRequestReference({ method: traceItem.method, path: traceItem.path });
1075
+ };
1076
+
1077
+ const ApiRequestListEntry = ({
1078
+ isSelected,
1079
+ item,
1080
+ onSelect,
1081
+ }: {
1082
+ isSelected: boolean;
1083
+ item: TApiRequestItem;
1084
+ onSelect: () => void;
1085
+ }) => {
1086
+ const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
1087
+
1088
+ return (
1089
+ <button
1090
+ aria-pressed={isSelected}
1091
+ className={`proteum-profiler__row proteum-profiler__row--interactive ${isSelected ? 'proteum-profiler__row--selected' : ''}`}
1092
+ onClick={onSelect}
1093
+ type="button"
1094
+ >
1095
+ <div className="proteum-profiler__rowHeader">
1096
+ <strong>{formatApiReference(item.method, item.path, item.requestData, item.label)}</strong>
1097
+ <span className="proteum-profiler__mono proteum-profiler__muted">
1098
+ {formatDuration(item.durationMs)} | {statusText}
1099
+ </span>
1100
+ </div>
1101
+ <div className="proteum-profiler__tags">
1102
+ {item.tags.map((tag) => (
1103
+ <span className="proteum-profiler__tag" key={`${item.id}:${tag}`}>
1104
+ {tag}
1105
+ </span>
1106
+ ))}
1107
+ {item.errorMessage ? <span className="proteum-profiler__tag">{truncate(item.errorMessage, 72)}</span> : null}
1108
+ </div>
1109
+ </button>
1110
+ );
1111
+ };
1112
+
1113
+ const ApiRequestSidebar = ({ item }: { item?: TApiRequestItem }) => {
1114
+ if (!item) {
1115
+ return (
1116
+ <aside className="proteum-profiler__sidebar">
1117
+ <div className="proteum-profiler__sidebarScroller">
1118
+ <div className="proteum-profiler__sidebarHeader">
1119
+ <div className="proteum-profiler__sidebarEyebrow">Request details</div>
1120
+ <div className="proteum-profiler__sidebarEmpty">
1121
+ Select a request to inspect its payload, result, and timing.
1122
+ </div>
1123
+ </div>
1124
+ </div>
1125
+ </aside>
1126
+ );
1127
+ }
1128
+
1129
+ const statusText = getRequestStatusText(item.statusCode, item.statusLabel);
1130
+
1131
+ return (
1132
+ <aside className="proteum-profiler__sidebar">
1133
+ <div className="proteum-profiler__sidebarScroller">
1134
+ <div className="proteum-profiler__sidebarHeader">
1135
+ <div className="proteum-profiler__sidebarEyebrow">{item.groupLabel}</div>
1136
+ <div className="proteum-profiler__sidebarTitle">
1137
+ <strong>{formatApiReference(item.method, item.path, item.requestData, item.label)}</strong>
1138
+ </div>
1139
+ <div className="proteum-profiler__mono proteum-profiler__muted">
1140
+ {formatProfilerRequestReference({
1141
+ fallbackLabel: item.label,
1142
+ method: item.method,
1143
+ path: item.path,
1144
+ requestData: item.requestData,
1145
+ })}
1146
+ </div>
1147
+ </div>
1148
+
1149
+ <div className="proteum-profiler__metrics">
1150
+ <SummaryRow label="Status" value={statusText} />
1151
+ <SummaryRow label="Duration" value={formatDuration(item.durationMs)} />
1152
+ <SummaryRow label="Started" value={formatTimestamp(item.startedAt)} />
1153
+ <SummaryRow label="Finished" value={item.finishedAt ? formatTimestamp(item.finishedAt) : 'pending'} />
1154
+ <SummaryRow
1155
+ label="Endpoint"
1156
+ value={formatProfilerRequestReference({
1157
+ fallbackLabel: item.label,
1158
+ method: item.method,
1159
+ path: item.path,
1160
+ requestData: item.requestData,
1161
+ })}
1162
+ />
1163
+ </div>
1164
+
1165
+ {item.tags.length > 0 ? (
1166
+ <div className="proteum-profiler__sidebarSection">
1167
+ <div className="proteum-profiler__sidebarSectionTitle">Tags</div>
1168
+ <div className="proteum-profiler__tags">
1169
+ {item.tags.map((tag) => (
1170
+ <span className="proteum-profiler__tag" key={`${item.id}:detail:${tag}`}>
1171
+ {tag}
1172
+ </span>
1173
+ ))}
1174
+ </div>
1175
+ </div>
1176
+ ) : null}
1177
+
1178
+ <div className="proteum-profiler__sidebarSection">
1179
+ <div className="proteum-profiler__sidebarSectionTitle">Arguments</div>
1180
+ <JsonCodeBlock value={formatSummaryJson(item.requestData)} />
1181
+ </div>
1182
+
1183
+ <div className="proteum-profiler__sidebarSection">
1184
+ <div className="proteum-profiler__sidebarSectionTitle">Result</div>
1185
+ <JsonCodeBlock value={formatSummaryJson(item.result)} />
1186
+ </div>
1187
+
1188
+ {item.errorMessage ? (
1189
+ <div className="proteum-profiler__sidebarSection">
1190
+ <div className="proteum-profiler__sidebarSectionTitle">Error</div>
1191
+ <div className="proteum-profiler__mono">{item.errorMessage}</div>
1192
+ </div>
1193
+ ) : null}
1194
+ </div>
1195
+ </aside>
1196
+ );
1197
+ };
1198
+
1199
+ const ApiPanel = ({ session }: { session: TProfilerNavigationSession }) => {
1200
+ const syncItems: TApiRequestItem[] = session.traces
1201
+ .flatMap((trace) => trace.trace?.calls.filter((call) => call.origin !== 'client-async') || [])
1202
+ .map((call: TTraceCall) => ({
1203
+ id: call.id,
1204
+ groupLabel: 'Synchronous call',
1205
+ durationMs: call.durationMs,
1206
+ errorMessage: call.errorMessage,
1207
+ finishedAt: call.finishedAt,
1208
+ label: call.label,
1209
+ method: call.method,
1210
+ path: call.path,
1211
+ requestData: call.requestData,
1212
+ result: call.result,
1213
+ startedAt: call.startedAt,
1214
+ statusCode: call.statusCode,
1215
+ tags: [
1216
+ call.origin,
1217
+ ...(call.fetcherId ? [`fetcher:${call.fetcherId}`] : []),
1218
+ ...call.requestDataKeys.map((key) => `arg:${key}`),
1219
+ ...call.resultKeys.map((key) => `res:${key}`),
1220
+ ],
1221
+ }));
1222
+ const asyncItems: TApiRequestItem[] = session.traces
1223
+ .filter((trace) => trace.kind === 'async')
1224
+ .map((trace) => ({
1225
+ id: trace.id,
1226
+ groupLabel: 'Async request',
1227
+ durationMs: trace.durationMs,
1228
+ errorMessage: trace.errorMessage || trace.trace?.errorMessage,
1229
+ finishedAt: trace.finishedAt,
1230
+ label: trace.label,
1231
+ method: trace.method,
1232
+ path: trace.path,
1233
+ requestData: getTraceRequestData(trace.trace),
1234
+ result: getTraceResultData(trace.trace),
1235
+ startedAt: trace.startedAt,
1236
+ statusCode: trace.trace?.statusCode,
1237
+ statusLabel: trace.status,
1238
+ tags: [trace.status, ...(trace.requestId ? [`request:${trace.requestId}`] : [])],
1239
+ }));
1240
+ const requestItems = [...syncItems, ...asyncItems];
1241
+ const [selectedRequestId, setSelectedRequestId] = React.useState<string | undefined>(() => requestItems[0]?.id);
1242
+
1243
+ React.useEffect(() => {
1244
+ if (requestItems.some((item) => item.id === selectedRequestId)) return;
1245
+ setSelectedRequestId(requestItems[0]?.id);
1246
+ }, [requestItems, selectedRequestId]);
1247
+
1248
+ const selectedItem = requestItems.find((item) => item.id === selectedRequestId) || requestItems[0];
1249
+
1250
+ return (
1251
+ <div className="proteum-profiler__requestWorkspace">
1252
+ <div className="proteum-profiler__requestGroups">
1253
+ <div className="proteum-profiler__requestGroup">
1254
+ <div className="proteum-profiler__requestGroupHeader">
1255
+ <div className="proteum-profiler__sectionTitle">Synchronous calls</div>
1256
+ <div className="proteum-profiler__requestGroupCount">
1257
+ {syncItems.length} item{syncItems.length === 1 ? '' : 's'}
1258
+ </div>
1259
+ </div>
1260
+
1261
+ {syncItems.length === 0 ? (
1262
+ <div className="proteum-profiler__empty">No synchronous SSR or batched API calls captured.</div>
1263
+ ) : (
1264
+ <div className="proteum-profiler__list">
1265
+ {syncItems.map((item) => (
1266
+ <ApiRequestListEntry
1267
+ isSelected={item.id === selectedItem?.id}
1268
+ item={item}
1269
+ key={item.id}
1270
+ onSelect={() => setSelectedRequestId(item.id)}
1271
+ />
1272
+ ))}
1273
+ </div>
1274
+ )}
1275
+ </div>
1276
+
1277
+ <div className="proteum-profiler__requestGroup">
1278
+ <div className="proteum-profiler__requestGroupHeader">
1279
+ <div className="proteum-profiler__sectionTitle">Async requests</div>
1280
+ <div className="proteum-profiler__requestGroupCount">
1281
+ {asyncItems.length} item{asyncItems.length === 1 ? '' : 's'}
1282
+ </div>
1283
+ </div>
1284
+
1285
+ {asyncItems.length === 0 ? (
1286
+ <div className="proteum-profiler__empty">No async API calls captured.</div>
1287
+ ) : (
1288
+ <div className="proteum-profiler__list">
1289
+ {asyncItems.map((item) => (
1290
+ <ApiRequestListEntry
1291
+ isSelected={item.id === selectedItem?.id}
1292
+ item={item}
1293
+ key={item.id}
1294
+ onSelect={() => setSelectedRequestId(item.id)}
1295
+ />
1296
+ ))}
1297
+ </div>
1298
+ )}
1299
+ </div>
1300
+ </div>
1301
+
1302
+ <ApiRequestSidebar item={selectedItem} />
1303
+ </div>
1304
+ );
1305
+ };
1306
+
1307
+ const getTraceEventKey = (traceId: string, event: TRequestTrace['events'][number]) => `${traceId}:${event.index}`;
1308
+
1309
+ const TraceEventSidebar = ({
1310
+ event,
1311
+ label,
1312
+ trace,
1313
+ }: {
1314
+ event?: TRequestTrace['events'][number];
1315
+ label: string;
1316
+ trace?: TRequestTrace;
1317
+ }) => {
1318
+ if (!event) {
1319
+ return (
1320
+ <aside className="proteum-profiler__sidebar">
1321
+ <div className="proteum-profiler__sidebarScroller">
1322
+ <div className="proteum-profiler__sidebarHeader">
1323
+ <div className="proteum-profiler__sidebarEyebrow">{label}</div>
1324
+ <div className="proteum-profiler__sidebarEmpty">Select an event to inspect its timing and payload.</div>
1325
+ </div>
1326
+ </div>
1327
+ </aside>
1328
+ );
1329
+ }
1330
+
1331
+ return (
1332
+ <aside className="proteum-profiler__sidebar">
1333
+ <div className="proteum-profiler__sidebarScroller">
1334
+ <div className="proteum-profiler__sidebarHeader">
1335
+ <div className="proteum-profiler__sidebarEyebrow">{label}</div>
1336
+ <div className="proteum-profiler__sidebarTitle">
1337
+ <strong>{event.type}</strong>
1338
+ </div>
1339
+ {trace ? (
1340
+ <div className="proteum-profiler__mono proteum-profiler__muted">
1341
+ {formatProfilerRequestReference({
1342
+ method: trace.method,
1343
+ path: trace.path,
1344
+ requestData: getTraceRequestData(trace),
1345
+ })}
1346
+ </div>
1347
+ ) : null}
1348
+ </div>
1349
+
1350
+ <div className="proteum-profiler__metrics">
1351
+ <SummaryRow label="Elapsed" value={formatDuration(event.elapsedMs)} />
1352
+ <SummaryRow label="Captured" value={formatTimestamp(event.at)} />
1353
+ <SummaryRow label="Trace" value={trace?.id || 'n/a'} />
1354
+ </div>
1355
+
1356
+ <div className="proteum-profiler__sidebarSection">
1357
+ <div className="proteum-profiler__sidebarSectionTitle">Summary</div>
1358
+ <div className="proteum-profiler__tags">
1359
+ {Object.entries(event.details).map(([key, value]) => (
1360
+ <span className="proteum-profiler__tag" key={`${trace?.id || 'trace'}:${event.index}:detail:${key}`}>
1361
+ {key}:{truncate(renderSummaryValue(value), 72)}
1362
+ </span>
1363
+ ))}
1364
+ </div>
1365
+ </div>
1366
+
1367
+ <div className="proteum-profiler__sidebarSection">
1368
+ <div className="proteum-profiler__sidebarSectionTitle">Details</div>
1369
+ <JsonCodeBlock value={formatTraceEventDetailsJson(event.details)} />
1370
+ </div>
1371
+ </div>
1372
+ </aside>
1373
+ );
1374
+ };
1375
+
1376
+ const TraceRows = ({
1377
+ onSelect,
1378
+ selectedEventKey,
1379
+ trace,
1380
+ }: {
1381
+ onSelect: (selectionKey: string) => void;
1382
+ selectedEventKey?: string;
1383
+ trace: TRequestTrace;
1384
+ }) => (
1385
+ <div className="proteum-profiler__section">
1386
+ <div className="proteum-profiler__sectionHeader">
1387
+ <div className="proteum-profiler__sectionTitle">
1388
+ {formatProfilerRequestReference({
1389
+ method: trace.method,
1390
+ path: trace.path,
1391
+ requestData: getTraceRequestData(trace),
1392
+ })}
1393
+ </div>
1394
+ <div className="proteum-profiler__mono proteum-profiler__muted">{trace.id}</div>
1395
+ </div>
1396
+
1397
+ {trace.calls.length > 0 && (
1398
+ <div className="proteum-profiler__list">
1399
+ {trace.calls.map((call) => (
1400
+ <div className="proteum-profiler__row" key={call.id}>
1401
+ <div className="proteum-profiler__rowHeader">
1402
+ <strong>{formatTraceCallDisplay(call)}</strong>
1403
+ <span className="proteum-profiler__mono proteum-profiler__muted">
1404
+ {formatDuration(call.durationMs)}
1405
+ {call.statusCode !== undefined ? ` | ${call.statusCode}` : ''}
1406
+ </span>
1407
+ </div>
1408
+ <div className="proteum-profiler__tags">
1409
+ <span className="proteum-profiler__tag">{call.origin}</span>
1410
+ {call.fetcherId ? <span className="proteum-profiler__tag">fetcher:{call.fetcherId}</span> : null}
1411
+ {call.requestDataKeys.map((key) => (
1412
+ <span className="proteum-profiler__tag" key={`${call.id}:req:${key}`}>
1413
+ req:{key}
1414
+ </span>
1415
+ ))}
1416
+ {call.resultKeys.map((key) => (
1417
+ <span className="proteum-profiler__tag" key={`${call.id}:res:${key}`}>
1418
+ res:{key}
1419
+ </span>
1420
+ ))}
1421
+ {call.errorMessage ? <span className="proteum-profiler__tag">{truncate(call.errorMessage, 72)}</span> : null}
1422
+ </div>
1423
+ </div>
1424
+ ))}
1425
+ </div>
1426
+ )}
1427
+
1428
+ <div className="proteum-profiler__list">
1429
+ {trace.events.map((event) => {
1430
+ const selectionKey = getTraceEventKey(trace.id, event);
1431
+
1432
+ return (
1433
+ <TraceEventEntry
1434
+ event={event}
1435
+ isSelected={selectionKey === selectedEventKey}
1436
+ key={selectionKey}
1437
+ onSelect={() => onSelect(selectionKey)}
1438
+ traceId={trace.id}
1439
+ />
1440
+ );
1441
+ })}
1442
+ </div>
1443
+ </div>
1444
+ );
1445
+
1446
+ const AuthTraceSection = ({
1447
+ authEvents,
1448
+ label,
1449
+ onSelect,
1450
+ selectedEventKey,
1451
+ trace,
1452
+ }: {
1453
+ authEvents: TRequestTrace['events'];
1454
+ label: string;
1455
+ onSelect: (selectionKey: string) => void;
1456
+ selectedEventKey?: string;
1457
+ trace: TRequestTrace;
1458
+ }) => (
1459
+ <div className="proteum-profiler__section">
1460
+ <div className="proteum-profiler__sectionHeader">
1461
+ <div>
1462
+ <div className="proteum-profiler__sectionTitle">{label}</div>
1463
+ <div className="proteum-profiler__mono proteum-profiler__muted">
1464
+ {formatProfilerRequestReference({
1465
+ method: trace.method,
1466
+ path: trace.path,
1467
+ requestData: getTraceRequestData(trace),
1468
+ })}
1469
+ </div>
1470
+ </div>
1471
+ <div className="proteum-profiler__actions">
1472
+ <span className="proteum-profiler__tag">capture:{trace.capture}</span>
1473
+ <span className="proteum-profiler__tag">events:{authEvents.length}</span>
1474
+ {trace.statusCode !== undefined ? <span className="proteum-profiler__tag">status:{trace.statusCode}</span> : null}
1475
+ </div>
1476
+ </div>
1477
+
1478
+ <div className="proteum-profiler__list">
1479
+ {authEvents.map((event) => {
1480
+ const selectionKey = getTraceEventKey(trace.id, event);
1481
+
1482
+ return (
1483
+ <TraceEventEntry
1484
+ event={event}
1485
+ isSelected={selectionKey === selectedEventKey}
1486
+ key={selectionKey}
1487
+ onSelect={() => onSelect(selectionKey)}
1488
+ traceId={trace.id}
1489
+ />
1490
+ );
1491
+ })}
1492
+ </div>
1493
+ </div>
1494
+ );
1495
+
1496
+ const TraceEventEntry = ({
1497
+ event,
1498
+ isSelected,
1499
+ onSelect,
1500
+ traceId,
1501
+ }: {
1502
+ event: TRequestTrace['events'][number];
1503
+ isSelected: boolean;
1504
+ onSelect: () => void;
1505
+ traceId: string;
1506
+ }) => {
1507
+ const depth = getTraceEventDepth(event);
1508
+
1509
+ return (
1510
+ <button
1511
+ aria-pressed={isSelected}
1512
+ className={`proteum-profiler__row proteum-profiler__row--interactive proteum-profiler__traceEventRow ${isSelected ? 'proteum-profiler__row--selected' : ''}`}
1513
+ onClick={onSelect}
1514
+ style={
1515
+ {
1516
+ '--profiler-trace-depth': depth,
1517
+ '--profiler-trace-guide-opacity': depth > 0 ? 1 : 0,
1518
+ } as React.CSSProperties
1519
+ }
1520
+ type="button"
1521
+ >
1522
+ <div className="proteum-profiler__rowHeader">
1523
+ <strong>{event.type}</strong>
1524
+ <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(event.elapsedMs)}</span>
1525
+ </div>
1526
+ <div className="proteum-profiler__tags">
1527
+ {Object.entries(event.details).map(([key, value]) => (
1528
+ <span className="proteum-profiler__tag" key={`${traceId}:${event.index}:${key}`}>
1529
+ {key}:{truncate(renderSummaryValue(value), 72)}
1530
+ </span>
1531
+ ))}
1532
+ </div>
1533
+ </button>
1534
+ );
1535
+ };
1536
+
1537
+ type TTraceEventInspectorSelection = {
1538
+ event: TRequestTrace['events'][number];
1539
+ key: string;
1540
+ label: string;
1541
+ trace: TRequestTrace;
1542
+ };
1543
+
1544
+ const readDateMs = (value?: string) => {
1545
+ if (!value) return undefined;
1546
+ const ms = new Date(value).valueOf();
1547
+ return Number.isFinite(ms) ? ms : undefined;
1548
+ };
1549
+
1550
+ const getTimelineDurationColor = (durationMs?: number) => {
1551
+ if (durationMs === undefined) return '#93c5fd';
1552
+ if (durationMs >= 800) return '#ef4444';
1553
+ if (durationMs >= 450) return '#f97316';
1554
+ if (durationMs >= 220) return '#f59e0b';
1555
+ if (durationMs >= 100) return '#3b82f6';
1556
+ return '#22c55e';
1557
+ };
1558
+
1559
+ const escapeHtml = (value: string) =>
1560
+ value
1561
+ .replace(/&/g, '&amp;')
1562
+ .replace(/</g, '&lt;')
1563
+ .replace(/>/g, '&gt;')
1564
+ .replace(/"/g, '&quot;')
1565
+ .replace(/'/g, '&#39;');
1566
+
1567
+ const timelineWaterfallMinDurationMs = 6;
1568
+ const timelineWaterfallBarHeight = 15;
1569
+ const timelineWaterfallRowGap = 1;
1570
+ const timelineWaterfallRowHeight = timelineWaterfallBarHeight + timelineWaterfallRowGap;
1571
+
1572
+ const TimelineChart = ({ session }: { session: TProfilerNavigationSession }) => {
1573
+ const [ApexChartComponent, setApexChartComponent] = React.useState<unknown>(null);
1574
+
1575
+ React.useEffect(() => {
1576
+ let isDisposed = false;
1577
+
1578
+ void import('react-apexcharts').then((module) => {
1579
+ if (isDisposed) return;
1580
+ setApexChartComponent(() => module.default);
1581
+ });
1582
+
1583
+ return () => {
1584
+ isDisposed = true;
1585
+ };
1586
+ }, []);
1587
+
1588
+ const sessionStartMs = readDateMs(session.startedAt) ?? 0;
1589
+ const rawItems = session.traces.flatMap((traceItem) => {
1590
+ const trace = traceItem.trace;
1591
+ if (!trace) return [];
1592
+
1593
+ const traceStartMs = readDateMs(trace.startedAt) ?? sessionStartMs;
1594
+ const traceFinishedMs = readDateMs(trace.finishedAt) ?? (trace.durationMs !== undefined ? traceStartMs + trace.durationMs : undefined);
1595
+ const traceLabel = formatSessionTraceDisplay(traceItem);
1596
+
1597
+ return trace.events.map((event, index): Omit<TTimelineWaterfallEventItem, 'chartLabel' | 'color' | 'endOffsetMs' | 'startOffsetMs'> => {
1598
+ const nextEvent = trace.events[index + 1];
1599
+ const startMs = readDateMs(event.at) ?? traceStartMs + event.elapsedMs;
1600
+ const nextStartMs = nextEvent ? readDateMs(nextEvent.at) ?? traceStartMs + nextEvent.elapsedMs : undefined;
1601
+ const endMs = Math.max(startMs + 1, nextStartMs ?? traceFinishedMs ?? startMs + 1);
1602
+
1603
+ return {
1604
+ durationMs: Math.max(1, endMs - startMs),
1605
+ endMs,
1606
+ event,
1607
+ startMs,
1608
+ traceLabel,
1609
+ };
1610
+ });
1611
+ });
1612
+
1613
+ const sortedItems = [...rawItems].sort((left, right) => left.startMs - right.startMs || left.event.index - right.event.index);
1614
+ const chartStartMs = sortedItems.length > 0 ? Math.min(...sortedItems.map((item) => item.startMs)) : 0;
1615
+ const chartEndMs = sortedItems.length > 0 ? Math.max(...sortedItems.map((item) => item.endMs)) : chartStartMs + 1;
1616
+ const totalDurationMs = Math.max(chartEndMs - chartStartMs, 1);
1617
+ const items: TTimelineWaterfallEventItem[] = sortedItems
1618
+ .filter((item) => item.durationMs >= timelineWaterfallMinDurationMs)
1619
+ .map((item) => ({
1620
+ ...item,
1621
+ chartLabel: truncate(`${item.event.type} | ${item.traceLabel}`, 84),
1622
+ color: getTimelineDurationColor(item.durationMs),
1623
+ endOffsetMs: item.endMs - chartStartMs,
1624
+ startOffsetMs: item.startMs - chartStartMs,
1625
+ }));
1626
+ const chartHeight = Math.max(260, items.length * timelineWaterfallRowHeight + 24);
1627
+ const ChartComponent = ApexChartComponent as any;
1628
+
1629
+ const series = [
1630
+ {
1631
+ data: items.map((item) => ({
1632
+ fillColor: item.color,
1633
+ x: item.chartLabel,
1634
+ y: [item.startOffsetMs, item.endOffsetMs],
1635
+ })),
1636
+ name: 'Timeline events',
1637
+ },
1638
+ ];
1639
+
1640
+ const options = {
1641
+ chart: {
1642
+ animations: { enabled: false },
1643
+ background: 'transparent',
1644
+ foreColor: '#627186',
1645
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
1646
+ toolbar: { show: false },
1647
+ type: 'rangeBar',
1648
+ zoom: { enabled: false },
1649
+ },
1650
+ dataLabels: {
1651
+ enabled: false,
1652
+ },
1653
+ fill: {
1654
+ opacity: 1,
1655
+ },
1656
+ grid: {
1657
+ borderColor: 'rgba(19, 32, 51, 0.08)',
1658
+ padding: { bottom: 0, left: 0, right: 0, top: 4 },
1659
+ xaxis: { lines: { show: true } },
1660
+ yaxis: { lines: { show: false } },
1661
+ },
1662
+ legend: {
1663
+ show: false,
1664
+ },
1665
+ noData: {
1666
+ style: {
1667
+ color: '#627186',
1668
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
1669
+ fontSize: '11px',
1670
+ },
1671
+ text: 'No timeline events captured.',
1672
+ },
1673
+ plotOptions: {
1674
+ bar: {
1675
+ barHeight: timelineWaterfallBarHeight,
1676
+ borderRadius: 2,
1677
+ horizontal: true,
1678
+ rangeBarGroupRows: false,
1679
+ },
1680
+ },
1681
+ stroke: {
1682
+ colors: ['#ffffff'],
1683
+ width: 1,
1684
+ },
1685
+ tooltip: {
1686
+ custom: ({ dataPointIndex }: { dataPointIndex: number }) => {
1687
+ const item = items[dataPointIndex];
1688
+ if (!item) return '';
1689
+
1690
+ return `
1691
+ <div style="padding:8px 10px; color:#132033; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace; font-size:11px; line-height:1.5;">
1692
+ <div style="font-weight:700;">${escapeHtml(item.event.type)}</div>
1693
+ <div style="color:#627186;">${escapeHtml(item.traceLabel)}</div>
1694
+ <div style="margin-top:6px; color:#627186;">Start: +${Math.round(item.startOffsetMs)} ms</div>
1695
+ <div style="color:#627186;">End: +${Math.round(item.endOffsetMs)} ms</div>
1696
+ <div style="color:#627186;">Span: ${escapeHtml(formatDuration(item.durationMs))}</div>
1697
+ </div>
1698
+ `;
1699
+ },
1700
+ },
1701
+ xaxis: {
1702
+ axisBorder: { show: false },
1703
+ axisTicks: { show: false },
1704
+ labels: {
1705
+ formatter: (value: string | number) => `${Math.round(Number(value))} ms`,
1706
+ style: {
1707
+ colors: '#627186',
1708
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
1709
+ fontSize: '10px',
1710
+ },
1711
+ },
1712
+ max: totalDurationMs,
1713
+ min: 0,
1714
+ tickAmount: Math.min(6, Math.max(2, items.length > 0 ? 6 : 2)),
1715
+ type: 'numeric',
1716
+ },
1717
+ yaxis: {
1718
+ show: false,
1719
+ labels: {
1720
+ show: false,
1721
+ },
1722
+ },
1723
+ };
1724
+
1725
+ return (
1726
+ <div className="proteum-profiler__section">
1727
+ <div className="proteum-profiler__timelineChart">
1728
+ <div className="proteum-profiler__timelineChartMeta">
1729
+ <span className="proteum-profiler__mono proteum-profiler__muted">
1730
+ {items.length} event{items.length === 1 ? '' : 's'}
1731
+ </span>
1732
+ <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(totalDurationMs)}</span>
1733
+ </div>
1734
+
1735
+ <div className="proteum-profiler__timelineChartCanvas" style={{ height: `${chartHeight}px` }}>
1736
+ {ChartComponent && items.length > 0 ? (
1737
+ <ChartComponent height={chartHeight} options={options} series={series} type="rangeBar" width="100%" />
1738
+ ) : items.length > 0 ? (
1739
+ <div className="proteum-profiler__empty">Loading waterfall chart...</div>
1740
+ ) : (
1741
+ <div className="proteum-profiler__empty">No timeline events were captured for this session.</div>
1742
+ )}
1743
+ </div>
1744
+ </div>
1745
+ </div>
1746
+ );
1747
+ };
1748
+
1749
+ const TimelinePanel = ({ session }: { session: TProfilerNavigationSession }) => {
1750
+ const selections: TTraceEventInspectorSelection[] = session.traces.flatMap((traceItem) =>
1751
+ traceItem.trace
1752
+ ? traceItem.trace.events.map((event) => ({
1753
+ event,
1754
+ key: getTraceEventKey(traceItem.trace!.id, event),
1755
+ label: formatSessionTraceDisplay(traceItem),
1756
+ trace: traceItem.trace!,
1757
+ }))
1758
+ : [],
1759
+ );
1760
+ const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(() => selections[0]?.key);
1761
+
1762
+ React.useEffect(() => {
1763
+ if (selections.some((selection) => selection.key === selectedEventKey)) return;
1764
+ setSelectedEventKey(selections[0]?.key);
1765
+ }, [selectedEventKey, selections]);
1766
+
1767
+ const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
1768
+
1769
+ return (
1770
+ <div className="proteum-profiler__splitColumn">
1771
+ <TimelineChart session={session} />
1772
+ <div className="proteum-profiler__splitView proteum-profiler__splitView--stacked">
1773
+ <div className="proteum-profiler__splitColumn">
1774
+ <div className="proteum-profiler__section">
1775
+ <div className="proteum-profiler__titleRow">
1776
+ <div className="proteum-profiler__sectionTitle">Navigation steps</div>
1777
+ </div>
1778
+ <div className="proteum-profiler__list">
1779
+ {session.steps.map((step) => (
1780
+ <div className="proteum-profiler__row" key={step.id}>
1781
+ <div className="proteum-profiler__rowHeader">
1782
+ <strong>{step.label}</strong>
1783
+ <span className="proteum-profiler__mono proteum-profiler__muted">{formatDuration(step.durationMs)}</span>
1784
+ </div>
1785
+ <div className="proteum-profiler__tags">
1786
+ <span className="proteum-profiler__tag">{step.status}</span>
1787
+ {Object.entries(step.details || {}).map(([key, value]) => (
1788
+ <span className="proteum-profiler__tag" key={`${step.id}:${key}`}>
1789
+ {key}:{String(value)}
1790
+ </span>
1791
+ ))}
1792
+ {step.errorMessage ? <span className="proteum-profiler__tag">{truncate(step.errorMessage, 72)}</span> : null}
1793
+ </div>
1794
+ </div>
1795
+ ))}
1796
+ </div>
1797
+ </div>
1798
+
1799
+ {session.traces.map((traceItem) =>
1800
+ traceItem.trace ? (
1801
+ <TraceRows
1802
+ key={traceItem.id}
1803
+ onSelect={setSelectedEventKey}
1804
+ selectedEventKey={selectedEventKey}
1805
+ trace={traceItem.trace}
1806
+ />
1807
+ ) : (
1808
+ <div className="proteum-profiler__row" key={traceItem.id}>
1809
+ <div className="proteum-profiler__rowHeader">
1810
+ <strong>{formatSessionTraceDisplay(traceItem)}</strong>
1811
+ <span className="proteum-profiler__mono proteum-profiler__muted">{traceItem.status}</span>
1812
+ </div>
1813
+ <div className="proteum-profiler__mono">
1814
+ {formatProfilerRequestReference({
1815
+ fallbackLabel: traceItem.label,
1816
+ method: traceItem.method,
1817
+ path: traceItem.path,
1818
+ requestData: getTraceRequestData(traceItem.trace),
1819
+ })}
1820
+ </div>
1821
+ </div>
1822
+ ),
1823
+ )}
1824
+ </div>
1825
+
1826
+ <TraceEventSidebar event={selected?.event} label={selected?.label || 'Trace event'} trace={selected?.trace} />
1827
+ </div>
1828
+ </div>
1829
+ );
1830
+ };
1831
+
1832
+ const AuthPanel = ({ session }: { session: TProfilerNavigationSession }) => {
1833
+ const authSections = session.traces.flatMap((traceItem) => {
1834
+ const authEvents = traceItem.trace ? findTraceEvents(traceItem.trace, authEventTypes) : [];
1835
+ return traceItem.trace && authEvents.length > 0
1836
+ ? [{ authEvents, id: traceItem.id, label: formatSessionTraceDisplay(traceItem), trace: traceItem.trace }]
1837
+ : [];
1838
+ });
1839
+ const selections: TTraceEventInspectorSelection[] = authSections.flatMap((section) =>
1840
+ section.authEvents.map((event) => ({
1841
+ event,
1842
+ key: getTraceEventKey(section.trace.id, event),
1843
+ label: `${section.label} event`,
1844
+ trace: section.trace,
1845
+ })),
1846
+ );
1847
+ const [selectedEventKey, setSelectedEventKey] = React.useState<string | undefined>(() => selections[0]?.key);
1848
+
1849
+ React.useEffect(() => {
1850
+ if (selections.some((selection) => selection.key === selectedEventKey)) return;
1851
+ setSelectedEventKey(selections[0]?.key);
1852
+ }, [selectedEventKey, selections]);
1853
+
1854
+ if (authSections.length === 0) return <div className="proteum-profiler__empty">No auth activity was captured for this session.</div>;
1855
+
1856
+ const selected = selections.find((selection) => selection.key === selectedEventKey) || selections[0];
1857
+
1858
+ return (
1859
+ <div className="proteum-profiler__splitView">
1860
+ <div className="proteum-profiler__splitColumn">
1861
+ {authSections.map((section) => (
1862
+ <AuthTraceSection
1863
+ authEvents={section.authEvents}
1864
+ key={section.id}
1865
+ label={section.label}
1866
+ onSelect={setSelectedEventKey}
1867
+ selectedEventKey={selectedEventKey}
1868
+ trace={section.trace}
1869
+ />
1870
+ ))}
1871
+ </div>
1872
+
1873
+ <TraceEventSidebar event={selected?.event} label={selected?.label || 'Auth event'} trace={selected?.trace} />
1874
+ </div>
1875
+ );
1876
+ };
1877
+
1878
+ const SimpleSection = ({
1879
+ empty,
1880
+ rows,
1881
+ showTitle = true,
1882
+ title,
1883
+ }: {
1884
+ empty: string;
1885
+ rows: Array<{ key: string; title: string; value: string }>;
1886
+ showTitle?: boolean;
1887
+ title: string;
1888
+ }) => (
1889
+ <div className="proteum-profiler__section">
1890
+ {showTitle ? (
1891
+ <div className="proteum-profiler__titleRow">
1892
+ <div className="proteum-profiler__sectionTitle">{title}</div>
1893
+ </div>
1894
+ ) : null}
1895
+ {rows.length === 0 ? (
1896
+ <div className="proteum-profiler__empty">{empty}</div>
1897
+ ) : (
1898
+ <div className="proteum-profiler__list">
1899
+ {rows.map((row) => (
1900
+ <div className="proteum-profiler__row" key={row.key}>
1901
+ <div className="proteum-profiler__rowHeader">
1902
+ <strong>{row.title}</strong>
1903
+ </div>
1904
+ <div className="proteum-profiler__mono">{row.value}</div>
1905
+ </div>
1906
+ ))}
1907
+ </div>
1908
+ )}
1909
+ </div>
1910
+ );
1911
+
1912
+ const TextBlocks = ({ blocks }: { blocks: THumanTextBlock[] }) => (
1913
+ <>
1914
+ {blocks.map((block) => (
1915
+ <div className="proteum-profiler__section" key={block.title}>
1916
+ <div className="proteum-profiler__titleRow">
1917
+ <div className="proteum-profiler__sectionTitle">{block.title}</div>
1918
+ </div>
1919
+ {block.items.length === 0 ? (
1920
+ <div className="proteum-profiler__empty">{block.empty || 'none'}</div>
1921
+ ) : (
1922
+ <div className="proteum-profiler__list">
1923
+ {block.items.map((item, index) => (
1924
+ <div className="proteum-profiler__row" key={`${block.title}:${index}`}>
1925
+ <div className="proteum-profiler__mono">{item}</div>
1926
+ </div>
1927
+ ))}
1928
+ </div>
1929
+ )}
1930
+ </div>
1931
+ ))}
1932
+ </>
1933
+ );
1934
+
1935
+ const renderPanel = (panel: TProfilerPanel, session: TProfilerNavigationSession, summary: TSessionSummary, state: TProfilerState) => {
1936
+ const primaryTrace = summary.primaryTrace?.trace;
1937
+
1938
+ if (panel === 'summary') {
1939
+ return (
1940
+ <div className="proteum-profiler__metrics">
1941
+ <SummaryRow label="Session" value={session.label} />
1942
+ <SummaryRow label="Status" value={summary.statusLabel} />
1943
+ <SummaryRow label="Duration" value={formatDuration(summary.totalMs)} />
1944
+ <SummaryRow label="Route" value={summary.routeLabel} />
1945
+ <SummaryRow
1946
+ label="SSR"
1947
+ value={
1948
+ summary.ssrPayloadBytes !== undefined
1949
+ ? `${formatDuration(summary.renderMs)} | ${formatBytes(summary.ssrPayloadBytes)}`
1950
+ : formatDuration(summary.renderMs)
1951
+ }
1952
+ />
1953
+ <SummaryRow label="API" value={`sync ${summary.apiSyncCount} / async ${summary.apiAsyncCount}`} />
1954
+ <SummaryRow label="Errors" value={String(summary.errorCount)} />
1955
+ <SummaryRow label="Request" value={session.requestId || 'client-only'} />
1956
+ </div>
1957
+ );
1958
+ }
1959
+
1960
+ if (panel === 'timeline') {
1961
+ return <TimelinePanel session={session} />;
1962
+ }
1963
+
1964
+ if (panel === 'auth') {
1965
+ return <AuthPanel session={session} />;
1966
+ }
1967
+
1968
+ if (panel === 'routing') {
1969
+ return (
1970
+ <SimpleSection
1971
+ empty="No routing data captured yet."
1972
+ rows={findTraceEvents(primaryTrace, [
1973
+ 'resolve.start',
1974
+ 'resolve.controller-route',
1975
+ 'resolve.route-match',
1976
+ 'resolve.routes-evaluated',
1977
+ 'resolve.not-found',
1978
+ ]).map((event) => ({
1979
+ key: `${event.index}:${event.type}`,
1980
+ title: event.type,
1981
+ value: Object.entries(event.details)
1982
+ .map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
1983
+ .join(' '),
1984
+ }))}
1985
+ showTitle={false}
1986
+ title="Routing"
1987
+ />
1988
+ );
1989
+ }
1990
+
1991
+ if (panel === 'controller') {
1992
+ return (
1993
+ <SimpleSection
1994
+ empty="No controller data captured yet."
1995
+ rows={findTraceEvents(primaryTrace, ['controller.start', 'controller.result', 'setup.options', 'context.create']).map(
1996
+ (event) => ({
1997
+ key: `${event.index}:${event.type}`,
1998
+ title: event.type,
1999
+ value: Object.entries(event.details)
2000
+ .map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
2001
+ .join(' '),
2002
+ }),
2003
+ )}
2004
+ showTitle={false}
2005
+ title="Controller"
2006
+ />
2007
+ );
2008
+ }
2009
+
2010
+ if (panel === 'ssr') {
2011
+ return (
2012
+ <SimpleSection
2013
+ empty="No SSR data captured for this session."
2014
+ rows={findTraceEvents(primaryTrace, ['page.data', 'ssr.payload', 'render.start', 'render.end']).map((event) => ({
2015
+ key: `${event.index}:${event.type}`,
2016
+ title: event.type,
2017
+ value: Object.entries(event.details)
2018
+ .map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
2019
+ .join(' '),
2020
+ }))}
2021
+ showTitle={false}
2022
+ title="SSR"
2023
+ />
2024
+ );
2025
+ }
2026
+
2027
+ if (panel === 'api') {
2028
+ return <ApiPanel session={session} />;
2029
+ }
2030
+
2031
+ if (panel === 'explain') {
2032
+ const explain = state.explain;
2033
+ const blocks = explain.manifest
2034
+ ? [
2035
+ { title: 'Overview', items: buildExplainSummaryItems(explain.manifest) },
2036
+ ...buildExplainBlocks(explain.manifest, [...explainSectionNames]),
2037
+ ]
2038
+ : [];
2039
+
2040
+ return (
2041
+ <div className="proteum-profiler__section">
2042
+ <div className="proteum-profiler__sectionHeader">
2043
+ <div className="proteum-profiler__sectionTitle">Explain</div>
2044
+ <div className="proteum-profiler__actions">
2045
+ <button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshExplain()} type="button">
2046
+ Refresh
2047
+ </button>
2048
+ </div>
2049
+ </div>
2050
+
2051
+ {explain.errorMessage ? (
2052
+ <div className="proteum-profiler__row">
2053
+ <div className="proteum-profiler__rowHeader">
2054
+ <strong>Last explain panel error</strong>
2055
+ </div>
2056
+ <div className="proteum-profiler__mono">{explain.errorMessage}</div>
2057
+ </div>
2058
+ ) : null}
2059
+
2060
+ {explain.status === 'loading' && !explain.manifest ? (
2061
+ <div className="proteum-profiler__empty">Loading explain data...</div>
2062
+ ) : !explain.manifest ? (
2063
+ <div className="proteum-profiler__empty">No explain manifest is available.</div>
2064
+ ) : (
2065
+ <>
2066
+ <div className="proteum-profiler__row">
2067
+ <div className="proteum-profiler__rowHeader">
2068
+ <strong>Manifest snapshot</strong>
2069
+ <span className="proteum-profiler__mono proteum-profiler__muted">
2070
+ {explain.lastLoadedAt ? formatTimestamp(explain.lastLoadedAt) : 'Not loaded'}
2071
+ </span>
2072
+ </div>
2073
+ <div className="proteum-profiler__mono">
2074
+ Same manifest-backed sections as `proteum explain`, rendered from the shared diagnostics contract.
2075
+ </div>
2076
+ </div>
2077
+ <TextBlocks blocks={blocks} />
2078
+ </>
2079
+ )}
2080
+ </div>
2081
+ );
2082
+ }
2083
+
2084
+ if (panel === 'doctor') {
2085
+ const doctor = state.doctor;
2086
+ const doctorRows =
2087
+ doctor.response?.diagnostics.map((diagnostic, index) => ({
2088
+ key: `${diagnostic.code}:${index}`,
2089
+ title: `[${diagnostic.level}] ${diagnostic.code}`,
2090
+ value: `${diagnostic.message} source=${diagnostic.filepath}${formatManifestLocation(
2091
+ diagnostic.sourceLocation?.line,
2092
+ diagnostic.sourceLocation?.column,
2093
+ )}${diagnostic.relatedFilepaths?.length ? ` related=${diagnostic.relatedFilepaths.join(',')}` : ''}`,
2094
+ })) || [];
2095
+ const doctorBlocks = state.explain.manifest ? buildDoctorBlocks(state.explain.manifest) : [];
2096
+
2097
+ return (
2098
+ <div className="proteum-profiler__section">
2099
+ <div className="proteum-profiler__sectionHeader">
2100
+ <div className="proteum-profiler__sectionTitle">Doctor</div>
2101
+ <div className="proteum-profiler__actions">
2102
+ <button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshDoctor()} type="button">
2103
+ Refresh
2104
+ </button>
2105
+ </div>
2106
+ </div>
2107
+
2108
+ {doctor.errorMessage ? (
2109
+ <div className="proteum-profiler__row">
2110
+ <div className="proteum-profiler__rowHeader">
2111
+ <strong>Last doctor panel error</strong>
2112
+ </div>
2113
+ <div className="proteum-profiler__mono">{doctor.errorMessage}</div>
2114
+ </div>
2115
+ ) : null}
2116
+
2117
+ {doctor.status === 'loading' && !doctor.response ? (
2118
+ <div className="proteum-profiler__empty">Loading doctor diagnostics...</div>
2119
+ ) : !doctor.response ? (
2120
+ <div className="proteum-profiler__empty">No doctor diagnostics are available.</div>
2121
+ ) : (
2122
+ <>
2123
+ <div className="proteum-profiler__metrics">
2124
+ <SummaryRow label="Errors" value={String(doctor.response.summary.errors)} />
2125
+ <SummaryRow label="Warnings" value={String(doctor.response.summary.warnings)} />
2126
+ <SummaryRow label="Strict" value={doctor.response.summary.strictFailed ? 'failed' : 'ok'} />
2127
+ <SummaryRow
2128
+ label="Refreshed"
2129
+ value={doctor.lastLoadedAt ? formatTimestamp(doctor.lastLoadedAt) : 'Not loaded'}
2130
+ />
2131
+ </div>
2132
+ {doctorBlocks.length > 0 ? (
2133
+ <TextBlocks blocks={doctorBlocks} />
2134
+ ) : (
2135
+ <SimpleSection empty="No manifest diagnostics were found." rows={doctorRows} title="Diagnostics" />
2136
+ )}
2137
+ </>
2138
+ )}
2139
+ </div>
2140
+ );
2141
+ }
2142
+
2143
+ if (panel === 'commands') {
2144
+ const commandsState = state.commands;
2145
+
2146
+ return (
2147
+ <div className="proteum-profiler__section">
2148
+ <div className="proteum-profiler__sectionHeader">
2149
+ <div className="proteum-profiler__sectionTitle">Available commands</div>
2150
+ <div className="proteum-profiler__actions">
2151
+ <button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshCommands()} type="button">
2152
+ Refresh
2153
+ </button>
2154
+ </div>
2155
+ </div>
2156
+
2157
+ <div className="proteum-profiler__row">
2158
+ <div className="proteum-profiler__rowHeader">
2159
+ <strong>Dev commands</strong>
2160
+ <span className="proteum-profiler__mono proteum-profiler__muted">
2161
+ {commandsState.commands.length} command{commandsState.commands.length === 1 ? '' : 's'}
2162
+ </span>
2163
+ </div>
2164
+ <div className="proteum-profiler__mono">
2165
+ Commands live under /commands, extend the Proteum Commands class, and run only in a dev context.
2166
+ </div>
2167
+ </div>
2168
+
2169
+ {commandsState.errorMessage ? (
2170
+ <div className="proteum-profiler__row">
2171
+ <div className="proteum-profiler__rowHeader">
2172
+ <strong>Last command panel error</strong>
2173
+ </div>
2174
+ <div className="proteum-profiler__mono">{commandsState.errorMessage}</div>
2175
+ </div>
2176
+ ) : null}
2177
+
2178
+ {commandsState.status === 'loading' && commandsState.commands.length === 0 ? (
2179
+ <div className="proteum-profiler__empty">Loading commands...</div>
2180
+ ) : commandsState.commands.length === 0 ? (
2181
+ <div className="proteum-profiler__empty">No commands are registered for this app.</div>
2182
+ ) : (
2183
+ <div className="proteum-profiler__list">
2184
+ {commandsState.commands.map((command: TDevCommandDefinition) => {
2185
+ const execution = commandsState.executions[command.path] as TDevCommandExecution | undefined;
2186
+ return (
2187
+ <div className="proteum-profiler__row" key={command.path}>
2188
+ <div className="proteum-profiler__rowHeader">
2189
+ <strong>{command.path}</strong>
2190
+ <div className="proteum-profiler__actions">
2191
+ <span className="proteum-profiler__mono proteum-profiler__muted">
2192
+ {execution ? formatTimestamp(execution.finishedAt) : 'Never run'}
2193
+ </span>
2194
+ <button
2195
+ className="proteum-profiler__pill"
2196
+ onClick={() => void profilerRuntime.runCommand(command.path)}
2197
+ type="button"
2198
+ >
2199
+ Run now
2200
+ </button>
2201
+ </div>
2202
+ </div>
2203
+
2204
+ <div className="proteum-profiler__tags">
2205
+ <span className="proteum-profiler__tag">{command.className}</span>
2206
+ <span className="proteum-profiler__tag">{command.methodName}</span>
2207
+ <span className="proteum-profiler__tag">{command.scope}</span>
2208
+ {execution ? <span className="proteum-profiler__tag">{execution.status}</span> : null}
2209
+ {execution ? (
2210
+ <span className="proteum-profiler__tag">{formatDuration(execution.durationMs)}</span>
2211
+ ) : null}
2212
+ {execution?.errorMessage ? (
2213
+ <span className="proteum-profiler__tag">{truncate(execution.errorMessage, 72)}</span>
2214
+ ) : null}
2215
+ </div>
2216
+
2217
+ <div className="proteum-profiler__mono proteum-profiler__muted">
2218
+ source {command.filepath}:{command.sourceLocation.line}:{command.sourceLocation.column}
2219
+ {commandsState.lastLoadedAt
2220
+ ? ` | refreshed ${formatTimestamp(commandsState.lastLoadedAt)}`
2221
+ : ''}
2222
+ </div>
2223
+
2224
+ {execution ? (
2225
+ <div className="proteum-profiler__section">
2226
+ <div className="proteum-profiler__sectionTitle">Last result</div>
2227
+ <JsonCodeBlock
2228
+ value={
2229
+ execution.result?.json !== undefined
2230
+ ? formatStructuredValue(execution.result.json)
2231
+ : execution.result
2232
+ ? formatStructuredValue(execution.result.summary)
2233
+ : execution.errorMessage || 'undefined'
2234
+ }
2235
+ />
2236
+ </div>
2237
+ ) : null}
2238
+ </div>
2239
+ );
2240
+ })}
2241
+ </div>
2242
+ )}
2243
+ </div>
2244
+ );
2245
+ }
2246
+
2247
+ if (panel === 'cron') {
2248
+ const cron = state.cron;
2249
+
2250
+ return (
2251
+ <div className="proteum-profiler__section">
2252
+ <div className="proteum-profiler__sectionHeader">
2253
+ <div className="proteum-profiler__sectionTitle">Registered tasks</div>
2254
+ <div className="proteum-profiler__actions">
2255
+ <button className="proteum-profiler__pill" onClick={() => void profilerRuntime.refreshCronTasks()} type="button">
2256
+ Refresh
2257
+ </button>
2258
+ </div>
2259
+ </div>
2260
+
2261
+ <div className="proteum-profiler__row">
2262
+ <div className="proteum-profiler__rowHeader">
2263
+ <strong>Dev mode</strong>
2264
+ <span className="proteum-profiler__mono proteum-profiler__muted">
2265
+ {cron.tasks.length} task{cron.tasks.length === 1 ? '' : 's'}
2266
+ </span>
2267
+ </div>
2268
+ <div className="proteum-profiler__mono">
2269
+ Automatic execution is disabled in dev. Registered cron tasks stay visible here and only run when
2270
+ triggered manually.
2271
+ </div>
2272
+ </div>
2273
+
2274
+ {cron.errorMessage ? (
2275
+ <div className="proteum-profiler__row">
2276
+ <div className="proteum-profiler__rowHeader">
2277
+ <strong>Last cron panel error</strong>
2278
+ </div>
2279
+ <div className="proteum-profiler__mono">{cron.errorMessage}</div>
2280
+ </div>
2281
+ ) : null}
2282
+
2283
+ {cron.status === 'loading' && cron.tasks.length === 0 ? (
2284
+ <div className="proteum-profiler__empty">Loading cron tasks...</div>
2285
+ ) : cron.tasks.length === 0 ? (
2286
+ <div className="proteum-profiler__empty">No cron tasks are registered for this app.</div>
2287
+ ) : (
2288
+ <div className="proteum-profiler__list">
2289
+ {cron.tasks.map((task) => (
2290
+ <div className="proteum-profiler__row" key={task.name}>
2291
+ <div className="proteum-profiler__rowHeader">
2292
+ <strong>{task.name}</strong>
2293
+ <div className="proteum-profiler__actions">
2294
+ <span className="proteum-profiler__mono proteum-profiler__muted">
2295
+ {task.running
2296
+ ? 'Running...'
2297
+ : task.lastRunFinishedAt
2298
+ ? formatTimestamp(task.lastRunFinishedAt)
2299
+ : 'Never run'}
2300
+ </span>
2301
+ <button
2302
+ className="proteum-profiler__pill"
2303
+ disabled={task.running}
2304
+ onClick={() => void profilerRuntime.runCronTask(task.name)}
2305
+ type="button"
2306
+ >
2307
+ {task.running ? 'Running...' : 'Run now'}
2308
+ </button>
2309
+ </div>
2310
+ </div>
2311
+
2312
+ <div className="proteum-profiler__tags">
2313
+ <span className="proteum-profiler__tag">schedule:{truncate(formatCronFrequency(task), 64)}</span>
2314
+ <span className="proteum-profiler__tag">
2315
+ next:{task.nextInvocation ? formatTimestamp(task.nextInvocation) : 'none'}
2316
+ </span>
2317
+ <span className="proteum-profiler__tag">autoexec:{task.autoexec ? 'yes' : 'no'}</span>
2318
+ <span className="proteum-profiler__tag">
2319
+ automatic:{task.automaticExecution ? 'enabled' : 'disabled in dev'}
2320
+ </span>
2321
+ <span className="proteum-profiler__tag">runs:{task.runCount}</span>
2322
+ {task.lastTrigger ? <span className="proteum-profiler__tag">trigger:{task.lastTrigger}</span> : null}
2323
+ {task.lastRunStatus ? <span className="proteum-profiler__tag">{task.lastRunStatus}</span> : null}
2324
+ {task.lastRunDurationMs !== undefined ? (
2325
+ <span className="proteum-profiler__tag">{formatDuration(task.lastRunDurationMs)}</span>
2326
+ ) : null}
2327
+ {task.lastErrorMessage ? (
2328
+ <span className="proteum-profiler__tag">{truncate(task.lastErrorMessage, 72)}</span>
2329
+ ) : null}
2330
+ </div>
2331
+
2332
+ <div className="proteum-profiler__mono proteum-profiler__muted">
2333
+ registered {formatTimestamp(task.registeredAt)}
2334
+ {cron.lastLoadedAt ? ` | refreshed ${formatTimestamp(cron.lastLoadedAt)}` : ''}
2335
+ </div>
2336
+ </div>
2337
+ ))}
2338
+ </div>
2339
+ )}
2340
+ </div>
2341
+ );
2342
+ }
2343
+
2344
+ const errorRows = [
2345
+ ...session.steps
2346
+ .filter((step) => step.status === 'error')
2347
+ .map((step) => ({ key: step.id, title: step.label, value: step.errorMessage || 'Step failed' })),
2348
+ ...session.traces
2349
+ .filter((trace) => trace.status === 'error')
2350
+ .map((trace) => ({ key: trace.id, title: trace.label, value: trace.errorMessage || 'Request failed' })),
2351
+ ...findTraceEvents(primaryTrace, ['error']).map((event) => ({
2352
+ key: `${event.index}:error`,
2353
+ title: event.type,
2354
+ value: Object.entries(event.details)
2355
+ .map(([key, value]) => `${key}=${renderSummaryValue(value)}`)
2356
+ .join(' '),
2357
+ })),
2358
+ ];
2359
+
2360
+ return <SimpleSection empty="No errors captured." rows={errorRows} showTitle={false} title="Errors" />;
2361
+ };
2362
+
2363
+ export default function DevProfiler() {
2364
+ const [state, setState] = React.useState(() => profilerRuntime.getState());
2365
+
2366
+ React.useEffect(() => profilerRuntime.subscribe(() => setState(profilerRuntime.getState())), []);
2367
+ React.useEffect(() => {
2368
+ void profilerRuntime.refreshCommands();
2369
+ void profilerRuntime.refreshCronTasks();
2370
+ }, []);
2371
+
2372
+ React.useEffect(() => {
2373
+ const onKeyDown = (event: KeyboardEvent) => {
2374
+ if (event.key === 'Escape' && profilerRuntime.getState().uiState === 'expanded') {
2375
+ profilerRuntime.setUiState('minimized');
2376
+ }
2377
+ };
2378
+
2379
+ window.addEventListener('keydown', onKeyDown);
2380
+ return () => window.removeEventListener('keydown', onKeyDown);
2381
+ }, []);
2382
+
2383
+ if (!window.dev) return null;
2384
+
2385
+ const session = getSelectedSession(state.sessions, state.selectedSessionId, state.currentSessionId);
2386
+ if (!session) return null;
2387
+
2388
+ const summary = getSummary(session);
2389
+ const tone = summary.errorCount > 0 ? 'error' : (summary.totalMs || 0) > 500 ? 'warn' : 'ok';
2390
+ const primaryTrace = summary.primaryTrace?.trace;
2391
+ const minimizedLabel =
2392
+ session.kind === 'client-navigation'
2393
+ ? session.label
2394
+ : primaryTrace
2395
+ ? `${primaryTrace.statusCode || 'pending'} ${formatProfilerRequestReference({
2396
+ method: primaryTrace.method,
2397
+ path: primaryTrace.path,
2398
+ requestData: getTraceRequestData(primaryTrace),
2399
+ })}`
2400
+ : session.label;
2401
+ const recentSessions: TProfilerNavigationSession[] = state.sessions.slice(-6).reverse();
2402
+
2403
+ return (
2404
+ <div className="proteum-profiler">
2405
+ <style dangerouslySetInnerHTML={{ __html: profilerStyles }} />
2406
+
2407
+ {state.uiState === 'pinned-handle' ? (
2408
+ <button
2409
+ className="proteum-profiler__handle"
2410
+ onClick={() => profilerRuntime.setUiState('minimized')}
2411
+ type="button"
2412
+ >
2413
+ Proteum Profiler
2414
+ </button>
2415
+ ) : (
2416
+ <>
2417
+ {state.uiState === 'expanded' ? (
2418
+ <div className="proteum-profiler__panel">
2419
+ <div className="proteum-profiler__panelHeader">
2420
+ <select
2421
+ aria-label="Profiler path selector"
2422
+ className="proteum-profiler__select"
2423
+ onChange={(event) => profilerRuntime.selectSession(event.currentTarget.value)}
2424
+ value={session.id}
2425
+ >
2426
+ {recentSessions.map((recentSession) => (
2427
+ <option key={recentSession.id} value={recentSession.id}>
2428
+ {getSessionSelectorLabel(recentSession)}
2429
+ </option>
2430
+ ))}
2431
+ </select>
2432
+
2433
+ <div className="proteum-profiler__panelTabs">
2434
+ {(Object.keys(panelLabels) as TProfilerPanel[]).map((panel) => (
2435
+ <button
2436
+ className={`proteum-profiler__pill ${
2437
+ state.activePanel === panel ? 'proteum-profiler__pill--active' : ''
2438
+ }`}
2439
+ key={panel}
2440
+ onClick={() => profilerRuntime.openPanel(panel)}
2441
+ type="button"
2442
+ >
2443
+ {panelLabels[panel]}
2444
+ </button>
2445
+ ))}
2446
+ </div>
2447
+
2448
+ <div className="proteum-profiler__actions">
2449
+ <button className="proteum-profiler__pill" onClick={() => profilerRuntime.setUiState('minimized')} type="button">
2450
+ Collapse
2451
+ </button>
2452
+ <button className="proteum-profiler__pill" onClick={() => profilerRuntime.setUiState('pinned-handle')} type="button">
2453
+ Hide
2454
+ </button>
2455
+ </div>
2456
+ </div>
2457
+
2458
+ <div className="proteum-profiler__panelBody">{renderPanel(state.activePanel, session, summary, state)}</div>
2459
+ </div>
2460
+ ) : null}
2461
+
2462
+ <div className="proteum-profiler__bar">
2463
+ <button
2464
+ className="proteum-profiler__token proteum-profiler__token--brand"
2465
+ onClick={() => profilerRuntime.openPanel('summary')}
2466
+ type="button"
2467
+ >
2468
+ Proteum
2469
+ </button>
2470
+ <StatusToken label={truncate(minimizedLabel, 56)} onClick={() => profilerRuntime.openPanel('summary')} tone={tone} />
2471
+ <StatusToken
2472
+ label={formatDuration(summary.totalMs)}
2473
+ onClick={() => profilerRuntime.openPanel('summary')}
2474
+ tone={tone}
2475
+ />
2476
+ <StatusToken
2477
+ label={truncate(summary.routeLabel, 28)}
2478
+ onClick={() => profilerRuntime.openPanel('routing')}
2479
+ tone="ok"
2480
+ />
2481
+ <StatusToken
2482
+ label={
2483
+ summary.ssrPayloadBytes !== undefined
2484
+ ? `${formatDuration(summary.renderMs)} ${formatBytes(summary.ssrPayloadBytes)}`
2485
+ : formatDuration(summary.renderMs)
2486
+ }
2487
+ onClick={() => profilerRuntime.openPanel('ssr')}
2488
+ tone="ok"
2489
+ />
2490
+ <StatusToken
2491
+ label={`API ${summary.apiSyncCount} / ${summary.apiAsyncCount}`}
2492
+ onClick={() => profilerRuntime.openPanel('api')}
2493
+ tone={summary.apiAsyncCount > 0 || summary.apiSyncCount > 0 ? 'ok' : 'warn'}
2494
+ />
2495
+ {summary.errorCount > 0 ? (
2496
+ <StatusToken
2497
+ label={`${summary.errorCount} error${summary.errorCount === 1 ? '' : 's'}`}
2498
+ onClick={() => profilerRuntime.openPanel('errors')}
2499
+ tone="error"
2500
+ />
2501
+ ) : null}
2502
+ <div className="proteum-profiler__spacer" />
2503
+ <button className="proteum-profiler__token" onClick={() => profilerRuntime.setUiState('pinned-handle')} type="button">
2504
+ Hide
2505
+ </button>
2506
+ </div>
2507
+ </>
2508
+ )}
2509
+ </div>
2510
+ );
2511
+ }