rails_error_dashboard 0.1.28 → 0.1.30

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +50 -6
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +22 -0
  4. data/app/helpers/rails_error_dashboard/application_helper.rb +79 -7
  5. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +149 -0
  6. data/app/models/rails_error_dashboard/application.rb +1 -1
  7. data/app/models/rails_error_dashboard/error_log.rb +44 -16
  8. data/app/views/layouts/rails_error_dashboard.html.erb +71 -1237
  9. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -2
  10. data/app/views/rails_error_dashboard/errors/_source_code.html.erb +76 -0
  11. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +18 -82
  12. data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +70 -0
  13. data/app/views/rails_error_dashboard/errors/analytics.html.erb +9 -37
  14. data/app/views/rails_error_dashboard/errors/correlation.html.erb +11 -37
  15. data/app/views/rails_error_dashboard/errors/index.html.erb +64 -31
  16. data/app/views/rails_error_dashboard/errors/overview.html.erb +181 -3
  17. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +2 -1
  18. data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +286 -0
  19. data/app/views/rails_error_dashboard/errors/settings.html.erb +146 -480
  20. data/app/views/rails_error_dashboard/errors/show.html.erb +102 -76
  21. data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +188 -0
  22. data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +5 -0
  23. data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +3 -0
  24. data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +3 -0
  25. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +4 -0
  26. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +3 -0
  27. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +3 -0
  28. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +3 -0
  29. data/db/migrate/20251225100236_create_error_occurrences.rb +3 -0
  30. data/db/migrate/20251225101920_create_cascade_patterns.rb +3 -0
  31. data/db/migrate/20251225102500_create_error_baselines.rb +3 -0
  32. data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +3 -0
  33. data/db/migrate/20251226020100_create_error_comments.rb +3 -0
  34. data/db/migrate/20251229111223_add_additional_performance_indexes.rb +4 -0
  35. data/db/migrate/20260106094220_create_rails_error_dashboard_applications.rb +3 -0
  36. data/db/migrate/20260106094233_add_application_to_error_logs.rb +3 -0
  37. data/db/migrate/20260106094318_finalize_application_foreign_key.rb +5 -0
  38. data/lib/generators/rails_error_dashboard/install/install_generator.rb +32 -0
  39. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +37 -4
  40. data/lib/rails_error_dashboard/configuration.rb +160 -3
  41. data/lib/rails_error_dashboard/configuration_error.rb +24 -0
  42. data/lib/rails_error_dashboard/engine.rb +17 -0
  43. data/lib/rails_error_dashboard/helpers/user_model_detector.rb +138 -0
  44. data/lib/rails_error_dashboard/queries/analytics_stats.rb +1 -2
  45. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +19 -4
  46. data/lib/rails_error_dashboard/queries/errors_list.rb +27 -8
  47. data/lib/rails_error_dashboard/services/error_normalizer.rb +143 -0
  48. data/lib/rails_error_dashboard/services/git_blame_reader.rb +195 -0
  49. data/lib/rails_error_dashboard/services/github_link_generator.rb +159 -0
  50. data/lib/rails_error_dashboard/services/source_code_reader.rb +214 -0
  51. data/lib/rails_error_dashboard/version.rb +1 -1
  52. data/lib/rails_error_dashboard.rb +6 -0
  53. metadata +14 -10
  54. data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +0 -107
  55. data/app/assets/stylesheets/rails_error_dashboard/_components.scss +0 -625
  56. data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +0 -257
  57. data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +0 -203
  58. data/app/assets/stylesheets/rails_error_dashboard/application.css +0 -15
  59. data/app/assets/stylesheets/rails_error_dashboard/application.css.map +0 -7
  60. data/app/assets/stylesheets/rails_error_dashboard/application.scss +0 -61
  61. data/app/views/layouts/rails_error_dashboard/application.html.erb +0 -55
@@ -20,795 +20,14 @@
20
20
  <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
21
21
  <script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script>
22
22
 
23
- <!-- Catppuccin Mocha Theme - Pure CSS -->
24
- <style>
25
- /* Catppuccin Mocha Color Palette */
26
- :root {
27
- --ctp-rosewater: #f5e0dc;
28
- --ctp-flamingo: #f2cdcd;
29
- --ctp-pink: #f5c2e7;
30
- --ctp-mauve: #cba6f7;
31
- --ctp-red: #f38ba8;
32
- --ctp-maroon: #eba0ac;
33
- --ctp-peach: #fab387;
34
- --ctp-yellow: #f9e2af;
35
- --ctp-green: #a6e3a1;
36
- --ctp-teal: #94e2d5;
37
- --ctp-sky: #89dceb;
38
- --ctp-sapphire: #74c7ec;
39
- --ctp-blue: #89b4fa;
40
- --ctp-lavender: #b4befe;
41
- --ctp-text: #cdd6f4;
42
- --ctp-subtext1: #bac2de;
43
- --ctp-subtext0: #a6adc8;
44
- --ctp-overlay2: #9399b2;
45
- --ctp-overlay1: #7f849c;
46
- --ctp-overlay0: #6c7086;
47
- --ctp-surface2: #585b70;
48
- --ctp-surface1: #45475a;
49
- --ctp-surface0: #313244;
50
- --ctp-base: #1e1e2e;
51
- --ctp-mantle: #181825;
52
- --ctp-crust: #11111b;
53
- }
54
-
55
- /* Light Theme (Default) */
56
- body {
57
- background-color: #f3f4f6;
58
- color: #1f2937;
59
- transition: background-color 0.3s, color 0.3s;
60
- }
61
-
62
- /* Dark Theme */
63
- body.dark-mode {
64
- background-color: var(--ctp-base);
65
- color: var(--ctp-text);
66
- }
67
-
68
- /* Navbar */
69
- .navbar {
70
- background: linear-gradient(135deg, #8B5CF6, #6D28D9) !important;
71
- color: white !important;
72
- }
73
- .navbar * {
74
- color: white !important;
75
- }
76
- .navbar-brand {
77
- font-weight: bold;
78
- }
79
-
80
- /* Theme Toggle Button */
81
- .theme-toggle {
82
- cursor: pointer;
83
- padding: 0.5rem 1rem;
84
- border-radius: 0.5rem;
85
- background-color: rgba(255, 255, 255, 0.1);
86
- color: white;
87
- border: none;
88
- transition: background-color 0.2s;
89
- }
90
- .theme-toggle:hover {
91
- background-color: rgba(255, 255, 255, 0.2);
92
- }
93
-
94
- /* App Switcher Button - must override navbar * rule */
95
- .app-switcher-btn {
96
- background-color: rgba(255, 255, 255, 0.1) !important;
97
- color: white !important;
98
- border: 1px solid rgba(255, 255, 255, 0.3) !important;
99
- transition: background-color 0.2s;
100
- }
101
- .app-switcher-btn:hover {
102
- background-color: rgba(255, 255, 255, 0.2) !important;
103
- }
104
- .app-switcher-btn i,
105
- .app-switcher-btn * {
106
- color: white !important;
107
- }
108
-
109
- /* Sidebar */
110
- .sidebar {
111
- background: white;
112
- min-height: calc(100vh - 56px);
113
- box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
114
- }
115
- body.dark-mode .sidebar {
116
- background: var(--ctp-mantle);
117
- }
118
- .sidebar .nav-link {
119
- color: #1f2937;
120
- padding: 0.75rem 1.5rem;
121
- border-left: 3px solid transparent;
122
- transition: all 0.2s;
123
- }
124
- body.dark-mode .sidebar .nav-link {
125
- color: var(--ctp-text);
126
- }
127
- .sidebar .nav-link:hover {
128
- background-color: #f3f4f6;
129
- color: #8B5CF6;
130
- border-left-color: #8B5CF6;
131
- }
132
- body.dark-mode .sidebar .nav-link:hover {
133
- background-color: var(--ctp-surface0);
134
- color: var(--ctp-mauve);
135
- border-left-color: var(--ctp-mauve);
136
- }
137
- .sidebar .nav-link.active {
138
- background-color: #f3f4f6;
139
- color: #8B5CF6;
140
- border-left-color: #8B5CF6;
141
- font-weight: 600;
142
- }
143
- body.dark-mode .sidebar .nav-link.active {
144
- background-color: var(--ctp-surface0);
145
- color: var(--ctp-mauve);
146
- border-left-color: var(--ctp-mauve);
147
- }
148
-
149
- /* Cards */
150
- .card {
151
- background: white;
152
- border: 1px solid #e5e7eb;
153
- transition: background-color 0.3s, border-color 0.3s;
154
- }
155
- body.dark-mode .card {
156
- background: var(--ctp-surface0);
157
- border-color: var(--ctp-surface2);
158
- color: var(--ctp-text);
159
- }
160
- body.dark-mode .card-header {
161
- background-color: var(--ctp-surface1);
162
- border-color: var(--ctp-surface2);
163
- color: var(--ctp-text);
164
- }
165
-
166
- /* Tables */
167
- body.dark-mode .table {
168
- color: var(--ctp-text);
169
- }
170
- body.dark-mode .table-hover tbody tr:hover {
171
- background-color: var(--ctp-surface1);
172
- }
173
-
174
- /* Sticky table header for error list */
175
- .table-responsive thead th {
176
- position: sticky;
177
- top: 0;
178
- z-index: 10;
179
- background-color: #f8f9fa;
180
- }
181
- body.dark-mode .table-responsive thead th {
182
- background-color: var(--ctp-mantle);
183
- }
184
-
185
- /* Badges - Platform Colors */
186
- .badge-ios {
187
- background-color: #000;
188
- color: white;
189
- }
190
- body.dark-mode .badge-ios {
191
- background-color: var(--ctp-overlay0);
192
- color: var(--ctp-text);
193
- border: 1px solid var(--ctp-surface2);
194
- }
195
- .badge-android {
196
- background-color: #3DDC84;
197
- color: white;
198
- }
199
- body.dark-mode .badge-android {
200
- background-color: rgba(166, 227, 161, 0.2);
201
- color: var(--ctp-green);
202
- border: 1px solid var(--ctp-green);
203
- }
204
- .badge-web {
205
- background-color: #3B82F6;
206
- color: white;
207
- }
208
- body.dark-mode .badge-web {
209
- background-color: rgba(137, 180, 250, 0.2);
210
- color: var(--ctp-blue);
211
- border: 1px solid var(--ctp-blue);
212
- }
213
- .badge-api {
214
- background-color: #8B5CF6;
215
- color: white;
216
- }
217
- body.dark-mode .badge-api {
218
- background-color: rgba(116, 199, 236, 0.2);
219
- color: var(--ctp-sapphire);
220
- border: 1px solid var(--ctp-sapphire);
221
- }
222
-
223
- /* Forms */
224
- body.dark-mode .form-control,
225
- body.dark-mode .form-select {
226
- background-color: var(--ctp-surface0);
227
- border-color: var(--ctp-surface2);
228
- color: var(--ctp-text);
229
- }
230
- body.dark-mode .form-control:focus,
231
- body.dark-mode .form-select:focus {
232
- background-color: var(--ctp-surface1);
233
- border-color: var(--ctp-mauve);
234
- color: var(--ctp-text);
235
- }
236
-
237
- /* Code Blocks */
238
- .code-block,
239
- pre,
240
- code {
241
- background-color: #f9fafb;
242
- color: #1f2937;
243
- }
244
- body.dark-mode .code-block,
245
- body.dark-mode pre,
246
- body.dark-mode code {
247
- background-color: var(--ctp-mantle) !important;
248
- color: var(--ctp-text) !important;
249
- }
250
-
251
- /* Alerts */
252
- body.dark-mode .alert {
253
- background-color: var(--ctp-surface0);
254
- border-color: var(--ctp-surface2);
255
- color: var(--ctp-text);
256
- }
257
-
258
- /* Text colors */
259
- body.dark-mode .text-muted {
260
- color: var(--ctp-subtext0) !important;
261
- }
262
- body.dark-mode .text-primary {
263
- color: var(--ctp-mauve) !important;
264
- }
265
- body.dark-mode .text-danger {
266
- color: var(--ctp-red) !important;
267
- }
268
- body.dark-mode .text-success {
269
- color: var(--ctp-green) !important;
270
- }
271
- body.dark-mode .text-warning {
272
- color: var(--ctp-peach) !important;
273
- }
274
-
275
- /* Override Bootstrap bg-white and bg-light */
276
- body.dark-mode .bg-white {
277
- background-color: var(--ctp-surface0) !important;
278
- }
279
- body.dark-mode .bg-light {
280
- background-color: var(--ctp-surface1) !important;
281
- color: var(--ctp-text) !important;
282
- }
283
-
284
- /* Borders */
285
- body.dark-mode .border {
286
- border-color: var(--ctp-surface2) !important;
287
- }
288
- body.dark-mode .rounded {
289
- border-color: var(--ctp-surface2) !important;
290
- }
291
-
292
- /* Quick Filters Sidebar Section */
293
- .sidebar h6 {
294
- font-size: 0.75rem;
295
- text-transform: uppercase;
296
- letter-spacing: 0.05em;
297
- padding: 0.75rem 1.5rem;
298
- margin: 0;
299
- color: #6B7280;
300
- }
301
- body.dark-mode .sidebar h6 {
302
- color: var(--ctp-subtext0);
303
- }
304
-
305
- /* Stat Cards */
306
- .stat-card {
307
- border-radius: 0.75rem;
308
- transition: transform 0.2s, box-shadow 0.2s;
309
- }
310
- .stat-card:hover {
311
- transform: translateY(-2px);
312
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
313
- }
314
-
315
- /* Badges for severity */
316
- .badge.bg-danger {
317
- background-color: #EF4444 !important;
318
- }
319
- .badge.bg-warning {
320
- background-color: #F59E0B !important;
321
- }
322
- .badge.bg-info {
323
- background-color: #3B82F6 !important;
324
- }
325
- .badge.bg-secondary {
326
- background-color: #6B7280 !important;
327
- }
328
- .badge.bg-success {
329
- background-color: #10B981 !important;
330
- }
331
-
332
- body.dark-mode .badge.bg-danger {
333
- background-color: var(--ctp-red) !important;
334
- color: var(--ctp-base) !important;
335
- }
336
- body.dark-mode .badge.bg-warning {
337
- background-color: var(--ctp-peach) !important;
338
- color: var(--ctp-base) !important;
339
- }
340
- body.dark-mode .badge.bg-info {
341
- background-color: var(--ctp-blue) !important;
342
- color: var(--ctp-base) !important;
343
- }
344
- body.dark-mode .badge.bg-secondary {
345
- background-color: var(--ctp-overlay1) !important;
346
- }
347
- body.dark-mode .badge.bg-success {
348
- background-color: var(--ctp-green) !important;
349
- color: var(--ctp-base) !important;
350
- }
351
-
352
- /* Links */
353
- a {
354
- color: #8B5CF6;
355
- text-decoration: none;
356
- }
357
- a:hover {
358
- color: #6D28D9;
359
- text-decoration: underline;
360
- }
361
- body.dark-mode a {
362
- color: var(--ctp-mauve);
363
- }
364
- body.dark-mode a:hover {
365
- color: var(--ctp-pink);
366
- }
367
-
368
- /* Buttons */
369
- body.dark-mode .btn-primary {
370
- background-color: var(--ctp-mauve);
371
- border-color: var(--ctp-mauve);
372
- color: var(--ctp-base);
373
- }
374
- body.dark-mode .btn-primary:hover {
375
- background-color: var(--ctp-pink);
376
- border-color: var(--ctp-pink);
377
- }
378
- body.dark-mode .btn-outline-primary {
379
- color: var(--ctp-mauve);
380
- border-color: var(--ctp-mauve);
381
- }
382
- body.dark-mode .btn-outline-primary:hover {
383
- background-color: var(--ctp-mauve);
384
- color: var(--ctp-base);
385
- }
386
- body.dark-mode .btn-secondary,
387
- body.dark-mode .btn-outline-secondary {
388
- background-color: var(--ctp-surface1);
389
- border-color: var(--ctp-surface2);
390
- color: var(--ctp-text);
391
- }
392
-
393
- /* Modal dialogs */
394
- body.dark-mode .modal-content {
395
- background-color: var(--ctp-surface0);
396
- color: var(--ctp-text);
397
- border-color: var(--ctp-surface2);
398
- }
399
- body.dark-mode .modal-header {
400
- background-color: var(--ctp-surface1);
401
- border-bottom-color: var(--ctp-surface2);
402
- color: var(--ctp-text);
403
- }
404
- body.dark-mode .modal-footer {
405
- background-color: var(--ctp-surface1);
406
- border-top-color: var(--ctp-surface2);
407
- }
408
- body.dark-mode .modal-title {
409
- color: var(--ctp-text);
410
- }
411
- body.dark-mode .btn-close {
412
- filter: invert(1);
413
- }
414
-
415
- /* Table headers - CRITICAL FIX */
416
- body.dark-mode thead,
417
- body.dark-mode thead th {
418
- background-color: var(--ctp-surface1) !important;
419
- color: var(--ctp-text) !important;
420
- border-color: var(--ctp-surface2) !important;
421
- }
422
- body.dark-mode tbody tr {
423
- border-color: var(--ctp-surface2) !important;
424
- background-color: transparent !important;
425
- }
426
- body.dark-mode tbody td {
427
- border-color: var(--ctp-surface2) !important;
428
- background-color: transparent !important;
429
- color: var(--ctp-text) !important;
430
- }
431
- body.dark-mode tbody th {
432
- background-color: var(--ctp-surface0) !important;
433
- color: var(--ctp-text) !important;
434
- border-color: var(--ctp-surface2) !important;
435
- }
436
- body.dark-mode table {
437
- color: var(--ctp-text) !important;
438
- }
439
-
440
- /* Chart canvas backgrounds */
441
- body.dark-mode canvas {
442
- background-color: transparent !important;
443
- }
444
-
445
- /* Chart labels and text */
446
- body.dark-mode .chart-container text {
447
- fill: var(--ctp-text) !important;
448
- }
449
-
450
- /* Form placeholders */
451
- body.dark-mode .form-control::placeholder,
452
- body.dark-mode .form-select::placeholder {
453
- color: var(--ctp-overlay0);
454
- opacity: 1;
455
- }
456
-
457
- /* Dropdown menus */
458
- .dropdown-menu {
459
- background-color: white;
460
- border: 1px solid rgba(0, 0, 0, 0.15);
461
- }
462
- .dropdown-item {
463
- color: #1f2937 !important;
464
- }
465
- .dropdown-item:hover,
466
- .dropdown-item:focus {
467
- background-color: #f3f4f6;
468
- color: #8B5CF6 !important;
469
- }
470
- .dropdown-item.active {
471
- background-color: #8B5CF6;
472
- color: white !important;
473
- }
474
-
475
- body.dark-mode .dropdown-menu {
476
- background-color: var(--ctp-surface0);
477
- border-color: var(--ctp-surface2);
478
- }
479
- body.dark-mode .dropdown-item {
480
- color: var(--ctp-text) !important;
481
- }
482
- body.dark-mode .dropdown-item:hover,
483
- body.dark-mode .dropdown-item:focus {
484
- background-color: var(--ctp-surface1);
485
- color: var(--ctp-mauve) !important;
486
- }
487
- body.dark-mode .dropdown-item.active {
488
- background-color: var(--ctp-mauve);
489
- color: var(--ctp-base) !important;
490
- }
491
-
492
- /* Pagination */
493
- body.dark-mode .pagination .page-link {
494
- background-color: var(--ctp-surface0);
495
- border-color: var(--ctp-surface2);
496
- color: var(--ctp-text);
497
- }
498
- body.dark-mode .pagination .page-link:hover {
499
- background-color: var(--ctp-surface1);
500
- color: var(--ctp-mauve);
501
- }
502
- body.dark-mode .pagination .page-item.active .page-link {
503
- background-color: var(--ctp-mauve);
504
- border-color: var(--ctp-mauve);
505
- color: var(--ctp-base);
506
- }
507
-
508
- /* Progress bars */
509
- body.dark-mode .progress {
510
- background-color: var(--ctp-surface1);
511
- }
512
- body.dark-mode .progress-bar {
513
- background-color: var(--ctp-mauve);
514
- }
515
-
516
- /* Horizontal rules */
517
- body.dark-mode hr {
518
- border-color: var(--ctp-surface2);
519
- opacity: 1;
520
- }
521
-
522
- /* List groups */
523
- body.dark-mode .list-group-item {
524
- background-color: var(--ctp-surface0);
525
- border-color: var(--ctp-surface2);
526
- color: var(--ctp-text);
527
- }
528
- body.dark-mode .list-group-item:hover {
529
- background-color: var(--ctp-surface1);
530
- }
531
-
532
- /* Offcanvas (mobile menu) */
533
- body.dark-mode .offcanvas {
534
- background-color: var(--ctp-mantle);
535
- color: var(--ctp-text);
536
- }
537
- body.dark-mode .offcanvas-header {
538
- border-bottom-color: var(--ctp-surface2);
539
- }
540
-
541
- /* Small text and labels */
542
- body.dark-mode small {
543
- color: var(--ctp-subtext1) !important;
544
- }
545
- body.dark-mode label {
546
- color: var(--ctp-text);
547
- }
548
-
549
- /* Breadcrumbs */
550
- body.dark-mode .breadcrumb {
551
- background-color: var(--ctp-surface0);
552
- }
553
- body.dark-mode .breadcrumb-item {
554
- color: var(--ctp-text);
555
- }
556
- body.dark-mode .breadcrumb-item.active {
557
- color: var(--ctp-subtext0);
558
- }
559
-
560
- /* Tooltips */
561
- body.dark-mode .tooltip-inner {
562
- background-color: var(--ctp-surface0);
563
- color: var(--ctp-text);
564
- }
565
-
566
- /* Checkboxes and radios */
567
- body.dark-mode .form-check-input {
568
- background-color: var(--ctp-surface1);
569
- border-color: var(--ctp-surface2);
570
- }
571
- body.dark-mode .form-check-input:checked {
572
- background-color: var(--ctp-mauve);
573
- border-color: var(--ctp-mauve);
574
- }
575
- body.dark-mode .form-check-label {
576
- color: var(--ctp-text);
577
- }
578
-
579
- /* Nav tabs */
580
- body.dark-mode .nav-tabs {
581
- border-bottom-color: var(--ctp-surface2);
582
- }
583
- body.dark-mode .nav-tabs .nav-link {
584
- color: var(--ctp-text);
585
- background-color: transparent;
586
- border-color: transparent;
587
- }
588
- body.dark-mode .nav-tabs .nav-link:hover {
589
- border-color: var(--ctp-surface2);
590
- background-color: var(--ctp-surface1);
591
- }
592
- body.dark-mode .nav-tabs .nav-link.active {
593
- background-color: var(--ctp-surface0);
594
- border-color: var(--ctp-surface2) var(--ctp-surface2) var(--ctp-surface0);
595
- color: var(--ctp-mauve);
596
- }
597
-
598
- /* Collapsible sections - AGGRESSIVE */
599
- body.dark-mode .accordion-item {
600
- background-color: var(--ctp-surface0) !important;
601
- border-color: var(--ctp-surface2) !important;
602
- }
603
- body.dark-mode .accordion-button {
604
- background-color: var(--ctp-surface1) !important;
605
- color: var(--ctp-text) !important;
606
- }
607
- body.dark-mode .accordion-button.bg-light {
608
- background-color: var(--ctp-surface1) !important;
609
- }
610
- body.dark-mode .accordion-button:not(.collapsed) {
611
- background-color: var(--ctp-surface0) !important;
612
- color: var(--ctp-mauve) !important;
613
- }
614
- body.dark-mode .accordion-button::after {
615
- filter: invert(1);
616
- }
617
- body.dark-mode .accordion-body {
618
- background-color: var(--ctp-surface0) !important;
619
- color: var(--ctp-text) !important;
620
- }
621
-
622
- /* Heatmap specific styling - AGGRESSIVE */
623
- body.dark-mode .heatmap-cell {
624
- border-color: var(--ctp-surface2) !important;
625
- color: var(--ctp-text) !important;
626
- }
627
- body.dark-mode .heatmap-hour {
628
- color: var(--ctp-sky) !important;
629
- font-weight: 600 !important;
630
- font-size: 0.75rem !important;
631
- }
632
- body.dark-mode .heatmap-count {
633
- color: var(--ctp-peach) !important;
634
- font-weight: 600 !important;
635
- }
636
- body.dark-mode .heatmap-count.text-white {
637
- color: var(--ctp-text) !important;
638
- }
639
- body.dark-mode .heatmap-count.text-dark {
640
- color: var(--ctp-peach) !important;
641
- }
642
- body.dark-mode .heatmap-grid {
643
- background-color: var(--ctp-surface0);
644
- }
645
-
646
- /* Definition lists (dl, dt, dd) - used in Request Context */
647
- body.dark-mode dl {
648
- color: var(--ctp-text);
649
- }
650
- body.dark-mode dt {
651
- color: var(--ctp-subtext1);
652
- font-weight: 600;
653
- }
654
- body.dark-mode dd {
655
- color: var(--ctp-text);
656
- background-color: var(--ctp-surface0);
657
- }
658
-
659
- /* Override any remaining white backgrounds */
660
- body.dark-mode div[style*="background-color: white"],
661
- body.dark-mode div[style*="background-color: #fff"],
662
- body.dark-mode div[style*="background-color:#fff"],
663
- body.dark-mode div[style*="background: white"],
664
- body.dark-mode div[style*="background: #fff"] {
665
- background-color: var(--ctp-surface0) !important;
666
- }
667
-
668
- /* Chart.js specific fixes for axis labels */
669
- body.dark-mode .chartjs-render-monitor {
670
- background-color: transparent !important;
671
- }
672
-
673
- /* Make sure all headings are visible */
674
- body.dark-mode h1, body.dark-mode h2, body.dark-mode h3,
675
- body.dark-mode h4, body.dark-mode h5, body.dark-mode h6 {
676
- color: var(--ctp-text);
677
- }
678
-
679
- /* Stronger text colors for better visibility */
680
- body.dark-mode .text-secondary {
681
- color: var(--ctp-subtext1) !important;
682
- }
683
-
684
- /* SVG text elements (for charts) - MORE AGGRESSIVE */
685
- body.dark-mode svg text {
686
- fill: var(--ctp-text) !important;
687
- }
688
- body.dark-mode svg .domain,
689
- body.dark-mode svg .tick line {
690
- stroke: var(--ctp-surface2) !important;
691
- }
692
- body.dark-mode svg tspan {
693
- fill: var(--ctp-text) !important;
694
- }
695
-
696
- /* Details/Summary (collapsible sections) */
697
- body.dark-mode details {
698
- background-color: var(--ctp-surface0) !important;
699
- border-color: var(--ctp-surface2) !important;
700
- }
701
- body.dark-mode summary {
702
- background-color: var(--ctp-surface0) !important;
703
- color: var(--ctp-text) !important;
704
- border-color: var(--ctp-surface2) !important;
705
- }
706
- body.dark-mode details[open] summary {
707
- background-color: var(--ctp-surface1) !important;
708
- border-bottom-color: var(--ctp-surface2) !important;
709
- }
710
-
711
- /* Button-like summary elements */
712
- body.dark-mode .btn.collapsed,
713
- body.dark-mode [data-bs-toggle="collapse"] {
714
- background-color: var(--ctp-surface0) !important;
715
- color: var(--ctp-text) !important;
716
- border-color: var(--ctp-surface2) !important;
717
- }
718
-
719
- /* Specific override for white backgrounds in backtrace sections */
720
- body.dark-mode .card .card-body > div[style*="background"],
721
- body.dark-mode .card-body > button[style*="background"] {
722
- background-color: var(--ctp-surface0) !important;
723
- }
724
-
725
- /* Error header/banner */
726
- body.dark-mode .alert-danger {
727
- background-color: rgba(243, 139, 168, 0.2) !important;
728
- border-color: var(--ctp-red) !important;
729
- color: var(--ctp-text) !important;
730
- }
731
-
732
- /* Chartkick specific - force text visibility */
733
- body.dark-mode #chart-1 text,
734
- body.dark-mode [id^="chart-"] text {
735
- fill: var(--ctp-text) !important;
736
- color: var(--ctp-text) !important;
737
- }
738
-
739
- /* Force all text in charts to be visible */
740
- body.dark-mode canvas + div text,
741
- body.dark-mode .chartjs-size-monitor text {
742
- color: var(--ctp-text) !important;
743
- }
744
-
745
- /* ULTRA AGGRESSIVE - Chart.js axis labels and titles */
746
- body.dark-mode canvas {
747
- color: var(--ctp-text) !important;
748
- }
749
-
750
- /* Target Google Charts (alternative library) */
751
- body.dark-mode svg > g > g > text,
752
- body.dark-mode svg g text {
753
- fill: var(--ctp-text) !important;
754
- font-size: 12px !important;
755
- }
756
-
757
- /* Chart.js specific selectors - NUCLEAR OPTION */
758
- body.dark-mode .chartjs-render-monitor + div text,
759
- body.dark-mode [class*="chart"] text,
760
- body.dark-mode div[id*="chart"] text {
761
- fill: var(--ctp-text) !important;
762
- color: var(--ctp-text) !important;
763
- }
764
-
765
- /* Ensure axis tick labels are visible */
766
- body.dark-mode g.tick text,
767
- body.dark-mode .tick text,
768
- body.dark-mode text.highcharts-axis-title,
769
- body.dark-mode .highcharts-axis-labels text {
770
- fill: var(--ctp-text) !important;
771
- color: var(--ctp-text) !important;
772
- }
773
-
774
- /* Force visibility on ALL text elements inside chart containers */
775
- body.dark-mode .card-body text,
776
- body.dark-mode .card text {
777
- fill: var(--ctp-text) !important;
778
- }
779
-
780
- /* Toast notifications */
781
- .toast {
782
- min-width: 250px;
783
- }
784
- .toast-container {
785
- max-width: 350px;
786
- }
787
-
788
- /* Filter pills */
789
- .filter-pill {
790
- display: inline-flex;
791
- align-items: center;
792
- padding: 0.5rem 0.75rem;
793
- font-size: 0.875rem;
794
- border-radius: 0.375rem;
795
- transition: all 0.2s ease;
796
- }
797
- .filter-pill:hover {
798
- opacity: 0.85;
799
- transform: translateY(-1px);
800
- }
801
- .filter-pill .bi-x {
802
- font-size: 1.1rem;
803
- font-weight: bold;
804
- }
805
- body.dark-mode .filter-pill.bg-primary {
806
- background-color: var(--ctp-mauve) !important;
807
- }
808
- body.dark-mode .filter-pill.bg-secondary {
809
- background-color: var(--ctp-surface2) !important;
810
- }
811
- </style>
23
+ <!-- Syntax Highlighting for Source Code Viewer -->
24
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@catppuccin/highlightjs@1.0.0/css/catppuccin-mocha.min.css">
25
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlightjs-line-numbers.js@2.8.0/dist/styles.min.css">
26
+ <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
27
+ <script src="https://cdn.jsdelivr.net/npm/highlightjs-line-numbers.js@2.8.0/dist/highlightjs-line-numbers.min.js"></script>
28
+
29
+ <!-- Dashboard CSS -->
30
+ <link rel="stylesheet" href="/rails_error_dashboard/css/dashboard.css">
812
31
  </head>
813
32
 
814
33
  <body>
@@ -821,9 +40,14 @@
821
40
  <nav class="navbar navbar-dark">
822
41
  <div class="container-fluid">
823
42
  <div class="d-flex align-items-center">
43
+ <!-- Mobile menu toggle -->
824
44
  <button class="btn btn-link text-white d-md-none me-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu">
825
45
  <i class="bi bi-list fs-4"></i>
826
46
  </button>
47
+ <!-- Desktop sidebar toggle -->
48
+ <button class="btn btn-link text-white d-none d-md-block me-2" type="button" id="sidebarToggle" title="Toggle sidebar">
49
+ <i class="bi bi-layout-sidebar-inset fs-5"></i>
50
+ </button>
827
51
  <%
828
52
  # Check if main app has a root route defined
829
53
  begin
@@ -900,7 +124,7 @@
900
124
  <div class="container-fluid">
901
125
  <div class="row">
902
126
  <!-- Sidebar -->
903
- <nav class="col-md-2 d-none d-md-block sidebar">
127
+ <nav class="col-md-2 d-none d-md-block sidebar" id="sidebar">
904
128
  <div class="position-sticky pt-3">
905
129
  <%
906
130
  # Preserve application_id across navigation
@@ -913,7 +137,7 @@
913
137
  <% end %>
914
138
  </li>
915
139
  <li class="nav-item">
916
- <%= link_to errors_path(nav_params), class: "nav-link #{request.path == errors_path ? 'active' : ''}" do %>
140
+ <%= link_to errors_path(nav_params), class: "nav-link #{(controller_name == 'errors' && action_name == 'index') ? 'active' : ''}" do %>
917
141
  <i class="bi bi-list-ul"></i> All Errors
918
142
  <% end %>
919
143
  </li>
@@ -922,6 +146,16 @@
922
146
  <i class="bi bi-graph-up"></i> Analytics
923
147
  <% end %>
924
148
  </li>
149
+ <li class="nav-item">
150
+ <%= link_to platform_comparison_errors_path(nav_params), class: "nav-link #{request.path == platform_comparison_errors_path ? 'active' : ''}" do %>
151
+ <i class="bi bi-heart-pulse"></i> Platform Health
152
+ <% end %>
153
+ </li>
154
+ <li class="nav-item">
155
+ <%= link_to correlation_errors_path(nav_params), class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}" do %>
156
+ <i class="bi bi-diagram-3"></i> Correlation
157
+ <% end %>
158
+ </li>
925
159
  <li class="nav-item">
926
160
  <%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
927
161
  <i class="bi bi-gear"></i> Settings
@@ -947,13 +181,28 @@
947
181
  <% end %>
948
182
  </li>
949
183
  <li class="nav-item">
950
- <%= link_to errors_path(nav_params.merge(platform: 'iOS')), class: "nav-link" do %>
951
- <i class="bi bi-phone"></i> iOS Errors
184
+ <%= link_to errors_path(nav_params.merge(platform: 'iOS')), class: "nav-link #{params[:platform] == 'iOS' ? 'active' : ''}" do %>
185
+ <i class="bi bi-apple"></i> iOS Errors
186
+ <% end %>
187
+ </li>
188
+ <li class="nav-item">
189
+ <%= link_to errors_path(nav_params.merge(platform: 'Android')), class: "nav-link #{params[:platform] == 'Android' ? 'active' : ''}" do %>
190
+ <i class="bi bi-android2"></i> Android Errors
191
+ <% end %>
192
+ </li>
193
+ <li class="nav-item">
194
+ <%= link_to errors_path(nav_params.merge(platform: 'Web')), class: "nav-link #{params[:platform] == 'Web' ? 'active' : ''}" do %>
195
+ <i class="bi bi-globe"></i> Web Errors
196
+ <% end %>
197
+ </li>
198
+ <li class="nav-item">
199
+ <%= link_to errors_path(nav_params.merge(platform: 'API')), class: "nav-link #{params[:platform] == 'API' ? 'active' : ''}" do %>
200
+ <i class="bi bi-server"></i> API Errors
952
201
  <% end %>
953
202
  </li>
954
203
  <li class="nav-item">
955
- <%= link_to errors_path(nav_params.merge(platform: 'Android')), class: "nav-link" do %>
956
- <i class="bi bi-phone"></i> Android Errors
204
+ <%= link_to errors_path(nav_params.merge(platform: 'Background Jobs')), class: "nav-link #{params[:platform] == 'Background Jobs' ? 'active' : ''}" do %>
205
+ <i class="bi bi-gear-fill"></i> Background Jobs
957
206
  <% end %>
958
207
  </li>
959
208
  </ul>
@@ -961,7 +210,7 @@
961
210
  </nav>
962
211
 
963
212
  <!-- Main content -->
964
- <main class="col-md-10 ms-sm-auto px-md-4">
213
+ <main class="col-md-10 ms-sm-auto px-md-4" id="mainContent">
965
214
  <%= yield %>
966
215
  </main>
967
216
  </div>
@@ -991,6 +240,10 @@
991
240
  <span><i class="bi bi-graph-up text-primary"></i> Go to analytics</span>
992
241
  <kbd class="bg-secondary text-white px-2 py-1 rounded">A</kbd>
993
242
  </div>
243
+ <div class="list-group-item d-flex justify-content-between align-items-center">
244
+ <span><i class="bi bi-layout-sidebar text-primary"></i> Toggle sidebar</span>
245
+ <kbd class="bg-secondary text-white px-2 py-1 rounded">S</kbd>
246
+ </div>
994
247
  <div class="list-group-item d-flex justify-content-between align-items-center">
995
248
  <span><i class="bi bi-question-circle text-primary"></i> Show this help</span>
996
249
  <kbd class="bg-secondary text-white px-2 py-1 rounded">?</kbd>
@@ -1020,449 +273,30 @@
1020
273
  <!-- Bootstrap JS -->
1021
274
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
1022
275
 
1023
- <!-- Pure JavaScript Theme Toggle -->
1024
- <script>
1025
- // Load saved theme on page load (before DOMContentLoaded to prevent flash)
1026
- (function() {
1027
- const savedTheme = localStorage.getItem('theme');
1028
- if (savedTheme === 'dark') {
1029
- document.body.classList.add('dark-mode');
1030
- }
1031
- })();
1032
-
1033
- // Theme toggle after DOM loads
1034
- document.addEventListener('DOMContentLoaded', function() {
1035
- const themeToggle = document.getElementById('themeToggle');
1036
- const themeIcon = document.getElementById('themeIcon');
1037
-
1038
- // Update icon based on current theme
1039
- function updateIcon() {
1040
- if (document.body.classList.contains('dark-mode')) {
1041
- themeIcon.className = 'bi bi-sun-fill';
1042
- } else {
1043
- themeIcon.className = 'bi bi-moon-fill';
1044
- }
1045
- }
1046
-
1047
- // Set initial icon
1048
- updateIcon();
1049
-
1050
- // Toggle theme on button click
1051
- themeToggle.addEventListener('click', function() {
1052
- console.log('🎨 Theme toggle clicked');
1053
-
1054
- document.body.classList.toggle('dark-mode');
1055
- const isDark = document.body.classList.contains('dark-mode');
1056
-
1057
- console.log('Dark mode:', isDark);
1058
-
1059
- // Save preference
1060
- localStorage.setItem('theme', isDark ? 'dark' : 'light');
1061
- console.log('💾 Saved to localStorage:', isDark ? 'dark' : 'light');
1062
-
1063
- // Update icon
1064
- updateIcon();
1065
- console.log('✅ Theme toggled successfully');
1066
-
1067
- // Reapply chart theme
1068
- applyChartTheme();
1069
-
1070
- // Reload page to update charts properly
1071
- setTimeout(() => location.reload(), 300);
1072
- });
1073
-
1074
- // Chart.js theme colors - ULTRA AGGRESSIVE setup
1075
- function applyChartTheme() {
1076
- if (typeof Chart !== 'undefined') {
1077
- const isDark = document.body.classList.contains('dark-mode');
1078
- const textColor = isDark ? '#cdd6f4' : '#1f2937';
1079
- const gridColor = isDark ? 'rgba(88, 91, 112, 0.2)' : 'rgba(0, 0, 0, 0.1)';
1080
-
1081
- console.log('📊 Setting Chart.js theme:', isDark ? 'DARK' : 'light', '| Text:', textColor);
1082
-
1083
- // Global defaults
1084
- Chart.defaults.color = textColor;
1085
- Chart.defaults.borderColor = gridColor;
1086
- Chart.defaults.font = Chart.defaults.font || {};
1087
- Chart.defaults.font.color = textColor;
1088
-
1089
- // Scale defaults (axes) - AGGRESSIVE
1090
- if (Chart.defaults.scale) {
1091
- Chart.defaults.scale.ticks = Chart.defaults.scale.ticks || {};
1092
- Chart.defaults.scale.ticks.color = textColor;
1093
- Chart.defaults.scale.ticks.font = Chart.defaults.scale.ticks.font || {};
1094
- Chart.defaults.scale.ticks.font.color = textColor;
1095
-
1096
- Chart.defaults.scale.grid = Chart.defaults.scale.grid || {};
1097
- Chart.defaults.scale.grid.color = gridColor;
1098
-
1099
- // Axis title (xtitle, ytitle)
1100
- Chart.defaults.scale.title = Chart.defaults.scale.title || {};
1101
- Chart.defaults.scale.title.color = textColor;
1102
- Chart.defaults.scale.title.font = Chart.defaults.scale.title.font || {};
1103
- Chart.defaults.scale.title.font.size = 14;
1104
- }
1105
-
1106
- // X and Y axis specific
1107
- if (Chart.defaults.scales) {
1108
- // X axis
1109
- Chart.defaults.scales.x = Chart.defaults.scales.x || {};
1110
- Chart.defaults.scales.x.ticks = Chart.defaults.scales.x.ticks || {};
1111
- Chart.defaults.scales.x.ticks.color = textColor;
1112
- Chart.defaults.scales.x.title = Chart.defaults.scales.x.title || {};
1113
- Chart.defaults.scales.x.title.color = textColor;
1114
- Chart.defaults.scales.x.grid = Chart.defaults.scales.x.grid || {};
1115
- Chart.defaults.scales.x.grid.color = gridColor;
1116
-
1117
- // Y axis
1118
- Chart.defaults.scales.y = Chart.defaults.scales.y || {};
1119
- Chart.defaults.scales.y.ticks = Chart.defaults.scales.y.ticks || {};
1120
- Chart.defaults.scales.y.ticks.color = textColor;
1121
- Chart.defaults.scales.y.title = Chart.defaults.scales.y.title || {};
1122
- Chart.defaults.scales.y.title.color = textColor;
1123
- Chart.defaults.scales.y.grid = Chart.defaults.scales.y.grid || {};
1124
- Chart.defaults.scales.y.grid.color = gridColor;
1125
- }
1126
-
1127
- // Plugin defaults
1128
- if (Chart.defaults.plugins) {
1129
- // Legend
1130
- if (Chart.defaults.plugins.legend) {
1131
- Chart.defaults.plugins.legend.labels = Chart.defaults.plugins.legend.labels || {};
1132
- Chart.defaults.plugins.legend.labels.color = textColor;
1133
- Chart.defaults.plugins.legend.labels.font = Chart.defaults.plugins.legend.labels.font || {};
1134
- Chart.defaults.plugins.legend.labels.font.color = textColor;
1135
- }
1136
-
1137
- // Tooltip
1138
- if (Chart.defaults.plugins.tooltip) {
1139
- Chart.defaults.plugins.tooltip.backgroundColor = isDark ? '#313244' : 'rgba(255, 255, 255, 0.95)';
1140
- Chart.defaults.plugins.tooltip.titleColor = isDark ? textColor : '#1f2937';
1141
- Chart.defaults.plugins.tooltip.bodyColor = isDark ? textColor : '#1f2937';
1142
- Chart.defaults.plugins.tooltip.borderColor = isDark ? '#585b70' : 'rgba(0, 0, 0, 0.2)';
1143
- Chart.defaults.plugins.tooltip.borderWidth = 1;
1144
- }
1145
-
1146
- // Title plugin
1147
- if (Chart.defaults.plugins.title) {
1148
- Chart.defaults.plugins.title.color = textColor;
1149
- Chart.defaults.plugins.title.font = Chart.defaults.plugins.title.font || {};
1150
- Chart.defaults.plugins.title.font.color = textColor;
1151
- }
1152
- }
1153
-
1154
- console.log('✅ Chart.js theme applied - all text should be:', textColor);
1155
- } else {
1156
- console.warn('⚠️ Chart.js not loaded yet');
1157
- }
1158
-
1159
- // Also update Google Charts if present
1160
- if (typeof google !== 'undefined' && google.visualization) {
1161
- console.log('📊 Google Charts detected - applying theme');
1162
- }
1163
- }
1164
-
1165
- // Apply chart theme on load
1166
- applyChartTheme();
1167
-
1168
- // NUCLEAR OPTION: Watch for chart creation and force colors
1169
- const observer = new MutationObserver(function(mutations) {
1170
- mutations.forEach(function(mutation) {
1171
- mutation.addedNodes.forEach(function(node) {
1172
- if (node.tagName === 'CANVAS') {
1173
- console.log('🎨 New chart detected, forcing theme...');
1174
- setTimeout(applyChartTheme, 100);
1175
- }
1176
- });
1177
- });
1178
- });
1179
-
1180
- observer.observe(document.body, {
1181
- childList: true,
1182
- subtree: true
1183
- });
1184
-
1185
- // Also listen for Chartkick chart creation events
1186
- document.addEventListener('chartkick:load', function() {
1187
- console.log('📊 Chartkick loaded, applying theme');
1188
- applyChartTheme();
1189
- });
1190
-
1191
- // Force reapply every 500ms for the first 3 seconds (in case charts load late)
1192
- let attempts = 0;
1193
- const forceInterval = setInterval(function() {
1194
- attempts++;
1195
- applyChartTheme();
1196
- console.log('🔄 Force applying theme (attempt', attempts, ')');
1197
- if (attempts >= 6) {
1198
- clearInterval(forceInterval);
1199
- console.log('✅ Stopped force applying');
1200
- }
1201
- }, 500);
1202
-
1203
- // Copy to clipboard functionality
1204
- window.copyToClipboard = function(text, button) {
1205
- navigator.clipboard.writeText(text).then(function() {
1206
- // Store original button HTML
1207
- const originalHTML = button.innerHTML;
1208
-
1209
- // Show success state on button
1210
- button.innerHTML = '<i class="bi bi-check"></i> Copied!';
1211
- button.classList.remove('btn-outline-secondary');
1212
- button.classList.add('btn-success');
1213
-
1214
- // Show toast notification
1215
- showToast('Copied to clipboard!', 'success');
1216
-
1217
- // Reset button after 2 seconds
1218
- setTimeout(function() {
1219
- button.innerHTML = originalHTML;
1220
- button.classList.remove('btn-success');
1221
- button.classList.add('btn-outline-secondary');
1222
- }, 2000);
1223
- }).catch(function(err) {
1224
- console.error('Failed to copy:', err);
1225
- button.innerHTML = '<i class="bi bi-x"></i> Failed';
1226
- showToast('Failed to copy to clipboard', 'danger');
1227
- });
1228
- };
1229
-
1230
- // Toast notification functionality
1231
- window.showToast = function(message, type) {
1232
- type = type || 'success';
1233
-
1234
- const toastId = 'toast-' + Date.now();
1235
- const iconClass = type === 'success' ? 'bi-check-circle-fill' :
1236
- type === 'danger' ? 'bi-exclamation-circle-fill' :
1237
- 'bi-info-circle-fill';
1238
- const bgClass = type === 'success' ? 'bg-success' :
1239
- type === 'danger' ? 'bg-danger' :
1240
- 'bg-info';
1241
-
1242
- const toastHTML = `
1243
- <div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
1244
- <div class="d-flex">
1245
- <div class="toast-body">
1246
- <i class="bi ${iconClass} me-2"></i>
1247
- ${message}
1248
- </div>
1249
- <button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
1250
- </div>
1251
- </div>
1252
- `;
1253
-
1254
- const container = document.querySelector('.toast-container');
1255
- container.insertAdjacentHTML('beforeend', toastHTML);
1256
-
1257
- const toastElement = document.getElementById(toastId);
1258
- const toast = new bootstrap.Toast(toastElement, { delay: 4000 });
1259
- toast.show();
1260
-
1261
- // Remove toast element after it's hidden
1262
- toastElement.addEventListener('hidden.bs.toast', function() {
1263
- toastElement.remove();
1264
- });
1265
- };
1266
276
 
1267
- // Show flash messages as toasts
1268
- <% if defined?(flash) && flash.present? %>
1269
- <% if flash[:notice] %>
1270
- showToast('<%= j flash[:notice] %>', 'success');
1271
- <% end %>
1272
- <% if flash[:alert] %>
1273
- showToast('<%= j flash[:alert] %>', 'danger');
1274
- <% end %>
1275
- <% if flash[:success] %>
1276
- showToast('<%= j flash[:success] %>', 'success');
1277
- <% end %>
1278
- <% if flash[:error] %>
1279
- showToast('<%= j flash[:error] %>', 'danger');
1280
- <% end %>
277
+ <!-- Dashboard JavaScript -->
278
+ <script src="/rails_error_dashboard/js/syntax-highlighting.js"></script>
279
+ <script src="/rails_error_dashboard/js/theme-toggle.js"></script>
280
+ <script src="/rails_error_dashboard/js/utilities.js"></script>
281
+ <script src="/rails_error_dashboard/js/sidebar-toggle.js"></script>
282
+
283
+ <!-- Flash Messages -->
284
+ <script>
285
+ // Show flash messages as toasts
286
+ <% if defined?(flash) && flash.present? %>
287
+ <% if flash[:notice] %>
288
+ showToast('<%= j flash[:notice] %>', 'success');
1281
289
  <% end %>
1282
-
1283
- // Local Timezone Conversion
1284
- // Converts UTC timestamps to user's local timezone on page load
1285
- function convertToLocalTime() {
1286
- // Convert all .local-time elements (formatted timestamps)
1287
- document.querySelectorAll('.local-time').forEach(function(element) {
1288
- const utcString = element.dataset.utc;
1289
- const formatString = element.dataset.format;
1290
-
1291
- if (!utcString) return;
1292
-
1293
- try {
1294
- const date = new Date(utcString);
1295
- if (isNaN(date.getTime())) return; // Invalid date
1296
-
1297
- // Parse the format string and convert to local time
1298
- const formatted = formatDateTime(date, formatString);
1299
-
1300
- // Get timezone abbreviation
1301
- const timezone = getTimezoneAbbreviation(date);
1302
-
1303
- // Update element text
1304
- element.textContent = formatted + ' ' + timezone;
1305
- element.title = 'Your local time (click to see UTC)';
1306
-
1307
- // Add click handler to toggle between local and UTC
1308
- element.style.cursor = 'pointer';
1309
- element.dataset.originalUtc = utcString;
1310
- element.dataset.localFormatted = formatted + ' ' + timezone;
1311
- element.dataset.showingLocal = 'true';
1312
-
1313
- element.addEventListener('click', function() {
1314
- if (this.dataset.showingLocal === 'true') {
1315
- // Show UTC
1316
- const utcDate = new Date(this.dataset.originalUtc);
1317
- const utcFormatted = formatDateTime(utcDate, formatString);
1318
- this.textContent = utcFormatted + ' UTC';
1319
- this.title = 'UTC time (click to see local time)';
1320
- this.dataset.showingLocal = 'false';
1321
- } else {
1322
- // Show local
1323
- this.textContent = this.dataset.localFormatted;
1324
- this.title = 'Your local time (click to see UTC)';
1325
- this.dataset.showingLocal = 'true';
1326
- }
1327
- });
1328
- } catch (e) {
1329
- console.error('Error converting timestamp:', e);
1330
- }
1331
- });
1332
-
1333
- // Convert all .local-time-ago elements (relative time)
1334
- document.querySelectorAll('.local-time-ago').forEach(function(element) {
1335
- const utcString = element.dataset.utc;
1336
- if (!utcString) return;
1337
-
1338
- try {
1339
- const date = new Date(utcString);
1340
- if (isNaN(date.getTime())) return; // Invalid date
1341
-
1342
- // Calculate relative time
1343
- const now = new Date();
1344
- const diffMs = now - date;
1345
- const formatted = formatRelativeTime(diffMs);
1346
-
1347
- // Update element text
1348
- element.textContent = formatted;
1349
- element.title = 'Click to see exact time';
1350
-
1351
- // Add click handler to toggle between relative and absolute
1352
- element.style.cursor = 'pointer';
1353
- element.dataset.originalUtc = utcString;
1354
- element.dataset.showingRelative = 'true';
1355
-
1356
- element.addEventListener('click', function() {
1357
- if (this.dataset.showingRelative === 'true') {
1358
- // Show absolute time
1359
- const absoluteDate = new Date(this.dataset.originalUtc);
1360
- const absoluteFormatted = formatDateTime(absoluteDate, '%B %d, %Y %I:%M:%S %p');
1361
- const timezone = getTimezoneAbbreviation(absoluteDate);
1362
- this.textContent = absoluteFormatted + ' ' + timezone;
1363
- this.title = 'Click to see relative time';
1364
- this.dataset.showingRelative = 'false';
1365
- } else {
1366
- // Show relative time
1367
- const now = new Date();
1368
- const date = new Date(this.dataset.originalUtc);
1369
- const diffMs = now - date;
1370
- this.textContent = formatRelativeTime(diffMs);
1371
- this.title = 'Click to see exact time';
1372
- this.dataset.showingRelative = 'true';
1373
- }
1374
- });
1375
- } catch (e) {
1376
- console.error('Error converting relative time:', e);
1377
- }
1378
- });
1379
- }
1380
-
1381
- // Format date according to strftime-like format string
1382
- function formatDateTime(date, formatString) {
1383
- const months = ['January', 'February', 'March', 'April', 'May', 'June',
1384
- 'July', 'August', 'September', 'October', 'November', 'December'];
1385
- const monthsShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
1386
- 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1387
- const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
1388
- const daysShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1389
-
1390
- const year = date.getFullYear();
1391
- const month = date.getMonth();
1392
- const day = date.getDate();
1393
- const hours = date.getHours();
1394
- const minutes = date.getMinutes();
1395
- const seconds = date.getSeconds();
1396
- const dayOfWeek = date.getDay();
1397
-
1398
- // 12-hour format
1399
- const hours12 = hours % 12 || 12;
1400
- const ampm = hours >= 12 ? 'PM' : 'AM';
1401
-
1402
- // Padding helper
1403
- const pad = (n) => n.toString().padStart(2, '0');
1404
-
1405
- // Replace format specifiers
1406
- let result = formatString
1407
- .replace('%Y', year)
1408
- .replace('%y', year.toString().substr(2))
1409
- .replace('%B', months[month])
1410
- .replace('%b', monthsShort[month])
1411
- .replace('%m', pad(month + 1))
1412
- .replace('%d', pad(day))
1413
- .replace('%e', day)
1414
- .replace('%A', days[dayOfWeek])
1415
- .replace('%a', daysShort[dayOfWeek])
1416
- .replace('%H', pad(hours))
1417
- .replace('%I', pad(hours12))
1418
- .replace('%M', pad(minutes))
1419
- .replace('%S', pad(seconds))
1420
- .replace('%p', ampm)
1421
- .replace('%P', ampm.toLowerCase());
1422
-
1423
- return result;
1424
- }
1425
-
1426
- // Get timezone abbreviation (e.g., "PST", "EST", "UTC+2")
1427
- function getTimezoneAbbreviation(date) {
1428
- const timeZoneString = date.toLocaleTimeString('en-US', { timeZoneName: 'short' });
1429
- const parts = timeZoneString.split(' ');
1430
- return parts[parts.length - 1]; // Last part is timezone abbreviation
1431
- }
1432
-
1433
- // Format relative time ("3 hours ago", "2 days ago")
1434
- function formatRelativeTime(diffMs) {
1435
- const seconds = Math.floor(diffMs / 1000);
1436
- const minutes = Math.floor(seconds / 60);
1437
- const hours = Math.floor(minutes / 60);
1438
- const days = Math.floor(hours / 24);
1439
- const months = Math.floor(days / 30);
1440
- const years = Math.floor(days / 365);
1441
-
1442
- if (seconds < 60) {
1443
- return seconds <= 1 ? '1 second ago' : seconds + ' seconds ago';
1444
- } else if (minutes < 60) {
1445
- return minutes === 1 ? '1 minute ago' : minutes + ' minutes ago';
1446
- } else if (hours < 24) {
1447
- return hours === 1 ? '1 hour ago' : hours + ' hours ago';
1448
- } else if (days < 30) {
1449
- return days === 1 ? '1 day ago' : days + ' days ago';
1450
- } else if (months < 12) {
1451
- return months === 1 ? '1 month ago' : months + ' months ago';
1452
- } else {
1453
- return years === 1 ? '1 year ago' : years + ' years ago';
1454
- }
1455
- }
1456
-
1457
- // Run conversion on page load
1458
- convertToLocalTime();
1459
-
1460
- // Also run after Turbo navigation (if using Turbo/Hotwire)
1461
- if (typeof Turbo !== 'undefined') {
1462
- document.addEventListener('turbo:load', convertToLocalTime);
1463
- document.addEventListener('turbo:frame-load', convertToLocalTime);
1464
- }
1465
- });
290
+ <% if flash[:alert] %>
291
+ showToast('<%= j flash[:alert] %>', 'danger');
292
+ <% end %>
293
+ <% if flash[:success] %>
294
+ showToast('<%= j flash[:success] %>', 'success');
295
+ <% end %>
296
+ <% if flash[:error] %>
297
+ showToast('<%= j flash[:error] %>', 'danger');
298
+ <% end %>
299
+ <% end %>
1466
300
  </script>
1467
301
  </body>
1468
302
  </html>