brainzlab 0.1.1 → 0.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -0
  3. data/lib/brainzlab/beacon/client.rb +209 -0
  4. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  5. data/lib/brainzlab/beacon.rb +215 -0
  6. data/lib/brainzlab/configuration.rb +341 -3
  7. data/lib/brainzlab/cortex/cache.rb +59 -0
  8. data/lib/brainzlab/cortex/client.rb +141 -0
  9. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  10. data/lib/brainzlab/cortex.rb +227 -0
  11. data/lib/brainzlab/dendrite/client.rb +232 -0
  12. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  13. data/lib/brainzlab/dendrite.rb +195 -0
  14. data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
  15. data/lib/brainzlab/devtools/assets/devtools.js +322 -0
  16. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  17. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -0
  18. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  19. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  20. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  21. data/lib/brainzlab/devtools/middleware/database_handler.rb +180 -0
  22. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  23. data/lib/brainzlab/devtools/middleware/error_page.rb +376 -0
  24. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +155 -0
  25. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +94 -0
  26. data/lib/brainzlab/devtools.rb +75 -0
  27. data/lib/brainzlab/flux/buffer.rb +96 -0
  28. data/lib/brainzlab/flux/client.rb +70 -0
  29. data/lib/brainzlab/flux/provisioner.rb +57 -0
  30. data/lib/brainzlab/flux.rb +174 -0
  31. data/lib/brainzlab/instrumentation/active_record.rb +18 -1
  32. data/lib/brainzlab/instrumentation/aws.rb +179 -0
  33. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  34. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  35. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  36. data/lib/brainzlab/instrumentation/resque.rb +115 -0
  37. data/lib/brainzlab/instrumentation/solid_queue.rb +198 -0
  38. data/lib/brainzlab/instrumentation/stripe.rb +164 -0
  39. data/lib/brainzlab/instrumentation/typhoeus.rb +104 -0
  40. data/lib/brainzlab/instrumentation.rb +72 -0
  41. data/lib/brainzlab/nerve/client.rb +217 -0
  42. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  43. data/lib/brainzlab/nerve.rb +219 -0
  44. data/lib/brainzlab/pulse/instrumentation.rb +35 -2
  45. data/lib/brainzlab/pulse/propagation.rb +1 -1
  46. data/lib/brainzlab/pulse/tracer.rb +1 -1
  47. data/lib/brainzlab/pulse.rb +1 -1
  48. data/lib/brainzlab/rails/log_subscriber.rb +1 -2
  49. data/lib/brainzlab/rails/railtie.rb +36 -3
  50. data/lib/brainzlab/recall/provisioner.rb +17 -0
  51. data/lib/brainzlab/recall.rb +6 -1
  52. data/lib/brainzlab/reflex.rb +2 -2
  53. data/lib/brainzlab/sentinel/client.rb +218 -0
  54. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  55. data/lib/brainzlab/sentinel.rb +165 -0
  56. data/lib/brainzlab/signal/client.rb +62 -0
  57. data/lib/brainzlab/signal/provisioner.rb +55 -0
  58. data/lib/brainzlab/signal.rb +136 -0
  59. data/lib/brainzlab/synapse/client.rb +290 -0
  60. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  61. data/lib/brainzlab/synapse.rb +270 -0
  62. data/lib/brainzlab/utilities/circuit_breaker.rb +265 -0
  63. data/lib/brainzlab/utilities/health_check.rb +296 -0
  64. data/lib/brainzlab/utilities/log_formatter.rb +256 -0
  65. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  66. data/lib/brainzlab/utilities.rb +17 -0
  67. data/lib/brainzlab/vault/cache.rb +80 -0
  68. data/lib/brainzlab/vault/client.rb +198 -0
  69. data/lib/brainzlab/vault/provisioner.rb +49 -0
  70. data/lib/brainzlab/vault.rb +268 -0
  71. data/lib/brainzlab/version.rb +1 -1
  72. data/lib/brainzlab/vision/client.rb +128 -0
  73. data/lib/brainzlab/vision/provisioner.rb +136 -0
  74. data/lib/brainzlab/vision.rb +157 -0
  75. data/lib/brainzlab.rb +101 -0
  76. metadata +60 -1
@@ -0,0 +1,1086 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= h(@exception.class.name) %> - BrainzLab DevTools</title>
7
+
8
+ <!-- CDN Fonts -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
12
+
13
+ <!-- Highlight.js for syntax highlighting -->
14
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css">
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
16
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/ruby.min.js"></script>
17
+
18
+ <style>
19
+ :root {
20
+ --bg-primary: #0a0a0a;
21
+ --bg-secondary: #141414;
22
+ --bg-tertiary: #1a1a1a;
23
+ --bg-card: #1e1e1e;
24
+ --text-primary: #fafafa;
25
+ --text-secondary: #a1a1aa;
26
+ --text-muted: #71717a;
27
+ --border-color: #27272a;
28
+ --accent: #f97316;
29
+ --accent-hover: #fb923c;
30
+ --error: #ef4444;
31
+ --error-bg: rgba(239, 68, 68, 0.1);
32
+ --success: #22c55e;
33
+ --warning: #eab308;
34
+ --info: #3b82f6;
35
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
36
+ --font-mono: 'JetBrains Mono', 'SF Mono', 'Monaco', 'Consolas', monospace;
37
+ }
38
+
39
+ * { box-sizing: border-box; margin: 0; padding: 0; }
40
+
41
+ body {
42
+ font-family: var(--font-sans);
43
+ background: var(--bg-primary);
44
+ color: var(--text-primary);
45
+ line-height: 1.6;
46
+ min-height: 100vh;
47
+ }
48
+
49
+ .container {
50
+ max-width: 1400px;
51
+ margin: 0 auto;
52
+ padding: 2rem;
53
+ }
54
+
55
+ /* Header */
56
+ .error-header {
57
+ display: flex;
58
+ align-items: flex-start;
59
+ gap: 1.5rem;
60
+ padding: 2rem;
61
+ background: linear-gradient(135deg, var(--error) 0%, #dc2626 100%);
62
+ border-radius: 16px;
63
+ margin-bottom: 1.5rem;
64
+ box-shadow: 0 4px 24px rgba(239, 68, 68, 0.3);
65
+ }
66
+
67
+ .error-icon {
68
+ width: 48px;
69
+ height: 48px;
70
+ background: rgba(255,255,255,0.2);
71
+ border-radius: 12px;
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: center;
75
+ flex-shrink: 0;
76
+ }
77
+
78
+ .error-icon svg {
79
+ width: 28px;
80
+ height: 28px;
81
+ color: white;
82
+ }
83
+
84
+ .error-content {
85
+ flex: 1;
86
+ min-width: 0;
87
+ }
88
+
89
+ .error-type {
90
+ font-size: 0.875rem;
91
+ font-weight: 500;
92
+ opacity: 0.9;
93
+ margin-bottom: 0.25rem;
94
+ font-family: var(--font-mono);
95
+ }
96
+
97
+ .error-message {
98
+ font-size: 1.25rem;
99
+ font-weight: 600;
100
+ word-break: break-word;
101
+ line-height: 1.4;
102
+ }
103
+
104
+ .error-meta {
105
+ display: flex;
106
+ gap: 1rem;
107
+ margin-top: 1rem;
108
+ font-size: 0.8125rem;
109
+ opacity: 0.8;
110
+ }
111
+
112
+ .error-meta-item {
113
+ display: flex;
114
+ align-items: center;
115
+ gap: 0.375rem;
116
+ }
117
+
118
+ /* AI Copy Button */
119
+ .ai-actions {
120
+ display: flex;
121
+ gap: 0.75rem;
122
+ margin-top: 1rem;
123
+ }
124
+
125
+ .ai-btn {
126
+ display: inline-flex;
127
+ align-items: center;
128
+ gap: 0.5rem;
129
+ padding: 0.625rem 1rem;
130
+ background: rgba(255,255,255,0.15);
131
+ border: 1px solid rgba(255,255,255,0.2);
132
+ border-radius: 8px;
133
+ color: white;
134
+ font-size: 0.8125rem;
135
+ font-weight: 500;
136
+ cursor: pointer;
137
+ transition: all 0.15s ease;
138
+ font-family: var(--font-sans);
139
+ }
140
+
141
+ .ai-btn:hover {
142
+ background: rgba(255,255,255,0.25);
143
+ transform: translateY(-1px);
144
+ }
145
+
146
+ .ai-btn.copied {
147
+ background: var(--success);
148
+ border-color: var(--success);
149
+ }
150
+
151
+ .ai-btn.fix-btn {
152
+ background: var(--accent);
153
+ border-color: var(--accent);
154
+ }
155
+
156
+ .ai-btn.fix-btn:hover {
157
+ background: var(--accent-hover);
158
+ border-color: var(--accent-hover);
159
+ }
160
+
161
+ .ai-btn.running {
162
+ opacity: 0.7;
163
+ cursor: wait;
164
+ }
165
+
166
+ .ai-btn.error {
167
+ background: var(--error);
168
+ border-color: var(--error);
169
+ }
170
+
171
+ /* Result Modal */
172
+ .result-modal {
173
+ position: fixed;
174
+ top: 0;
175
+ left: 0;
176
+ right: 0;
177
+ bottom: 0;
178
+ background: rgba(0, 0, 0, 0.8);
179
+ display: none;
180
+ align-items: center;
181
+ justify-content: center;
182
+ z-index: 2000;
183
+ }
184
+
185
+ .result-modal.visible {
186
+ display: flex;
187
+ }
188
+
189
+ .result-content {
190
+ background: var(--bg-card);
191
+ border: 1px solid var(--border-color);
192
+ border-radius: 16px;
193
+ max-width: 700px;
194
+ width: 90%;
195
+ max-height: 80vh;
196
+ overflow: hidden;
197
+ display: flex;
198
+ flex-direction: column;
199
+ }
200
+
201
+ .result-header {
202
+ display: flex;
203
+ align-items: center;
204
+ justify-content: space-between;
205
+ padding: 1rem 1.5rem;
206
+ border-bottom: 1px solid var(--border-color);
207
+ background: var(--bg-tertiary);
208
+ }
209
+
210
+ .result-title {
211
+ font-weight: 600;
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 0.5rem;
215
+ }
216
+
217
+ .result-title.success { color: var(--success); }
218
+ .result-title.error { color: var(--error); }
219
+
220
+ .result-close {
221
+ background: none;
222
+ border: none;
223
+ color: var(--text-muted);
224
+ cursor: pointer;
225
+ padding: 0.25rem;
226
+ }
227
+
228
+ .result-close:hover {
229
+ color: var(--text-primary);
230
+ }
231
+
232
+ .result-body {
233
+ padding: 1.5rem;
234
+ overflow-y: auto;
235
+ flex: 1;
236
+ }
237
+
238
+ .result-output {
239
+ background: var(--bg-primary);
240
+ border-radius: 8px;
241
+ padding: 1rem;
242
+ font-family: var(--font-mono);
243
+ font-size: 0.8125rem;
244
+ white-space: pre-wrap;
245
+ color: var(--text-secondary);
246
+ max-height: 400px;
247
+ overflow-y: auto;
248
+ }
249
+
250
+ .result-actions {
251
+ padding: 1rem 1.5rem;
252
+ border-top: 1px solid var(--border-color);
253
+ display: flex;
254
+ gap: 0.75rem;
255
+ justify-content: flex-end;
256
+ }
257
+
258
+ .result-btn {
259
+ padding: 0.625rem 1.25rem;
260
+ border-radius: 8px;
261
+ font-size: 0.875rem;
262
+ font-weight: 500;
263
+ cursor: pointer;
264
+ border: 1px solid var(--border-color);
265
+ background: var(--bg-secondary);
266
+ color: var(--text-primary);
267
+ font-family: var(--font-sans);
268
+ }
269
+
270
+ .result-btn:hover {
271
+ background: var(--bg-tertiary);
272
+ }
273
+
274
+ .result-btn.primary {
275
+ background: var(--accent);
276
+ border-color: var(--accent);
277
+ color: white;
278
+ }
279
+
280
+ .result-btn.primary:hover {
281
+ background: var(--accent-hover);
282
+ }
283
+
284
+ .ai-btn svg {
285
+ width: 16px;
286
+ height: 16px;
287
+ }
288
+
289
+ /* Cards */
290
+ .card {
291
+ background: var(--bg-card);
292
+ border: 1px solid var(--border-color);
293
+ border-radius: 12px;
294
+ margin-bottom: 1rem;
295
+ overflow: hidden;
296
+ }
297
+
298
+ .card-header {
299
+ display: flex;
300
+ align-items: center;
301
+ justify-content: space-between;
302
+ padding: 1rem 1.25rem;
303
+ border-bottom: 1px solid var(--border-color);
304
+ background: var(--bg-tertiary);
305
+ }
306
+
307
+ .card-title {
308
+ font-size: 0.8125rem;
309
+ font-weight: 600;
310
+ text-transform: uppercase;
311
+ letter-spacing: 0.05em;
312
+ color: var(--text-secondary);
313
+ }
314
+
315
+ .card-badge {
316
+ font-size: 0.75rem;
317
+ padding: 0.25rem 0.625rem;
318
+ background: var(--bg-secondary);
319
+ border-radius: 9999px;
320
+ color: var(--text-muted);
321
+ font-family: var(--font-mono);
322
+ }
323
+
324
+ .card-body {
325
+ padding: 1.25rem;
326
+ }
327
+
328
+ /* Source Code */
329
+ .source-file {
330
+ display: flex;
331
+ align-items: center;
332
+ gap: 0.5rem;
333
+ padding: 0.75rem 1rem;
334
+ background: var(--bg-tertiary);
335
+ border-bottom: 1px solid var(--border-color);
336
+ font-family: var(--font-mono);
337
+ font-size: 0.8125rem;
338
+ color: var(--text-secondary);
339
+ }
340
+
341
+ .source-file svg {
342
+ width: 16px;
343
+ height: 16px;
344
+ color: var(--accent);
345
+ }
346
+
347
+ .source-code {
348
+ background: var(--bg-primary);
349
+ padding: 0;
350
+ margin: 0;
351
+ overflow-x: auto;
352
+ }
353
+
354
+ .source-line {
355
+ display: flex;
356
+ font-family: var(--font-mono);
357
+ font-size: 0.8125rem;
358
+ line-height: 1.7;
359
+ border-left: 3px solid transparent;
360
+ }
361
+
362
+ .source-line:hover {
363
+ background: var(--bg-tertiary);
364
+ }
365
+
366
+ .source-line.highlight {
367
+ background: var(--error-bg);
368
+ border-left-color: var(--error);
369
+ }
370
+
371
+ .line-number {
372
+ min-width: 4rem;
373
+ padding: 0 1rem;
374
+ text-align: right;
375
+ color: var(--text-muted);
376
+ user-select: none;
377
+ background: var(--bg-secondary);
378
+ border-right: 1px solid var(--border-color);
379
+ }
380
+
381
+ .line-content {
382
+ padding: 0 1rem;
383
+ white-space: pre;
384
+ color: var(--text-primary);
385
+ }
386
+
387
+ /* Backtrace */
388
+ .backtrace {
389
+ max-height: 400px;
390
+ overflow-y: auto;
391
+ }
392
+
393
+ .frame {
394
+ display: flex;
395
+ align-items: flex-start;
396
+ gap: 0.75rem;
397
+ padding: 0.75rem 1rem;
398
+ border-bottom: 1px solid var(--border-color);
399
+ font-family: var(--font-mono);
400
+ font-size: 0.8125rem;
401
+ transition: background 0.1s ease;
402
+ }
403
+
404
+ .frame:last-child {
405
+ border-bottom: none;
406
+ }
407
+
408
+ .frame:hover {
409
+ background: var(--bg-tertiary);
410
+ }
411
+
412
+ .frame.in-app {
413
+ background: rgba(249, 115, 22, 0.05);
414
+ }
415
+
416
+ .frame.in-app:hover {
417
+ background: rgba(249, 115, 22, 0.1);
418
+ }
419
+
420
+ .frame-number {
421
+ min-width: 2rem;
422
+ color: var(--text-muted);
423
+ text-align: right;
424
+ }
425
+
426
+ .frame-location {
427
+ flex: 1;
428
+ min-width: 0;
429
+ }
430
+
431
+ .frame-file {
432
+ color: var(--text-secondary);
433
+ word-break: break-all;
434
+ }
435
+
436
+ .frame.in-app .frame-file {
437
+ color: var(--accent);
438
+ }
439
+
440
+ .frame-line {
441
+ color: var(--success);
442
+ }
443
+
444
+ .frame-method {
445
+ color: var(--text-muted);
446
+ margin-left: 0.5rem;
447
+ }
448
+
449
+ /* Info Grid */
450
+ .info-grid {
451
+ display: grid;
452
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
453
+ gap: 1rem;
454
+ }
455
+
456
+ .info-table {
457
+ width: 100%;
458
+ font-size: 0.875rem;
459
+ }
460
+
461
+ .info-table tr {
462
+ border-bottom: 1px solid var(--border-color);
463
+ }
464
+
465
+ .info-table tr:last-child {
466
+ border-bottom: none;
467
+ }
468
+
469
+ .info-table th {
470
+ width: 120px;
471
+ padding: 0.625rem 0;
472
+ text-align: left;
473
+ font-weight: 500;
474
+ color: var(--text-muted);
475
+ vertical-align: top;
476
+ }
477
+
478
+ .info-table td {
479
+ padding: 0.625rem 0;
480
+ font-family: var(--font-mono);
481
+ font-size: 0.8125rem;
482
+ color: var(--text-primary);
483
+ word-break: break-all;
484
+ }
485
+
486
+ /* Params */
487
+ .params-block {
488
+ background: var(--bg-primary);
489
+ border-radius: 8px;
490
+ padding: 1rem;
491
+ font-family: var(--font-mono);
492
+ font-size: 0.8125rem;
493
+ overflow-x: auto;
494
+ white-space: pre-wrap;
495
+ color: var(--text-secondary);
496
+ }
497
+
498
+ /* Query Table */
499
+ .query-table {
500
+ width: 100%;
501
+ font-size: 0.8125rem;
502
+ }
503
+
504
+ .query-table th {
505
+ text-align: left;
506
+ padding: 0.75rem;
507
+ background: var(--bg-tertiary);
508
+ color: var(--text-muted);
509
+ font-weight: 500;
510
+ border-bottom: 1px solid var(--border-color);
511
+ }
512
+
513
+ .query-table td {
514
+ padding: 0.75rem;
515
+ border-bottom: 1px solid var(--border-color);
516
+ vertical-align: top;
517
+ }
518
+
519
+ .query-duration {
520
+ font-family: var(--font-mono);
521
+ white-space: nowrap;
522
+ }
523
+
524
+ .query-duration.slow {
525
+ color: var(--warning);
526
+ }
527
+
528
+ .query-sql {
529
+ font-family: var(--font-mono);
530
+ font-size: 0.75rem;
531
+ color: var(--text-secondary);
532
+ max-width: 500px;
533
+ overflow: hidden;
534
+ text-overflow: ellipsis;
535
+ white-space: nowrap;
536
+ }
537
+
538
+ /* Footer */
539
+ .footer {
540
+ margin-top: 2rem;
541
+ padding: 1.5rem;
542
+ text-align: center;
543
+ color: var(--text-muted);
544
+ font-size: 0.8125rem;
545
+ }
546
+
547
+ .footer a {
548
+ color: var(--accent);
549
+ text-decoration: none;
550
+ }
551
+
552
+ .footer a:hover {
553
+ text-decoration: underline;
554
+ }
555
+
556
+ /* Tooltip */
557
+ .tooltip {
558
+ position: fixed;
559
+ padding: 0.5rem 0.75rem;
560
+ background: var(--bg-card);
561
+ border: 1px solid var(--border-color);
562
+ border-radius: 6px;
563
+ font-size: 0.75rem;
564
+ color: var(--text-primary);
565
+ z-index: 1000;
566
+ pointer-events: none;
567
+ opacity: 0;
568
+ transition: opacity 0.15s ease;
569
+ }
570
+
571
+ .tooltip.visible {
572
+ opacity: 1;
573
+ }
574
+
575
+ /* Scrollbar */
576
+ ::-webkit-scrollbar {
577
+ width: 8px;
578
+ height: 8px;
579
+ }
580
+
581
+ ::-webkit-scrollbar-track {
582
+ background: var(--bg-secondary);
583
+ }
584
+
585
+ ::-webkit-scrollbar-thumb {
586
+ background: var(--border-color);
587
+ border-radius: 4px;
588
+ }
589
+
590
+ ::-webkit-scrollbar-thumb:hover {
591
+ background: var(--text-muted);
592
+ }
593
+ </style>
594
+ </head>
595
+ <body>
596
+ <div class="container">
597
+ <!-- Error Header -->
598
+ <div class="error-header">
599
+ <div class="error-icon">
600
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
601
+ <circle cx="12" cy="12" r="10"/>
602
+ <line x1="12" y1="8" x2="12" y2="12"/>
603
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
604
+ </svg>
605
+ </div>
606
+ <div class="error-content">
607
+ <div class="error-type"><%= h(@exception.class.name) %></div>
608
+ <div class="error-message"><%= h(@exception.message) %></div>
609
+ <div class="error-meta">
610
+ <span class="error-meta-item">
611
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
612
+ <path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
613
+ <circle cx="12" cy="10" r="3"/>
614
+ </svg>
615
+ <%= h(@request[:method]) %> <%= h(@request[:path]) %>
616
+ </span>
617
+ <%- if @context[:controller] -%>
618
+ <span class="error-meta-item">
619
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
620
+ <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
621
+ <line x1="3" y1="9" x2="21" y2="9"/>
622
+ </svg>
623
+ <%= h(@context[:controller]) %>#<%= h(@context[:action]) %>
624
+ </span>
625
+ <%- end -%>
626
+ </div>
627
+
628
+ <!-- Quick Fix Actions (context-aware) -->
629
+ <%- if @exception.class.name.include?('PendingMigrationError') || @exception.class.name.include?('NoDatabaseError') || @exception.class.name.include?('ActiveRecord::ConnectionNotEstablished') -%>
630
+ <div class="ai-actions" style="margin-bottom: 0.5rem;">
631
+ <%- if @exception.class.name.include?('PendingMigrationError') -%>
632
+ <button class="ai-btn fix-btn" onclick="runMigrations()" id="runMigrationsBtn">
633
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
634
+ <path d="M12 2L2 7l10 5 10-5-10-5z"/>
635
+ <path d="M2 17l10 5 10-5"/>
636
+ <path d="M2 12l10 5 10-5"/>
637
+ </svg>
638
+ <span>Run Pending Migrations</span>
639
+ </button>
640
+ <button class="ai-btn" onclick="runMigrations('status')" id="migrationStatusBtn">
641
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
642
+ <circle cx="12" cy="12" r="10"/>
643
+ <polyline points="12 6 12 12 16 14"/>
644
+ </svg>
645
+ <span>Check Status</span>
646
+ </button>
647
+ <%- elsif @exception.class.name.include?('NoDatabaseError') -%>
648
+ <button class="ai-btn fix-btn" onclick="setupDatabase()" id="setupDbBtn">
649
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
650
+ <ellipse cx="12" cy="5" rx="9" ry="3"/>
651
+ <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
652
+ <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
653
+ </svg>
654
+ <span>Create Database</span>
655
+ </button>
656
+ <%- end -%>
657
+ </div>
658
+ <%- end -%>
659
+
660
+ <!-- AI Copy Actions -->
661
+ <div class="ai-actions">
662
+ <button class="ai-btn" onclick="copyForAI()" id="copyAiBtn">
663
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
664
+ <path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
665
+ <rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
666
+ </svg>
667
+ <span>Copy for AI Assistant</span>
668
+ </button>
669
+ <button class="ai-btn" onclick="copyStackTrace()">
670
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
671
+ <polyline points="16 18 22 12 16 6"/>
672
+ <polyline points="8 6 2 12 8 18"/>
673
+ </svg>
674
+ <span>Copy Stack Trace</span>
675
+ </button>
676
+ </div>
677
+ </div>
678
+ </div>
679
+
680
+ <%- if @source_extract -%>
681
+ <!-- Source Code -->
682
+ <div class="card">
683
+ <div class="source-file">
684
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
685
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
686
+ <polyline points="14 2 14 8 20 8"/>
687
+ <line x1="16" y1="13" x2="8" y2="13"/>
688
+ <line x1="16" y1="17" x2="8" y2="17"/>
689
+ <polyline points="10 9 9 9 8 9"/>
690
+ </svg>
691
+ <%= h(@source_extract[:file]) %>:<%= @source_extract[:line_number] %>
692
+ </div>
693
+ <div class="source-code">
694
+ <%- @source_extract[:lines].each do |line| -%>
695
+ <div class="source-line<%= ' highlight' if line[:highlight] %>">
696
+ <span class="line-number"><%= line[:number] %></span>
697
+ <code class="line-content"><%= h(line[:content]) %></code>
698
+ </div>
699
+ <%- end -%>
700
+ </div>
701
+ </div>
702
+ <%- end -%>
703
+
704
+ <!-- Backtrace -->
705
+ <div class="card">
706
+ <div class="card-header">
707
+ <span class="card-title">Stack Trace</span>
708
+ <span class="card-badge"><%= @backtrace.length %> frames</span>
709
+ </div>
710
+ <div class="backtrace">
711
+ <%- @backtrace.each_with_index do |frame, idx| -%>
712
+ <div class="frame<%= ' in-app' if frame[:in_app] %>">
713
+ <span class="frame-number"><%= idx + 1 %></span>
714
+ <span class="frame-location">
715
+ <span class="frame-file"><%= h(frame[:file]) %></span><span class="frame-line">:<%= frame[:line] %></span>
716
+ <%- if frame[:function] -%>
717
+ <span class="frame-method">in `<%= h(frame[:function]) %>'</span>
718
+ <%- end -%>
719
+ </span>
720
+ </div>
721
+ <%- end -%>
722
+ </div>
723
+ </div>
724
+
725
+ <div class="info-grid">
726
+ <!-- Request Info -->
727
+ <div class="card">
728
+ <div class="card-header">
729
+ <span class="card-title">Request</span>
730
+ </div>
731
+ <div class="card-body">
732
+ <table class="info-table">
733
+ <tr>
734
+ <th>Method</th>
735
+ <td><%= h(@request[:method]) %></td>
736
+ </tr>
737
+ <tr>
738
+ <th>Path</th>
739
+ <td><%= h(@request[:path]) %></td>
740
+ </tr>
741
+ <tr>
742
+ <th>URL</th>
743
+ <td><%= h(@request[:url]) %></td>
744
+ </tr>
745
+ <%- if @context[:request_id] -%>
746
+ <tr>
747
+ <th>Request ID</th>
748
+ <td><%= h(@context[:request_id]) %></td>
749
+ </tr>
750
+ <%- end -%>
751
+ </table>
752
+
753
+ <%- if @request[:params] && !@request[:params].empty? -%>
754
+ <h4 style="margin: 1rem 0 0.5rem; font-size: 0.75rem; text-transform: uppercase; color: var(--text-muted); letter-spacing: 0.05em;">Parameters</h4>
755
+ <div class="params-block"><%= format_params(@request[:params]) %></div>
756
+ <%- end -%>
757
+ </div>
758
+ </div>
759
+
760
+ <!-- Environment -->
761
+ <div class="card">
762
+ <div class="card-header">
763
+ <span class="card-title">Environment</span>
764
+ </div>
765
+ <div class="card-body">
766
+ <table class="info-table">
767
+ <tr>
768
+ <th>Rails</th>
769
+ <td><%= h(@environment[:rails_version]) %></td>
770
+ </tr>
771
+ <tr>
772
+ <th>Ruby</th>
773
+ <td><%= h(@environment[:ruby_version]) %></td>
774
+ </tr>
775
+ <tr>
776
+ <th>Environment</th>
777
+ <td><%= h(@environment[:env]) %></td>
778
+ </tr>
779
+ <tr>
780
+ <th>Server</th>
781
+ <td><%= h(@environment[:server]) %></td>
782
+ </tr>
783
+ <tr>
784
+ <th>PID</th>
785
+ <td><%= @environment[:pid] %></td>
786
+ </tr>
787
+ </table>
788
+ </div>
789
+ </div>
790
+ </div>
791
+
792
+ <%- if @sql_queries && !@sql_queries.empty? -%>
793
+ <!-- SQL Queries -->
794
+ <div class="card">
795
+ <div class="card-header">
796
+ <span class="card-title">SQL Queries Before Error</span>
797
+ <span class="card-badge"><%= @sql_queries.length %> queries</span>
798
+ </div>
799
+ <div class="card-body" style="padding: 0;">
800
+ <table class="query-table">
801
+ <thead>
802
+ <tr>
803
+ <th width="80">Duration</th>
804
+ <th width="120">Name</th>
805
+ <th>Query</th>
806
+ </tr>
807
+ </thead>
808
+ <tbody>
809
+ <%- @sql_queries.each do |query| -%>
810
+ <tr>
811
+ <td class="query-duration<%= ' slow' if (query[:duration] || 0) > 50 %>">
812
+ <%= sprintf('%.2f', query[:duration] || 0) %>ms
813
+ </td>
814
+ <td><%= h(query[:name] || 'SQL') %></td>
815
+ <td class="query-sql" title="<%= h(query[:sql]) %>">
816
+ <%= h(truncate(query[:sql], 120)) %>
817
+ </td>
818
+ </tr>
819
+ <%- end -%>
820
+ </tbody>
821
+ </table>
822
+ </div>
823
+ </div>
824
+ <%- end -%>
825
+
826
+ <%- if @context[:user] && !@context[:user].empty? -%>
827
+ <!-- User Context -->
828
+ <div class="card">
829
+ <div class="card-header">
830
+ <span class="card-title">User Context</span>
831
+ </div>
832
+ <div class="card-body">
833
+ <table class="info-table">
834
+ <%- @context[:user].each do |key, value| -%>
835
+ <tr>
836
+ <th><%= h(key) %></th>
837
+ <td><%= h(value) %></td>
838
+ </tr>
839
+ <%- end -%>
840
+ </table>
841
+ </div>
842
+ </div>
843
+ <%- end -%>
844
+
845
+ <!-- Footer -->
846
+ <div class="footer">
847
+ <p>
848
+ <strong>BrainzLab DevTools</strong> &middot;
849
+ <a href="https://brainzlab.ai" target="_blank">brainzlab.ai</a>
850
+ </p>
851
+ </div>
852
+ </div>
853
+
854
+ <!-- Result Modal for Database Operations -->
855
+ <div class="result-modal" id="resultModal">
856
+ <div class="result-content">
857
+ <div class="result-header">
858
+ <div class="result-title" id="resultTitle">
859
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
860
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
861
+ <polyline points="22 4 12 14.01 9 11.01"/>
862
+ </svg>
863
+ <span>Operation Complete</span>
864
+ </div>
865
+ <button class="result-close" onclick="closeModal()">
866
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
867
+ <line x1="18" y1="6" x2="6" y2="18"/>
868
+ <line x1="6" y1="6" x2="18" y2="18"/>
869
+ </svg>
870
+ </button>
871
+ </div>
872
+ <div class="result-body">
873
+ <div class="result-output" id="resultOutput"></div>
874
+ </div>
875
+ <div class="result-actions">
876
+ <button class="result-btn" onclick="closeModal()">Close</button>
877
+ <button class="result-btn primary" onclick="reloadPage()">Reload Page</button>
878
+ </div>
879
+ </div>
880
+ </div>
881
+
882
+ <!-- Hidden data for AI copy -->
883
+ <script id="error-data" type="application/json">
884
+ <%= {
885
+ error_class: @exception.class.name,
886
+ message: @exception.message,
887
+ file: @source_extract&.dig(:file),
888
+ line: @source_extract&.dig(:line_number),
889
+ source_context: @source_extract&.dig(:lines)&.map { |l| "#{l[:number]}: #{l[:content]}" }&.join("\n"),
890
+ backtrace: @backtrace.first(15).map { |f| "#{f[:file]}:#{f[:line]}#{f[:function] ? " in `#{f[:function]}'" : ''}" },
891
+ request: {
892
+ method: @request[:method],
893
+ path: @request[:path],
894
+ controller: @context[:controller],
895
+ action: @context[:action]
896
+ },
897
+ rails_version: @environment[:rails_version],
898
+ ruby_version: @environment[:ruby_version],
899
+ environment: @environment[:env]
900
+ }.to_json %>
901
+ </script>
902
+
903
+ <script>
904
+ // Syntax highlighting
905
+ document.querySelectorAll('.line-content').forEach(el => {
906
+ hljs.highlightElement(el);
907
+ });
908
+
909
+ function copyForAI() {
910
+ const data = JSON.parse(document.getElementById('error-data').textContent);
911
+
912
+ const prompt = `I'm getting this error in my Rails application. Can you help me fix it?
913
+
914
+ ## Error
915
+ **${data.error_class}**: ${data.message}
916
+
917
+ ## Location
918
+ File: \`${data.file}:${data.line}\`
919
+
920
+ ## Source Code
921
+ \`\`\`ruby
922
+ ${data.source_context || 'N/A'}
923
+ \`\`\`
924
+
925
+ ## Stack Trace (top 15 frames)
926
+ \`\`\`
927
+ ${data.backtrace.join('\n')}
928
+ \`\`\`
929
+
930
+ ## Request Context
931
+ - **Method**: ${data.request.method}
932
+ - **Path**: ${data.request.path}
933
+ - **Controller**: ${data.request.controller}#${data.request.action}
934
+
935
+ ## Environment
936
+ - Rails ${data.rails_version}
937
+ - Ruby ${data.ruby_version}
938
+ - Environment: ${data.environment}
939
+
940
+ Please analyze this error and suggest:
941
+ 1. What's causing this error
942
+ 2. How to fix it
943
+ 3. Any best practices to prevent similar issues`;
944
+
945
+ navigator.clipboard.writeText(prompt).then(() => {
946
+ const btn = document.getElementById('copyAiBtn');
947
+ btn.classList.add('copied');
948
+ btn.querySelector('span').textContent = 'Copied!';
949
+ setTimeout(() => {
950
+ btn.classList.remove('copied');
951
+ btn.querySelector('span').textContent = 'Copy for AI Assistant';
952
+ }, 2000);
953
+ });
954
+ }
955
+
956
+ function copyStackTrace() {
957
+ const data = JSON.parse(document.getElementById('error-data').textContent);
958
+ const trace = `${data.error_class}: ${data.message}\n\n${data.backtrace.join('\n')}`;
959
+ navigator.clipboard.writeText(trace);
960
+ }
961
+
962
+ // Database operation functions
963
+ function runMigrations(action = 'migrate') {
964
+ const btn = document.getElementById(action === 'status' ? 'migrationStatusBtn' : 'runMigrationsBtn');
965
+ const originalText = btn.querySelector('span').textContent;
966
+
967
+ btn.classList.add('running');
968
+ btn.disabled = true;
969
+ btn.querySelector('span').textContent = action === 'status' ? 'Checking...' : 'Running...';
970
+
971
+ fetch('/_brainzlab/devtools/database', {
972
+ method: 'POST',
973
+ headers: {
974
+ 'Content-Type': 'application/json',
975
+ 'X-CSRF-Token': getCSRFToken()
976
+ },
977
+ body: JSON.stringify({ action: action })
978
+ })
979
+ .then(response => response.json())
980
+ .then(data => {
981
+ btn.classList.remove('running');
982
+ btn.disabled = false;
983
+ btn.querySelector('span').textContent = originalText;
984
+
985
+ showResult(data.success, data.output, action === 'status' ? 'Migration Status' : 'Migration Result');
986
+ })
987
+ .catch(error => {
988
+ btn.classList.remove('running');
989
+ btn.classList.add('error');
990
+ btn.disabled = false;
991
+ btn.querySelector('span').textContent = 'Failed';
992
+
993
+ showResult(false, error.message, 'Error');
994
+
995
+ setTimeout(() => {
996
+ btn.classList.remove('error');
997
+ btn.querySelector('span').textContent = originalText;
998
+ }, 3000);
999
+ });
1000
+ }
1001
+
1002
+ function setupDatabase() {
1003
+ const btn = document.getElementById('setupDbBtn');
1004
+ const originalText = btn.querySelector('span').textContent;
1005
+
1006
+ btn.classList.add('running');
1007
+ btn.disabled = true;
1008
+ btn.querySelector('span').textContent = 'Creating...';
1009
+
1010
+ fetch('/_brainzlab/devtools/database', {
1011
+ method: 'POST',
1012
+ headers: {
1013
+ 'Content-Type': 'application/json',
1014
+ 'X-CSRF-Token': getCSRFToken()
1015
+ },
1016
+ body: JSON.stringify({ action: 'create' })
1017
+ })
1018
+ .then(response => response.json())
1019
+ .then(data => {
1020
+ btn.classList.remove('running');
1021
+ btn.disabled = false;
1022
+ btn.querySelector('span').textContent = originalText;
1023
+
1024
+ showResult(data.success, data.output, 'Database Setup');
1025
+ })
1026
+ .catch(error => {
1027
+ btn.classList.remove('running');
1028
+ btn.classList.add('error');
1029
+ btn.disabled = false;
1030
+ btn.querySelector('span').textContent = 'Failed';
1031
+
1032
+ showResult(false, error.message, 'Error');
1033
+
1034
+ setTimeout(() => {
1035
+ btn.classList.remove('error');
1036
+ btn.querySelector('span').textContent = originalText;
1037
+ }, 3000);
1038
+ });
1039
+ }
1040
+
1041
+ function getCSRFToken() {
1042
+ const metaTag = document.querySelector('meta[name="csrf-token"]');
1043
+ return metaTag ? metaTag.getAttribute('content') : '';
1044
+ }
1045
+
1046
+ function showResult(success, output, title) {
1047
+ const modal = document.getElementById('resultModal');
1048
+ const titleEl = document.getElementById('resultTitle');
1049
+ const outputEl = document.getElementById('resultOutput');
1050
+
1051
+ titleEl.className = 'result-title ' + (success ? 'success' : 'error');
1052
+ titleEl.innerHTML = success
1053
+ ? `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1054
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
1055
+ <polyline points="22 4 12 14.01 9 11.01"/>
1056
+ </svg><span>${title}</span>`
1057
+ : `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1058
+ <circle cx="12" cy="12" r="10"/>
1059
+ <line x1="12" y1="8" x2="12" y2="12"/>
1060
+ <line x1="12" y1="16" x2="12.01" y2="16"/>
1061
+ </svg><span>${title}</span>`;
1062
+
1063
+ outputEl.textContent = output || 'No output';
1064
+ modal.classList.add('visible');
1065
+ }
1066
+
1067
+ function closeModal() {
1068
+ document.getElementById('resultModal').classList.remove('visible');
1069
+ }
1070
+
1071
+ function reloadPage() {
1072
+ window.location.reload();
1073
+ }
1074
+
1075
+ // Close modal on escape key
1076
+ document.addEventListener('keydown', function(e) {
1077
+ if (e.key === 'Escape') closeModal();
1078
+ });
1079
+
1080
+ // Close modal on backdrop click
1081
+ document.getElementById('resultModal').addEventListener('click', function(e) {
1082
+ if (e.target === this) closeModal();
1083
+ });
1084
+ </script>
1085
+ </body>
1086
+ </html>