rails_error_dashboard 0.5.15 → 0.6.1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/rails_error_dashboard/application_controller.rb +37 -12
  3. data/app/controllers/rails_error_dashboard/errors_controller.rb +48 -26
  4. data/app/helpers/rails_error_dashboard/application_helper.rb +12 -5
  5. data/app/views/layouts/rails_error_dashboard.html.erb +1219 -1927
  6. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +4 -4
  7. data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +1 -1
  8. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +3 -3
  9. data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +1 -1
  10. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +69 -79
  11. data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +1 -1
  12. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +1 -1
  13. data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +1 -1
  14. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +2 -2
  15. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +1 -1
  16. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +1 -1
  17. data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +1 -1
  18. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +4 -4
  19. data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +6 -6
  20. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +6 -6
  21. data/app/views/rails_error_dashboard/errors/analytics.html.erb +34 -50
  22. data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +7 -7
  23. data/app/views/rails_error_dashboard/errors/correlation.html.erb +11 -11
  24. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +114 -172
  25. data/app/views/rails_error_dashboard/errors/deprecations.html.erb +7 -7
  26. data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +6 -6
  27. data/app/views/rails_error_dashboard/errors/index.html.erb +311 -613
  28. data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +7 -7
  29. data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +7 -7
  30. data/app/views/rails_error_dashboard/errors/overview.html.erb +192 -363
  31. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +11 -11
  32. data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +6 -6
  33. data/app/views/rails_error_dashboard/errors/releases.html.erb +6 -6
  34. data/app/views/rails_error_dashboard/errors/settings.html.erb +53 -51
  35. data/app/views/rails_error_dashboard/errors/show.html.erb +200 -203
  36. data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +19 -19
  37. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +6 -6
  38. data/config/routes.rb +1 -0
  39. data/lib/rails_error_dashboard/configuration.rb +6 -0
  40. data/lib/rails_error_dashboard/services/error_broadcaster.rb +1 -1
  41. data/lib/rails_error_dashboard/services/system_health_snapshot.rb +1 -1
  42. data/lib/rails_error_dashboard/test_error.rb +7 -0
  43. data/lib/rails_error_dashboard/version.rb +1 -1
  44. data/lib/rails_error_dashboard.rb +1 -0
  45. metadata +3 -2
@@ -1,5 +1,32 @@
1
1
  <!DOCTYPE html>
2
- <html>
2
+ <%
3
+ # Accent color configuration — safe lookup with fallback
4
+ accent = begin
5
+ RailsErrorDashboard.configuration.accent_color&.to_sym || :crimson
6
+ rescue
7
+ :crimson
8
+ end
9
+
10
+ accent_colors = {
11
+ crimson: { light: '#DC2626', dark: '#f38ba8', subtle_light: 'rgba(220,38,38,0.08)', subtle_dark: 'rgba(243,139,168,0.1)' },
12
+ ruby: { light: '#E11D48', dark: '#f472b6', subtle_light: 'rgba(225,29,72,0.08)', subtle_dark: 'rgba(244,114,182,0.1)' },
13
+ ember: { light: '#EA580C', dark: '#fb923c', subtle_light: 'rgba(234,88,12,0.08)', subtle_dark: 'rgba(251,146,60,0.1)' },
14
+ violet: { light: '#8B5CF6', dark: '#cba6f7', subtle_light: 'rgba(139,92,246,0.08)', subtle_dark: 'rgba(203,166,247,0.1)' },
15
+ }
16
+ ac = accent_colors[accent] || accent_colors[:crimson]
17
+
18
+ # Unresolved error count for sidebar badge — safe with fallback
19
+ unresolved_badge_count = begin
20
+ scope = RailsErrorDashboard::ErrorLog.unresolved
21
+ if params[:application_id].present?
22
+ scope = scope.where(application_id: params[:application_id])
23
+ end
24
+ scope.count
25
+ rescue
26
+ nil
27
+ end
28
+ %>
29
+ <html lang="en" data-theme="light">
3
30
  <head>
4
31
  <title><%= content_for?(:page_title) ? "#{content_for(:page_title)} | " : "" %><%= Rails.application.class.module_parent_name %> - RED</title>
5
32
  <meta name="viewport" content="width=device-width,initial-scale=1">
@@ -11,1641 +38,979 @@
11
38
  <%= csp_meta_tag %>
12
39
  <% end %>
13
40
 
14
- <!-- Bootstrap CSS -->
15
- <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
16
- <!-- Bootstrap Icons -->
17
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
18
-
19
- <!-- Chart.js -->
20
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
21
- <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
22
- <script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script>
23
-
24
- <!-- Syntax Highlighting for Source Code Viewer -->
25
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@catppuccin/highlightjs@1.0.0/css/catppuccin-mocha.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 (inline for production compatibility) -->
30
- <style>
31
- /* Catppuccin Mocha Color Palette */
32
- :root {
33
- --ctp-rosewater: #f5e0dc;
34
- --ctp-flamingo: #f2cdcd;
35
- --ctp-pink: #f5c2e7;
36
- --ctp-mauve: #cba6f7;
37
- --ctp-red: #f38ba8;
38
- --ctp-maroon: #eba0ac;
39
- --ctp-peach: #fab387;
40
- --ctp-yellow: #f9e2af;
41
- --ctp-green: #a6e3a1;
42
- --ctp-teal: #94e2d5;
43
- --ctp-sky: #89dceb;
44
- --ctp-sapphire: #74c7ec;
45
- --ctp-blue: #89b4fa;
46
- --ctp-lavender: #b4befe;
47
- --ctp-text: #cdd6f4;
48
- --ctp-subtext1: #bac2de;
49
- --ctp-subtext0: #a6adc8;
50
- --ctp-overlay2: #9399b2;
51
- --ctp-overlay1: #7f849c;
52
- --ctp-overlay0: #6c7086;
53
- --ctp-surface2: #585b70;
54
- --ctp-surface1: #45475a;
55
- --ctp-surface0: #313244;
56
- --ctp-base: #1e1e2e;
57
- --ctp-mantle: #181825;
58
- --ctp-crust: #11111b;
59
- }
60
-
61
- /* Light Theme (Default) */
62
- body {
63
- background-color: #f3f4f6;
64
- color: #1f2937;
65
- transition: background-color 0.3s, color 0.3s;
66
- }
67
-
68
- /* Dark Theme */
69
- body.dark-mode {
70
- background-color: var(--ctp-base);
71
- color: var(--ctp-text);
72
- }
73
-
74
- /* Navbar */
75
- .navbar {
76
- background: linear-gradient(135deg, #8B5CF6, #6D28D9) !important;
77
- color: white !important;
78
- }
79
- .navbar * {
80
- color: white !important;
81
- }
82
- .navbar-brand {
83
- font-weight: bold;
84
- }
85
-
86
- /* Theme Toggle Button */
87
- .theme-toggle {
88
- cursor: pointer;
89
- padding: 0.5rem 1rem;
90
- border-radius: 0.5rem;
91
- background-color: rgba(255, 255, 255, 0.1);
92
- color: white;
93
- border: none;
94
- transition: background-color 0.2s;
95
- }
96
- .theme-toggle:hover {
97
- background-color: rgba(255, 255, 255, 0.2);
98
- }
99
-
100
- /* App Switcher Button - must override navbar * rule */
101
- .app-switcher-btn {
102
- background-color: rgba(255, 255, 255, 0.1) !important;
103
- color: white !important;
104
- border: 1px solid rgba(255, 255, 255, 0.3) !important;
105
- transition: background-color 0.2s;
106
- }
107
- .app-switcher-btn:hover {
108
- background-color: rgba(255, 255, 255, 0.2) !important;
109
- }
110
- .app-switcher-btn i,
111
- .app-switcher-btn * {
112
- color: white !important;
113
- }
114
-
115
- /* Sidebar */
116
- .sidebar {
117
- background: white;
118
- min-height: calc(100vh - 56px);
119
- box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
120
- }
121
- body.dark-mode .sidebar {
122
- background: var(--ctp-mantle);
123
- }
124
- .sidebar .nav-link {
125
- color: #1f2937;
126
- padding: 0.75rem 1.5rem;
127
- border-left: 3px solid transparent;
128
- transition: all 0.2s;
129
- }
130
- body.dark-mode .sidebar .nav-link {
131
- color: var(--ctp-text);
132
- }
133
- .sidebar .nav-link:hover {
134
- background-color: #f3f4f6;
135
- color: #8B5CF6;
136
- border-left-color: #8B5CF6;
137
- }
138
- body.dark-mode .sidebar .nav-link:hover {
139
- background-color: var(--ctp-surface0);
140
- color: var(--ctp-mauve);
141
- border-left-color: var(--ctp-mauve);
142
- }
143
- .sidebar .nav-link.active {
144
- background-color: #f3f4f6;
145
- color: #8B5CF6;
146
- border-left-color: #8B5CF6;
147
- font-weight: 600;
148
- }
149
- body.dark-mode .sidebar .nav-link.active {
150
- background-color: var(--ctp-surface0);
151
- color: var(--ctp-mauve);
152
- border-left-color: var(--ctp-mauve);
153
- }
154
-
155
- /* Cards */
156
- .card {
157
- background: white;
158
- border: 1px solid #e5e7eb;
159
- transition: background-color 0.3s, border-color 0.3s;
160
- }
161
- body.dark-mode .card {
162
- background: var(--ctp-surface0);
163
- border-color: var(--ctp-surface2);
164
- color: var(--ctp-text);
165
- }
166
- body.dark-mode .card-header {
167
- background-color: var(--ctp-surface1);
168
- border-color: var(--ctp-surface2);
169
- color: var(--ctp-text);
170
- }
171
-
172
- /* Tables */
173
- body.dark-mode .table {
174
- color: var(--ctp-text);
175
- }
176
- body.dark-mode .table-hover tbody tr:hover {
177
- background-color: var(--ctp-surface1);
178
- }
179
-
180
- /* Sticky table header for error list */
181
- .table-responsive thead th {
182
- position: sticky;
183
- top: 0;
184
- z-index: 10;
185
- background-color: #f8f9fa;
186
- }
187
- body.dark-mode .table-responsive thead th {
188
- background-color: var(--ctp-mantle);
189
- }
190
-
191
- /* Badges - Platform Colors */
192
- .badge-ios {
193
- background-color: #000;
194
- color: white;
195
- }
196
- body.dark-mode .badge-ios {
197
- background-color: var(--ctp-overlay0);
198
- color: var(--ctp-text);
199
- border: 1px solid var(--ctp-surface2);
200
- }
201
- .badge-android {
202
- background-color: #3DDC84;
203
- color: white;
204
- }
205
- body.dark-mode .badge-android {
206
- background-color: rgba(166, 227, 161, 0.2);
207
- color: var(--ctp-green);
208
- border: 1px solid var(--ctp-green);
209
- }
210
- .badge-web {
211
- background-color: #3B82F6;
212
- color: white;
213
- }
214
- body.dark-mode .badge-web {
215
- background-color: rgba(137, 180, 250, 0.2);
216
- color: var(--ctp-blue);
217
- border: 1px solid var(--ctp-blue);
218
- }
219
- .badge-api {
220
- background-color: #8B5CF6;
221
- color: white;
222
- }
223
- body.dark-mode .badge-api {
224
- background-color: rgba(116, 199, 236, 0.2);
225
- color: var(--ctp-sapphire);
226
- border: 1px solid var(--ctp-sapphire);
227
- }
228
-
229
- /* Forms */
230
- body.dark-mode .form-control,
231
- body.dark-mode .form-select {
232
- background-color: var(--ctp-surface0);
233
- border-color: var(--ctp-surface2);
234
- color: var(--ctp-text);
235
- }
236
- body.dark-mode .form-control:focus,
237
- body.dark-mode .form-select:focus {
238
- background-color: var(--ctp-surface1);
239
- border-color: var(--ctp-mauve);
240
- color: var(--ctp-text);
241
- }
242
-
243
- /* Code Blocks */
244
- .code-block,
245
- pre,
246
- code {
247
- background-color: #f9fafb;
248
- color: #1f2937;
249
- }
250
- body.dark-mode .code-block,
251
- body.dark-mode pre,
252
- body.dark-mode code {
253
- background-color: var(--ctp-mantle) !important;
254
- color: var(--ctp-text) !important;
255
- }
256
-
257
- /* Inline Code Highlighting (for backticks in comments/text) */
258
- .inline-code-highlight {
259
- padding: 2px 6px;
260
- margin: 0 2px;
261
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', 'Fira Mono', 'Roboto Mono', monospace;
262
- font-size: 0.9em;
263
- background-color: #f3f4f6;
264
- color: #6d28d9;
265
- border: 1px solid #e5e7eb;
266
- border-radius: 4px;
267
- white-space: nowrap;
268
- }
269
-
270
- body.dark-mode .inline-code-highlight {
271
- background-color: var(--ctp-surface0);
272
- color: var(--ctp-mauve);
273
- border: 1px solid var(--ctp-surface2);
274
- }
275
-
276
- /* File Path Links (elvish magic for GitHub linking) */
277
- .file-path-link {
278
- cursor: pointer;
279
- transition: all 0.2s ease;
280
- }
281
-
282
- .file-path-link:hover {
283
- background-color: #e0e7ff !important;
284
- color: #4c1d95 !important;
285
- border-color: #c7d2fe !important;
286
- box-shadow: 0 0 0 3px rgba(109, 40, 217, 0.1);
287
- }
288
-
289
- body.dark-mode .file-path-link:hover {
290
- background-color: var(--ctp-surface2) !important;
291
- color: var(--ctp-lavender) !important;
292
- border-color: var(--ctp-mauve) !important;
293
- box-shadow: 0 0 0 3px rgba(203, 166, 247, 0.2);
294
- }
295
-
296
- /* Alerts */
297
- body.dark-mode .alert {
298
- background-color: var(--ctp-surface0);
299
- border-color: var(--ctp-surface2);
300
- color: var(--ctp-text);
301
- }
302
-
303
- /* Text colors */
304
- body.dark-mode .text-muted {
305
- color: var(--ctp-subtext0) !important;
306
- }
307
- body.dark-mode .text-primary {
308
- color: var(--ctp-mauve) !important;
309
- }
310
- body.dark-mode .text-danger {
311
- color: var(--ctp-red) !important;
312
- }
313
- body.dark-mode .text-success {
314
- color: var(--ctp-green) !important;
315
- }
316
- body.dark-mode .text-warning {
317
- color: var(--ctp-peach) !important;
318
- }
319
-
320
- /* Override Bootstrap bg-white and bg-light */
321
- body.dark-mode .bg-white {
322
- background-color: var(--ctp-surface0) !important;
323
- }
324
- body.dark-mode .bg-light {
325
- background-color: var(--ctp-surface1) !important;
326
- color: var(--ctp-text) !important;
327
- }
328
-
329
- /* Borders */
330
- body.dark-mode .border {
331
- border-color: var(--ctp-surface2) !important;
332
- }
333
- body.dark-mode .rounded {
334
- border-color: var(--ctp-surface2) !important;
335
- }
336
-
337
- /* Quick Filters Sidebar Section */
338
- .sidebar h6 {
339
- font-size: 0.75rem;
340
- text-transform: uppercase;
341
- letter-spacing: 0.05em;
342
- padding: 0.75rem 1.5rem;
343
- margin: 0;
344
- color: #6B7280;
345
- }
346
- body.dark-mode .sidebar h6 {
347
- color: var(--ctp-subtext0);
348
- }
349
-
350
- /* Stat Cards */
351
- .stat-card {
352
- border-radius: 0.75rem;
353
- transition: transform 0.2s, box-shadow 0.2s;
354
- }
355
- .stat-card:hover {
356
- transform: translateY(-2px);
357
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
358
- }
359
-
360
- /* Badges for severity */
361
- .badge.bg-danger {
362
- background-color: #EF4444 !important;
363
- }
364
- .badge.bg-warning {
365
- background-color: #F59E0B !important;
366
- }
367
- .badge.bg-info {
368
- background-color: #3B82F6 !important;
369
- }
370
- .badge.bg-secondary {
371
- background-color: #6B7280 !important;
372
- }
373
- .badge.bg-success {
374
- background-color: #10B981 !important;
375
- }
376
-
377
- body.dark-mode .badge.bg-danger {
378
- background-color: var(--ctp-red) !important;
379
- color: var(--ctp-base) !important;
380
- }
381
- body.dark-mode .badge.bg-warning {
382
- background-color: var(--ctp-peach) !important;
383
- color: var(--ctp-base) !important;
384
- }
385
- body.dark-mode .badge.bg-info {
386
- background-color: var(--ctp-blue) !important;
387
- color: var(--ctp-base) !important;
388
- }
389
- body.dark-mode .badge.bg-secondary {
390
- background-color: var(--ctp-overlay1) !important;
391
- }
392
- body.dark-mode .badge.bg-success {
393
- background-color: var(--ctp-green) !important;
394
- color: var(--ctp-base) !important;
395
- }
396
-
397
- /* Links */
398
- a {
399
- color: #8B5CF6;
400
- text-decoration: none;
401
- }
402
- a:hover {
403
- color: #6D28D9;
404
- text-decoration: underline;
405
- }
406
- body.dark-mode a {
407
- color: var(--ctp-mauve);
408
- }
409
- body.dark-mode a:hover {
410
- color: var(--ctp-pink);
411
- }
412
-
413
- /* Buttons */
414
- body.dark-mode .btn-primary {
415
- background-color: var(--ctp-mauve);
416
- border-color: var(--ctp-mauve);
417
- color: var(--ctp-base);
418
- }
419
- body.dark-mode .btn-primary:hover {
420
- background-color: var(--ctp-pink);
421
- border-color: var(--ctp-pink);
422
- }
423
- body.dark-mode .btn-outline-primary {
424
- color: var(--ctp-mauve);
425
- border-color: var(--ctp-mauve);
426
- }
427
- body.dark-mode .btn-outline-primary:hover {
428
- background-color: var(--ctp-mauve);
429
- color: var(--ctp-base);
430
- }
431
- body.dark-mode .btn-secondary,
432
- body.dark-mode .btn-outline-secondary {
433
- background-color: var(--ctp-surface1);
434
- border-color: var(--ctp-surface2);
435
- color: var(--ctp-text);
436
- }
437
-
438
- /* Modal dialogs */
439
- body.dark-mode .modal-content {
440
- background-color: var(--ctp-surface0);
441
- color: var(--ctp-text);
442
- border-color: var(--ctp-surface2);
443
- }
444
- body.dark-mode .modal-header {
445
- background-color: var(--ctp-surface1);
446
- border-bottom-color: var(--ctp-surface2);
447
- color: var(--ctp-text);
448
- }
449
- body.dark-mode .modal-footer {
450
- background-color: var(--ctp-surface1);
451
- border-top-color: var(--ctp-surface2);
452
- }
453
- body.dark-mode .modal-title {
454
- color: var(--ctp-text);
455
- }
456
- body.dark-mode .btn-close {
457
- filter: invert(1);
458
- }
459
-
460
- /* Table headers - CRITICAL FIX */
461
- body.dark-mode thead,
462
- body.dark-mode thead th {
463
- background-color: var(--ctp-surface1) !important;
464
- color: var(--ctp-text) !important;
465
- border-color: var(--ctp-surface2) !important;
466
- }
467
- body.dark-mode tbody tr {
468
- border-color: var(--ctp-surface2) !important;
469
- background-color: transparent !important;
470
- }
471
- body.dark-mode tbody td {
472
- border-color: var(--ctp-surface2) !important;
473
- background-color: transparent !important;
474
- color: var(--ctp-text) !important;
475
- }
476
- body.dark-mode tbody th {
477
- background-color: var(--ctp-surface0) !important;
478
- color: var(--ctp-text) !important;
479
- border-color: var(--ctp-surface2) !important;
480
- }
481
- body.dark-mode table {
482
- color: var(--ctp-text) !important;
483
- }
484
-
485
- /* Chart canvas backgrounds */
486
- body.dark-mode canvas {
487
- background-color: transparent !important;
488
- }
489
-
490
- /* Chart labels and text */
491
- body.dark-mode .chart-container text {
492
- fill: var(--ctp-text) !important;
493
- }
494
-
495
- /* Form placeholders */
496
- body.dark-mode .form-control::placeholder,
497
- body.dark-mode .form-select::placeholder {
498
- color: var(--ctp-overlay0);
499
- opacity: 1;
500
- }
501
-
502
- /* Dropdown menus */
503
- .dropdown-menu {
504
- background-color: white;
505
- border: 1px solid rgba(0, 0, 0, 0.15);
506
- }
507
- .dropdown-item {
508
- color: #1f2937 !important;
509
- }
510
- .dropdown-item:hover,
511
- .dropdown-item:focus {
512
- background-color: #f3f4f6;
513
- color: #8B5CF6 !important;
514
- }
515
- .dropdown-item.active {
516
- background-color: #8B5CF6;
517
- color: white !important;
518
- }
519
-
520
- body.dark-mode .dropdown-menu {
521
- background-color: var(--ctp-surface0);
522
- border-color: var(--ctp-surface2);
523
- }
524
- body.dark-mode .dropdown-item {
525
- color: var(--ctp-text) !important;
526
- }
527
- body.dark-mode .dropdown-item:hover,
528
- body.dark-mode .dropdown-item:focus {
529
- background-color: var(--ctp-surface1);
530
- color: var(--ctp-mauve) !important;
531
- }
532
- body.dark-mode .dropdown-item.active {
533
- background-color: var(--ctp-mauve);
534
- color: var(--ctp-base) !important;
535
- }
536
-
537
- /* Pagination */
538
- body.dark-mode .pagination .page-link {
539
- background-color: var(--ctp-surface0);
540
- border-color: var(--ctp-surface2);
541
- color: var(--ctp-text);
542
- }
543
- body.dark-mode .pagination .page-link:hover {
544
- background-color: var(--ctp-surface1);
545
- color: var(--ctp-mauve);
546
- }
547
- body.dark-mode .pagination .page-item.active .page-link {
548
- background-color: var(--ctp-mauve);
549
- border-color: var(--ctp-mauve);
550
- color: var(--ctp-base);
551
- }
552
-
553
- /* Progress bars */
554
- body.dark-mode .progress {
555
- background-color: var(--ctp-surface1);
556
- }
557
- body.dark-mode .progress-bar {
558
- background-color: var(--ctp-mauve);
559
- }
560
-
561
- /* Horizontal rules */
562
- body.dark-mode hr {
563
- border-color: var(--ctp-surface2);
564
- opacity: 1;
565
- }
566
-
567
- /* List groups */
568
- body.dark-mode .list-group-item {
569
- background-color: var(--ctp-surface0);
570
- border-color: var(--ctp-surface2);
571
- color: var(--ctp-text);
572
- }
573
- body.dark-mode .list-group-item:hover {
574
- background-color: var(--ctp-surface1);
575
- }
576
-
577
- /* Offcanvas (mobile menu) */
578
- body.dark-mode .offcanvas {
579
- background-color: var(--ctp-mantle);
580
- color: var(--ctp-text);
581
- }
582
- body.dark-mode .offcanvas-header {
583
- border-bottom-color: var(--ctp-surface2);
584
- }
585
-
586
- /* Small text and labels */
587
- body.dark-mode small {
588
- color: var(--ctp-subtext1) !important;
589
- }
590
- body.dark-mode label {
591
- color: var(--ctp-text);
592
- }
593
-
594
- /* Breadcrumbs */
595
- body.dark-mode .breadcrumb {
596
- background-color: var(--ctp-surface0);
597
- }
598
- body.dark-mode .breadcrumb-item {
599
- color: var(--ctp-text);
600
- }
601
- body.dark-mode .breadcrumb-item.active {
602
- color: var(--ctp-subtext0);
603
- }
604
-
605
- /* Tooltips */
606
- body.dark-mode .tooltip-inner {
607
- background-color: var(--ctp-surface0);
608
- color: var(--ctp-text);
609
- }
610
-
611
- /* Checkboxes and radios */
612
- body.dark-mode .form-check-input {
613
- background-color: var(--ctp-surface1);
614
- border-color: var(--ctp-surface2);
615
- }
616
- body.dark-mode .form-check-input:checked {
617
- background-color: var(--ctp-mauve);
618
- border-color: var(--ctp-mauve);
619
- }
620
- body.dark-mode .form-check-label {
621
- color: var(--ctp-text);
622
- }
623
-
624
- /* Nav tabs */
625
- body.dark-mode .nav-tabs {
626
- border-bottom-color: var(--ctp-surface2);
627
- }
628
- body.dark-mode .nav-tabs .nav-link {
629
- color: var(--ctp-text);
630
- background-color: transparent;
631
- border-color: transparent;
632
- }
633
- body.dark-mode .nav-tabs .nav-link:hover {
634
- border-color: var(--ctp-surface2);
635
- background-color: var(--ctp-surface1);
636
- }
637
- body.dark-mode .nav-tabs .nav-link.active {
638
- background-color: var(--ctp-surface0);
639
- border-color: var(--ctp-surface2) var(--ctp-surface2) var(--ctp-surface0);
640
- color: var(--ctp-mauve);
641
- }
642
-
643
- /* Collapsible sections - AGGRESSIVE */
644
- body.dark-mode .accordion-item {
645
- background-color: var(--ctp-surface0) !important;
646
- border-color: var(--ctp-surface2) !important;
647
- }
648
- body.dark-mode .accordion-button {
649
- background-color: var(--ctp-surface1) !important;
650
- color: var(--ctp-text) !important;
651
- }
652
- body.dark-mode .accordion-button.bg-light {
653
- background-color: var(--ctp-surface1) !important;
654
- }
655
- body.dark-mode .accordion-button:not(.collapsed) {
656
- background-color: var(--ctp-surface0) !important;
657
- color: var(--ctp-mauve) !important;
658
- }
659
- body.dark-mode .accordion-button::after {
660
- filter: invert(1);
661
- }
662
- body.dark-mode .accordion-body {
663
- background-color: var(--ctp-surface0) !important;
664
- color: var(--ctp-text) !important;
665
- }
666
-
667
- /* Heatmap specific styling - AGGRESSIVE */
668
- body.dark-mode .heatmap-cell {
669
- border-color: var(--ctp-surface2) !important;
670
- color: var(--ctp-text) !important;
671
- }
672
- body.dark-mode .heatmap-hour {
673
- color: var(--ctp-sky) !important;
674
- font-weight: 600 !important;
675
- font-size: 0.75rem !important;
676
- }
677
- body.dark-mode .heatmap-count {
678
- color: var(--ctp-peach) !important;
679
- font-weight: 600 !important;
680
- }
681
- body.dark-mode .heatmap-count.text-white {
682
- color: var(--ctp-text) !important;
683
- }
684
- body.dark-mode .heatmap-count.text-dark {
685
- color: var(--ctp-peach) !important;
686
- }
687
- body.dark-mode .heatmap-grid {
688
- background-color: var(--ctp-surface0);
689
- }
690
-
691
- /* Definition lists (dl, dt, dd) - used in Request Context */
692
- body.dark-mode dl {
693
- color: var(--ctp-text);
694
- }
695
- body.dark-mode dt {
696
- color: var(--ctp-subtext1);
697
- font-weight: 600;
698
- }
699
- body.dark-mode dd {
700
- color: var(--ctp-text);
701
- background-color: var(--ctp-surface0);
702
- }
703
-
704
- /* Override any remaining white backgrounds */
705
- body.dark-mode div[style*="background-color: white"],
706
- body.dark-mode div[style*="background-color: #fff"],
707
- body.dark-mode div[style*="background-color:#fff"],
708
- body.dark-mode div[style*="background: white"],
709
- body.dark-mode div[style*="background: #fff"] {
710
- background-color: var(--ctp-surface0) !important;
711
- }
712
-
713
- /* Chart.js specific fixes for axis labels */
714
- body.dark-mode .chartjs-render-monitor {
715
- background-color: transparent !important;
716
- }
717
-
718
- /* Make sure all headings are visible */
719
- body.dark-mode h1, body.dark-mode h2, body.dark-mode h3,
720
- body.dark-mode h4, body.dark-mode h5, body.dark-mode h6 {
721
- color: var(--ctp-text);
722
- }
723
-
724
- /* Stronger text colors for better visibility */
725
- body.dark-mode .text-secondary {
726
- color: var(--ctp-subtext1) !important;
727
- }
728
-
729
- /* SVG text elements (for charts) - MORE AGGRESSIVE */
730
- body.dark-mode svg text {
731
- fill: var(--ctp-text) !important;
732
- }
733
- body.dark-mode svg .domain,
734
- body.dark-mode svg .tick line {
735
- stroke: var(--ctp-surface2) !important;
736
- }
737
- body.dark-mode svg tspan {
738
- fill: var(--ctp-text) !important;
739
- }
740
-
741
- /* Details/Summary (collapsible sections) */
742
- body.dark-mode details {
743
- background-color: var(--ctp-surface0) !important;
744
- border-color: var(--ctp-surface2) !important;
745
- }
746
- body.dark-mode summary {
747
- background-color: var(--ctp-surface0) !important;
748
- color: var(--ctp-text) !important;
749
- border-color: var(--ctp-surface2) !important;
750
- }
751
- body.dark-mode details[open] summary {
752
- background-color: var(--ctp-surface1) !important;
753
- border-bottom-color: var(--ctp-surface2) !important;
754
- }
755
-
756
- /* Button-like summary elements */
757
- body.dark-mode .btn.collapsed,
758
- body.dark-mode [data-bs-toggle="collapse"] {
759
- background-color: var(--ctp-surface0) !important;
760
- color: var(--ctp-text) !important;
761
- border-color: var(--ctp-surface2) !important;
762
- }
763
-
764
- /* Specific override for white backgrounds in backtrace sections */
765
- body.dark-mode .card .card-body > div[style*="background"],
766
- body.dark-mode .card-body > button[style*="background"] {
767
- background-color: var(--ctp-surface0) !important;
768
- }
769
-
770
- /* Error header/banner */
771
- body.dark-mode .alert-danger {
772
- background-color: rgba(243, 139, 168, 0.2) !important;
773
- border-color: var(--ctp-red) !important;
774
- color: var(--ctp-text) !important;
775
- }
776
-
777
- body.dark-mode .alert-warning {
778
- background-color: rgba(249, 226, 175, 0.15) !important;
779
- border-color: var(--ctp-yellow) !important;
780
- color: var(--ctp-text) !important;
781
- }
782
-
783
- body.dark-mode .alert-warning code {
784
- color: var(--ctp-peach) !important;
785
- }
786
-
787
- /* Chartkick specific - force text visibility */
788
- body.dark-mode #chart-1 text,
789
- body.dark-mode [id^="chart-"] text {
790
- fill: var(--ctp-text) !important;
791
- color: var(--ctp-text) !important;
792
- }
793
-
794
- /* Force all text in charts to be visible */
795
- body.dark-mode canvas + div text,
796
- body.dark-mode .chartjs-size-monitor text {
797
- color: var(--ctp-text) !important;
798
- }
799
-
800
- /* ULTRA AGGRESSIVE - Chart.js axis labels and titles */
801
- body.dark-mode canvas {
802
- color: var(--ctp-text) !important;
803
- }
804
-
805
- /* Target Google Charts (alternative library) */
806
- body.dark-mode svg > g > g > text,
807
- body.dark-mode svg g text {
808
- fill: var(--ctp-text) !important;
809
- font-size: 12px !important;
810
- }
811
-
812
- /* Chart.js specific selectors - NUCLEAR OPTION */
813
- body.dark-mode .chartjs-render-monitor + div text,
814
- body.dark-mode [class*="chart"] text,
815
- body.dark-mode div[id*="chart"] text {
816
- fill: var(--ctp-text) !important;
817
- color: var(--ctp-text) !important;
818
- }
819
-
820
- /* Ensure axis tick labels are visible */
821
- body.dark-mode g.tick text,
822
- body.dark-mode .tick text,
823
- body.dark-mode text.highcharts-axis-title,
824
- body.dark-mode .highcharts-axis-labels text {
825
- fill: var(--ctp-text) !important;
826
- color: var(--ctp-text) !important;
827
- }
828
-
829
- /* Force visibility on ALL text elements inside chart containers */
830
- body.dark-mode .card-body text,
831
- body.dark-mode .card text {
832
- fill: var(--ctp-text) !important;
833
- }
834
-
835
- /* Toast notifications */
836
- .toast {
837
- min-width: 250px;
838
- }
839
- .toast-container {
840
- max-width: 350px;
841
- }
842
-
843
- /* Filter pills */
844
- .filter-pill {
845
- display: inline-flex;
846
- align-items: center;
847
- padding: 0.5rem 0.75rem;
848
- font-size: 0.875rem;
849
- border-radius: 0.375rem;
850
- transition: all 0.2s ease;
851
- }
852
- .filter-pill:hover {
853
- opacity: 0.85;
854
- transform: translateY(-1px);
855
- }
856
- .filter-pill .bi-x {
857
- font-size: 1.1rem;
858
- font-weight: bold;
859
- }
860
- body.dark-mode .filter-pill.bg-primary {
861
- background-color: var(--ctp-mauve) !important;
862
- }
863
- body.dark-mode .filter-pill.bg-secondary {
864
- background-color: var(--ctp-surface2) !important;
865
- }
866
-
867
- /* Syntax Highlighting Styles - Polished & Professional */
868
-
869
- /* Source Code Viewer Container */
870
- .source-code-viewer {
871
- border-radius: 0.75rem;
872
- overflow: hidden;
873
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.06);
874
- border: 1px solid #e5e7eb;
875
- margin-bottom: 1.5rem;
876
- }
877
-
878
- body.dark-mode .source-code-viewer {
879
- box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4), 0 2px 6px rgba(0, 0, 0, 0.3);
880
- border-color: #45475a;
881
- }
882
-
883
- /* Header Styling */
884
- .source-code-viewer .source-code-header {
885
- background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f5 100%);
886
- font-weight: 500;
887
- border-bottom: 2px solid #e5e7eb;
888
- }
889
-
890
- body.dark-mode .source-code-viewer .source-code-header {
891
- background: linear-gradient(135deg, #313244 0%, #282a36 100%);
892
- border-bottom-color: #45475a;
893
- }
894
-
895
- /* Git Blame Info */
896
- .source-code-viewer .git-blame-info {
897
- background-color: #fafbfc;
898
- border-top: 1px solid #e9ecef;
899
- }
900
-
901
- body.dark-mode .source-code-viewer .git-blame-info {
902
- background-color: #1e1e2e;
903
- border-top-color: #45475a;
904
- }
905
-
906
- /* Code Content Area */
907
- .source-code-content {
908
- background-color: #ffffff;
909
- }
910
-
911
- body.dark-mode .source-code-content {
912
- background-color: #1e1e2e;
913
- }
914
-
915
- .source-code-content pre {
916
- margin: 0;
917
- padding: 0;
918
- background: transparent;
919
- border: none;
920
- overflow: auto;
921
- }
922
-
923
- .source-code-content pre code {
924
- display: block;
925
- padding: 0;
926
- font-size: 0.9rem;
927
- line-height: 1.8;
928
- font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
929
- color: #1f2937;
930
- background: transparent;
931
- letter-spacing: -0.01em;
932
- }
933
-
934
- body.dark-mode .source-code-content pre code {
935
- color: #cdd6f4;
936
- }
937
-
938
- /* Line Number Table */
939
- .source-code-content table.hljs-ln {
940
- width: 100%;
941
- border-collapse: collapse;
942
- margin: 0;
943
- background: transparent;
944
- }
945
-
946
- /* Individual Line Rows */
947
- .source-code-content .hljs-ln-line,
948
- .source-code-content tr {
949
- transition: all 0.2s ease;
950
- }
951
-
952
- .source-code-content .hljs-ln-line:hover,
953
- .source-code-content tr:hover {
954
- background-color: rgba(139, 92, 246, 0.04);
955
- }
956
-
957
- body.dark-mode .source-code-content .hljs-ln-line:hover,
958
- body.dark-mode .source-code-content tr:hover {
959
- background-color: rgba(203, 166, 247, 0.08);
960
- }
961
-
962
- /* Error Line Highlighting - Enhanced Visibility */
963
- .source-code-content tr.error-line,
964
- .source-code-content .hljs-ln-line.error-line {
965
- background: linear-gradient(90deg,
966
- rgba(250, 179, 135, 0.25) 0%,
967
- rgba(250, 179, 135, 0.12) 4px,
968
- rgba(250, 179, 135, 0.08) 100%) !important;
969
- border-left: 4px solid #fab387;
970
- position: relative;
971
- }
972
-
973
- /* Add pulse animation to error line */
974
- @keyframes error-pulse {
975
- 0%, 100% { border-left-color: #fab387; }
976
- 50% { border-left-color: #f9e2af; }
977
- }
978
-
979
- .source-code-content tr.error-line {
980
- animation: error-pulse 2s ease-in-out infinite;
981
- }
982
-
983
- body.dark-mode .source-code-content tr.error-line,
984
- body.dark-mode .source-code-content .hljs-ln-line.error-line {
985
- background: linear-gradient(90deg,
986
- rgba(249, 226, 175, 0.20) 0%,
987
- rgba(249, 226, 175, 0.12) 4px,
988
- rgba(249, 226, 175, 0.06) 100%) !important;
989
- border-left-color: #f9e2af;
990
- }
991
-
992
- .source-code-content tr.error-line td,
993
- .source-code-content .hljs-ln-line.error-line td {
994
- background-color: transparent;
995
- }
996
-
997
- /* Error Line Number - More Prominent */
998
- .source-code-content tr.error-line .hljs-ln-numbers {
999
- background: linear-gradient(90deg, #fab387 0%, rgba(250, 179, 135, 0.4) 100%) !important;
1000
- color: #1f2937 !important;
1001
- font-weight: 700;
1002
- text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
1003
- position: relative;
1004
- }
1005
-
1006
- .source-code-content tr.error-line .hljs-ln-numbers::before {
1007
- content: '\2192';
1008
- position: absolute;
1009
- left: 4px;
1010
- color: #dc2626;
1011
- font-weight: bold;
1012
- font-size: 1rem;
1013
- }
1014
-
1015
- body.dark-mode .source-code-content tr.error-line .hljs-ln-numbers {
1016
- background: linear-gradient(90deg, #f9e2af 0%, rgba(249, 226, 175, 0.3) 100%) !important;
1017
- color: #11111b !important;
1018
- text-shadow: none;
1019
- }
1020
-
1021
- body.dark-mode .source-code-content tr.error-line .hljs-ln-numbers::before {
1022
- color: #f38ba8;
1023
- }
1024
-
1025
- .source-code-content tr.error-line .hljs-ln-code {
1026
- background-color: transparent;
1027
- font-weight: 500;
1028
- }
1029
-
1030
- /* Line Numbers Column - Enhanced */
1031
- .source-code-content .hljs-ln-numbers {
1032
- width: 70px;
1033
- min-width: 70px;
1034
- text-align: right;
1035
- padding: 0.6rem 1rem;
1036
- color: #9ca3af;
1037
- user-select: none;
1038
- background: linear-gradient(90deg, #f9fafb 0%, #f3f4f6 100%);
1039
- border-right: 3px solid #e5e7eb;
1040
- font-weight: 500;
1041
- font-size: 0.8125rem;
1042
- vertical-align: top;
1043
- font-variant-numeric: tabular-nums;
1044
- }
1045
-
1046
- body.dark-mode .source-code-content .hljs-ln-numbers {
1047
- color: #7f849c;
1048
- background: linear-gradient(90deg, #313244 0%, #282a36 100%);
1049
- border-right-color: #45475a;
1050
- }
1051
-
1052
- /* Code Content Column */
1053
- .source-code-content .hljs-ln-code {
1054
- padding: 0.6rem 1.25rem;
1055
- white-space: pre;
1056
- vertical-align: top;
1057
- }
1058
-
1059
- /* Table Cells */
1060
- .source-code-content td.hljs-ln-numbers,
1061
- .source-code-content td.hljs-ln-code {
1062
- border: none;
1063
- }
1064
-
1065
- /* Highlighted Code Block */
1066
- .source-code-content code.hljs {
1067
- background: transparent;
1068
- padding: 0;
1069
- }
1070
-
1071
- /* Scrollbar Styling for Code Block */
1072
- .source-code-content .code-block {
1073
- scrollbar-width: thin;
1074
- scrollbar-color: #cbd5e1 #f1f5f9;
1075
- }
1076
-
1077
- .source-code-content .code-block::-webkit-scrollbar {
1078
- width: 10px;
1079
- height: 10px;
1080
- }
1081
-
1082
- .source-code-content .code-block::-webkit-scrollbar-track {
1083
- background: #f1f5f9;
1084
- border-radius: 0 0 0.75rem 0;
1085
- }
1086
-
1087
- .source-code-content .code-block::-webkit-scrollbar-thumb {
1088
- background: #cbd5e1;
1089
- border-radius: 5px;
1090
- border: 2px solid #f1f5f9;
1091
- }
1092
-
1093
- .source-code-content .code-block::-webkit-scrollbar-thumb:hover {
1094
- background: #94a3b8;
1095
- }
1096
-
1097
- body.dark-mode .source-code-content .code-block {
1098
- scrollbar-color: #45475a #1e1e2e;
1099
- }
1100
-
1101
- body.dark-mode .source-code-content .code-block::-webkit-scrollbar-track {
1102
- background: #1e1e2e;
1103
- }
1104
-
1105
- body.dark-mode .source-code-content .code-block::-webkit-scrollbar-thumb {
1106
- background: #45475a;
1107
- border-color: #1e1e2e;
1108
- }
1109
-
1110
- body.dark-mode .source-code-content .code-block::-webkit-scrollbar-thumb:hover {
1111
- background: #585b70;
1112
- }
1113
-
1114
- /* Syntax Highlighting Token Colors - Light Mode Override */
1115
- .source-code-content .hljs-keyword,
1116
- .source-code-content .hljs-selector-tag,
1117
- .source-code-content .hljs-literal,
1118
- .source-code-content .hljs-section,
1119
- .source-code-content .hljs-link {
1120
- color: #7c3aed !important;
1121
- }
1122
-
1123
- .source-code-content .hljs-string,
1124
- .source-code-content .hljs-attr {
1125
- color: #059669 !important;
1126
- }
1127
-
1128
- .source-code-content .hljs-name,
1129
- .source-code-content .hljs-type,
1130
- .source-code-content .hljs-title {
1131
- color: #dc2626 !important;
1132
- }
1133
-
1134
- .source-code-content .hljs-comment,
1135
- .source-code-content .hljs-quote {
1136
- color: #6b7280 !important;
1137
- font-style: italic;
1138
- }
1139
-
1140
- .source-code-content .hljs-number,
1141
- .source-code-content .hljs-symbol {
1142
- color: #ea580c !important;
1143
- }
1144
-
1145
- .source-code-content .hljs-built_in,
1146
- .source-code-content .hljs-builtin-name {
1147
- color: #0891b2 !important;
1148
- }
1149
-
1150
- /* Dark mode uses Catppuccin Mocha (from CDN) - no override needed */
1151
-
1152
- /* Source code file path styling */
1153
- .source-file-path {
1154
- color: #212529 !important; /* Dark text for light mode */
1155
- }
1156
-
1157
- [data-theme="dark"] .source-file-path,
1158
- body.dark-mode .source-file-path {
1159
- color: var(--ctp-text) !important; /* Bright text for dark mode */
1160
- }
1161
-
1162
- /* Backtrace frame number styling */
1163
- .backtrace-frame-number {
1164
- display: inline-block;
1165
- min-width: 2em;
1166
- text-align: right;
1167
- margin-right: 0.5em;
1168
- color: #9ca3af;
1169
- font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Code', monospace;
1170
- font-size: 0.85em;
1171
- user-select: none;
1172
- }
1173
-
1174
- body.dark-mode .backtrace-frame-number {
1175
- color: var(--ctp-overlay1);
1176
- }
1177
-
1178
- /* Backtrace method name styling - override Bootstrap text-info */
1179
- span.backtrace-method-name {
1180
- color: #0066cc !important; /* Darker blue for better readability in light mode */
1181
- }
1182
-
1183
- [data-theme="dark"] span.backtrace-method-name,
1184
- body.dark-mode span.backtrace-method-name,
1185
- html[data-theme="dark"] span.backtrace-method-name {
1186
- color: var(--ctp-sky) !important; /* Catppuccin sky for dark mode */
1187
- }
1188
-
1189
- /* Timeline styles */
1190
- .timeline {
1191
- position: relative;
1192
- padding-left: 0;
1193
- }
1194
-
1195
- .timeline-item {
1196
- position: relative;
1197
- padding-left: 40px;
1198
- padding-bottom: 30px;
1199
- }
1200
-
1201
- .timeline-item-last {
1202
- padding-bottom: 0;
1203
- }
41
+ <!-- Early theme detection (prevents flash of wrong theme) -->
42
+ <script>
43
+ (function(){
44
+ var t = localStorage.getItem('red-theme');
45
+ if (t === 'dark' || (!t && window.matchMedia && matchMedia('(prefers-color-scheme: dark)').matches)) {
46
+ document.documentElement.setAttribute('data-theme', 'dark');
47
+ }
48
+ })();
49
+ </script>
1204
50
 
1205
- .timeline-item::before {
1206
- content: '';
1207
- position: absolute;
1208
- left: 11px;
1209
- top: 30px;
1210
- bottom: 0;
1211
- width: 2px;
1212
- background: #dee2e6;
1213
- }
51
+ <!-- Fonts -->
52
+ <link rel="preconnect" href="https://fonts.googleapis.com">
53
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
54
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
55
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
1214
56
 
1215
- [data-theme="dark"] .timeline-item::before,
1216
- body.dark-mode .timeline-item::before {
1217
- background: var(--border-color);
1218
- }
57
+ <!-- Bootstrap Icons -->
58
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
1219
59
 
1220
- .timeline-item-last::before {
1221
- display: none;
1222
- }
60
+ <!-- Chart.js -->
61
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
62
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
63
+ <script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script>
1223
64
 
1224
- .timeline-marker {
1225
- position: absolute;
1226
- left: 0;
1227
- top: 0;
1228
- width: 24px;
1229
- height: 24px;
1230
- display: flex;
1231
- align-items: center;
1232
- justify-content: center;
1233
- background: #ffffff;
1234
- border: 2px solid #dee2e6;
1235
- border-radius: 50%;
1236
- z-index: 1;
1237
- }
65
+ <!-- Syntax Highlighting for Source Code Viewer -->
66
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@catppuccin/highlightjs@1.0.0/css/catppuccin-mocha.min.css">
67
+ <script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
68
+ <script src="https://cdn.jsdelivr.net/npm/highlightjs-line-numbers.js@2.8.0/dist/highlightjs-line-numbers.min.js"></script>
1238
69
 
1239
- [data-theme="dark"] .timeline-marker,
1240
- body.dark-mode .timeline-marker {
1241
- background: var(--ctp-surface0);
1242
- border-color: var(--border-color);
1243
- }
70
+ <style>
71
+ /* ============================================
72
+ RED Design Tokens
73
+ ============================================ */
74
+ :root {
75
+ /* Surfaces — Catppuccin Latte inspired */
76
+ --surface-base: #f2f3f7;
77
+ --surface-primary: #ffffff;
78
+ --surface-secondary: #e8eaef;
79
+ --surface-tertiary: #dcdee5;
80
+ --surface-hover: #f6f6f9;
81
+
82
+ /* Text */
83
+ --text-primary: #1a1a2e;
84
+ --text-secondary: #5c5f73;
85
+ --text-tertiary: #8b8fa3;
86
+
87
+ /* Borders */
88
+ --border-primary: #e2e4ea;
89
+ --border-secondary: #ecedf2;
90
+
91
+ /* Accent — set from configuration */
92
+ --accent: <%= ac[:light] %>;
93
+ --accent-hover: color-mix(in oklch, var(--accent) 85%, black);
94
+ --accent-subtle: <%= ac[:subtle_light] %>;
95
+
96
+ /* Status */
97
+ --status-critical: #e64553;
98
+ --status-critical-bg: rgba(230,69,83,0.08);
99
+ --status-warning: #df8e1d;
100
+ --status-warning-bg: rgba(223,142,29,0.08);
101
+ --status-caution: #e6a320;
102
+ --status-caution-bg: rgba(230,163,32,0.08);
103
+ --status-success: #40a02b;
104
+ --status-success-bg: rgba(64,160,43,0.08);
105
+ --status-info: #1e66f5;
106
+ --status-info-bg: rgba(30,102,245,0.08);
107
+
108
+ /* Spacing (4px base) */
109
+ --space-1: 0.25rem; --space-2: 0.5rem; --space-3: 0.75rem;
110
+ --space-4: 1rem; --space-5: 1.25rem; --space-6: 1.5rem;
111
+ --space-8: 2rem; --space-10: 2.5rem; --space-12: 3rem;
112
+
113
+ /* Typography */
114
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
115
+ --font-mono: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
116
+
117
+ /* Elevation */
118
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.03);
119
+ --shadow-md: 0 2px 8px rgba(0,0,0,0.05);
120
+ --shadow-lg: 0 8px 24px rgba(0,0,0,0.07);
121
+
122
+ /* Radius */
123
+ --radius-sm: 6px; --radius-md: 8px;
124
+ --radius-lg: 12px; --radius-full: 9999px;
125
+
126
+ /* Transitions */
127
+ --transition-fast: 100ms ease;
128
+ --transition-normal: 200ms ease;
129
+ }
130
+
131
+ /* --- Dark theme (Catppuccin Mocha) --- */
132
+ [data-theme="dark"] {
133
+ --surface-base: #1e1e2e;
134
+ --surface-primary: #24243a;
135
+ --surface-secondary: #313244;
136
+ --surface-tertiary: #45475a;
137
+ --surface-hover: #313244;
138
+ --text-primary: #ffffff;
139
+ --text-secondary: #cdd6f4;
140
+ --text-tertiary: #a6adc8;
141
+ --border-primary: #313244;
142
+ --border-secondary: #45475a;
143
+ --accent: <%= ac[:dark] %>;
144
+ --accent-subtle: <%= ac[:subtle_dark] %>;
145
+ --status-critical: #f38ba8;
146
+ --status-critical-bg: rgba(243,139,168,0.12);
147
+ --status-warning: #fab387;
148
+ --status-warning-bg: rgba(250,179,135,0.12);
149
+ --status-caution: #f9e2af;
150
+ --status-caution-bg: rgba(249,226,175,0.12);
151
+ --status-success: #a6e3a1;
152
+ --status-success-bg: rgba(166,227,161,0.12);
153
+ --status-info: #89b4fa;
154
+ --status-info-bg: rgba(137,180,250,0.12);
155
+ --shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
156
+ --shadow-md: 0 2px 8px rgba(0,0,0,0.25);
157
+ --shadow-lg: 0 8px 24px rgba(0,0,0,0.35);
158
+ }
159
+
160
+ /* ============================================
161
+ Reset + Base
162
+ ============================================ */
163
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
164
+
165
+ html, body {
166
+ font-family: var(--font-sans);
167
+ font-size: 14px;
168
+ line-height: 1.5;
169
+ color: var(--text-primary);
170
+ background: var(--surface-base);
171
+ -webkit-font-smoothing: antialiased;
172
+ -moz-osx-font-smoothing: grayscale;
173
+ }
174
+
175
+ /* Scrollbar */
176
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
177
+ ::-webkit-scrollbar-track { background: transparent; }
178
+ ::-webkit-scrollbar-thumb { background: var(--border-primary); border-radius: 3px; }
179
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-tertiary); }
180
+
181
+ /* Selection */
182
+ ::selection { background: var(--accent-subtle); color: var(--accent); }
1244
183
 
1245
- .timeline-marker-current {
1246
- border-color: #dc3545;
1247
- background: #f8d7da;
1248
- box-shadow: 0 0 0 4px rgba(220, 53, 69, 0.15);
1249
- }
184
+ /* Links */
185
+ a { color: var(--accent); text-decoration: none; }
186
+ a:hover { color: var(--accent-hover); text-decoration: underline; }
187
+
188
+ /* Code */
189
+ code, pre { font-family: var(--font-mono); }
190
+ code { font-size: 0.9em; padding: 2px 6px; background: var(--surface-secondary); border-radius: var(--radius-sm); color: var(--accent); }
191
+ pre { background: var(--surface-secondary); border-radius: var(--radius-md); padding: var(--space-4); overflow-x: auto; }
192
+ pre code { padding: 0; background: transparent; }
193
+
194
+ /* Headings */
195
+ h1, h2, h3, h4, h5, h6 { color: var(--text-primary); }
196
+
197
+ /* Horizontal rule */
198
+ hr { border: none; border-top: 1px solid var(--border-primary); margin: var(--space-4) 0; }
199
+
200
+ /* Small text */
201
+ small { color: var(--text-secondary); }
202
+
203
+ /* Labels */
204
+ label { color: var(--text-primary); font-weight: 500; font-size: 13px; }
205
+
206
+ /* Definition lists */
207
+ dt { color: var(--text-secondary); font-weight: 600; font-size: 12px; }
208
+ dd { color: var(--text-primary); }
209
+
210
+ /* Pulse animation */
211
+ @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
212
+ @keyframes error-pulse { 0%, 100% { border-left-color: #fab387; } 50% { border-left-color: #f9e2af; } }
213
+ @keyframes shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
214
+ @keyframes spinner-border { to { transform: rotate(360deg); } }
215
+
216
+ /* ============================================
217
+ Bootstrap Compatibility Layer
218
+ ============================================ */
219
+
220
+ /* Grid */
221
+ .container-fluid { width: 100%; padding-right: var(--space-4); padding-left: var(--space-4); }
222
+ .row { display: flex; flex-wrap: wrap; margin-right: calc(var(--space-3) * -1); margin-left: calc(var(--space-3) * -1); }
223
+ .row > * { flex-shrink: 0; width: 100%; padding-right: var(--space-3); padding-left: var(--space-3); }
224
+ .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-12 { width: 100%; }
225
+ @media (min-width: 768px) {
226
+ .col-md-2 { flex: 0 0 auto; width: 16.666667%; }
227
+ .col-md-3 { flex: 0 0 auto; width: 25%; }
228
+ .col-md-4 { flex: 0 0 auto; width: 33.333333%; }
229
+ .col-md-5 { flex: 0 0 auto; width: 41.666667%; }
230
+ .col-md-6 { flex: 0 0 auto; width: 50%; }
231
+ .col-md-7 { flex: 0 0 auto; width: 58.333333%; }
232
+ .col-md-8 { flex: 0 0 auto; width: 66.666667%; }
233
+ .col-md-9 { flex: 0 0 auto; width: 75%; }
234
+ .col-md-10 { flex: 0 0 auto; width: 83.333333%; }
235
+ .col-md-12 { flex: 0 0 auto; width: 100%; }
236
+ }
237
+ .col-lg-3, .col-lg-4, .col-lg-6 { width: 100%; }
238
+ @media (min-width: 992px) {
239
+ .col-lg-3 { flex: 0 0 auto; width: 25%; }
240
+ .col-lg-4 { flex: 0 0 auto; width: 33.333333%; }
241
+ .col-lg-6 { flex: 0 0 auto; width: 50%; }
242
+ }
243
+ .col-12 { flex: 0 0 auto; width: 100%; }
244
+ .g-3 { --bs-gutter-x: var(--space-4); --bs-gutter-y: var(--space-4); }
245
+ .g-3 > * { padding-right: calc(var(--bs-gutter-x) * .5); padding-left: calc(var(--bs-gutter-x) * .5); margin-top: var(--bs-gutter-y); }
246
+ .g-4 { --bs-gutter-x: var(--space-6); --bs-gutter-y: var(--space-6); }
247
+ .g-4 > * { padding-right: calc(var(--bs-gutter-x) * .5); padding-left: calc(var(--bs-gutter-x) * .5); margin-top: var(--bs-gutter-y); }
248
+
249
+ /* Flexbox */
250
+ .d-flex { display: flex; }
251
+ .d-inline-flex { display: inline-flex; }
252
+ .flex-column { flex-direction: column; }
253
+ .flex-wrap { flex-wrap: wrap; }
254
+ .flex-grow-1 { flex-grow: 1; }
255
+ .flex-shrink-0 { flex-shrink: 0; }
256
+ .justify-content-between { justify-content: space-between; }
257
+ .justify-content-center { justify-content: center; }
258
+ .justify-content-end { justify-content: flex-end; }
259
+ .align-items-center { align-items: center; }
260
+ .align-items-start { align-items: flex-start; }
261
+ .align-items-end { align-items: flex-end; }
262
+ .gap-1 { gap: var(--space-1); }
263
+ .gap-2 { gap: var(--space-2); }
264
+ .gap-3 { gap: var(--space-3); }
265
+ .gap-4 { gap: var(--space-4); }
266
+
267
+ /* Spacing */
268
+ .m-0 { margin: 0; } .m-auto { margin: auto; }
269
+ .mb-0 { margin-bottom: 0; } .mb-1 { margin-bottom: var(--space-1); } .mb-2 { margin-bottom: var(--space-2); } .mb-3 { margin-bottom: var(--space-4); } .mb-4 { margin-bottom: var(--space-6); } .mb-5 { margin-bottom: var(--space-12); }
270
+ .mt-0 { margin-top: 0; } .mt-1 { margin-top: var(--space-1); } .mt-2 { margin-top: var(--space-2); } .mt-3 { margin-top: var(--space-4); } .mt-4 { margin-top: var(--space-6); } .mt-5 { margin-top: var(--space-12); }
271
+ .ms-0 { margin-left: 0; } .ms-1 { margin-left: var(--space-1); } .ms-2 { margin-left: var(--space-2); } .ms-3 { margin-left: var(--space-4); } .ms-auto { margin-left: auto; }
272
+ .me-0 { margin-right: 0; } .me-1 { margin-right: var(--space-1); } .me-2 { margin-right: var(--space-2); } .me-3 { margin-right: var(--space-4); } .me-auto { margin-right: auto; }
273
+ .mx-2 { margin-left: var(--space-2); margin-right: var(--space-2); }
274
+ .p-0 { padding: 0; } .p-2 { padding: var(--space-2); } .p-3 { padding: var(--space-4); } .p-4 { padding: var(--space-6); }
275
+ .py-1 { padding-top: var(--space-1); padding-bottom: var(--space-1); } .py-2 { padding-top: var(--space-2); padding-bottom: var(--space-2); } .py-3 { padding-top: var(--space-4); padding-bottom: var(--space-4); } .py-4 { padding-top: var(--space-6); padding-bottom: var(--space-6); }
276
+ .px-2 { padding-left: var(--space-2); padding-right: var(--space-2); } .px-3 { padding-left: var(--space-4); padding-right: var(--space-4); } .px-4 { padding-left: var(--space-6); padding-right: var(--space-6); }
277
+ .pt-3 { padding-top: var(--space-4); }
278
+ .pb-3 { padding-bottom: var(--space-4); }
279
+ .ps-3 { padding-left: var(--space-4); }
280
+ .pe-2 { padding-right: var(--space-2); }
281
+
282
+ /* Display (use !important to match Bootstrap utility behavior) */
283
+ .d-none { display: none !important; }
284
+ .d-block { display: block !important; }
285
+ .d-inline { display: inline !important; }
286
+ .d-inline-block { display: inline-block !important; }
287
+ @media (min-width: 576px) { .d-sm-inline { display: inline !important; } .d-sm-block { display: block !important; } }
288
+ @media (min-width: 768px) { .d-md-block { display: block !important; } .d-md-none { display: none !important; } .d-md-flex { display: flex !important; } .d-md-inline { display: inline !important; } .ms-sm-auto { margin-left: auto; } .px-md-4 { padding-left: var(--space-6); padding-right: var(--space-6); } }
289
+
290
+ /* Text */
291
+ .text-muted { color: var(--text-secondary); }
292
+ .text-secondary { color: var(--text-secondary); }
293
+
294
+ /* Bootstrap compat: table-light thead */
295
+ .table-light, thead.table-light { background: var(--surface-secondary); }
296
+ .table-light th { color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
297
+ .text-center { text-align: center; }
298
+ .text-end { text-align: right; }
299
+ .text-start { text-align: left; }
300
+ .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
301
+ .text-nowrap { white-space: nowrap; }
302
+ .text-decoration-none { text-decoration: none; }
303
+ .text-white { color: #fff; }
304
+ .text-white-50 { color: rgba(255,255,255,0.5); }
305
+ .text-danger { color: var(--status-critical); }
306
+ .text-success { color: var(--status-success); }
307
+ .text-warning { color: var(--status-warning); }
308
+ .text-info { color: var(--status-info); }
309
+ .text-primary { color: var(--accent); }
310
+ .fw-bold { font-weight: 700; }
311
+ .fw-semibold { font-weight: 600; }
312
+ .fs-4 { font-size: 1.25rem; }
313
+ .fs-5 { font-size: 1.1rem; }
314
+ .small, small { font-size: 0.85em; }
315
+ .font-monospace { font-family: var(--font-mono); }
316
+ .text-uppercase { text-transform: uppercase; }
317
+
318
+ /* Backgrounds */
319
+ .bg-white { background: var(--surface-primary); }
320
+ .bg-light { background: var(--surface-secondary); }
321
+ .bg-danger { background: var(--status-critical); color: #fff; }
322
+ .bg-success { background: var(--status-success); color: #fff; }
323
+ .bg-warning { background: var(--status-warning); color: #fff; }
324
+ .bg-info { background: var(--status-info); color: #fff; }
325
+ .bg-secondary { background: var(--text-tertiary); color: #fff; }
326
+ .bg-primary { background: var(--accent); color: #fff; }
327
+ .bg-transparent { background: transparent; }
328
+
329
+ /* Borders & misc */
330
+ .border { border: 1px solid var(--border-primary); }
331
+ .border-top { border-top: 1px solid var(--border-primary); }
332
+ .border-bottom { border-bottom: 1px solid var(--border-primary); }
333
+ .border-start { border-left: 1px solid var(--border-primary); }
334
+ .rounded { border-radius: var(--radius-md); }
335
+ .rounded-circle { border-radius: 50%; }
336
+ .shadow-sm { box-shadow: var(--shadow-sm); }
337
+ .h-100 { height: 100%; }
338
+ .w-100 { width: 100%; }
339
+ .overflow-auto { overflow: auto; }
340
+ .overflow-hidden { overflow: hidden; }
341
+ .position-fixed { position: fixed; }
342
+ .position-sticky { position: sticky; }
343
+ .position-relative { position: relative; }
344
+ .position-absolute { position: absolute; }
345
+ .top-0 { top: 0; }
346
+ .end-0 { right: 0; }
347
+ .img-fluid { max-width: 100%; height: auto; }
348
+ .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
349
+
350
+ /* ============================================
351
+ Components
352
+ ============================================ */
353
+
354
+ /* Card */
355
+ .card { background: var(--surface-primary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); transition: box-shadow var(--transition-normal); }
356
+ .card-body { padding: var(--space-5); }
357
+ .card-header { padding: var(--space-4) var(--space-5); border-bottom: 1px solid var(--border-primary); background: var(--surface-hover); font-weight: 600; font-size: 13px; }
358
+ .card-footer { padding: var(--space-3) var(--space-5); border-top: 1px solid var(--border-primary); }
359
+ .card-title { font-size: 15px; font-weight: 600; margin-bottom: var(--space-2); }
360
+
361
+ /* Stat Card */
362
+ .stat-card { border-radius: var(--radius-md); transition: transform var(--transition-normal), box-shadow var(--transition-normal); }
363
+ .stat-card:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
364
+ .stat-label { font-size: 12px; font-weight: 500; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.05em; }
365
+ .stat-value { font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text-primary); }
366
+
367
+ /* Badge */
368
+ .badge { display: inline-flex; align-items: center; gap: 4px; padding: 3px 10px; font-size: 11px; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; border-radius: var(--radius-full); }
369
+ .badge.bg-danger { background: var(--status-critical-bg); color: var(--status-critical); }
370
+ .badge.bg-warning { background: var(--status-warning-bg); color: var(--status-warning); }
371
+ .badge.bg-info { background: var(--status-info-bg); color: var(--status-info); }
372
+ .badge.bg-success { background: var(--status-success-bg); color: var(--status-success); }
373
+ .badge.bg-secondary { background: var(--surface-tertiary); color: var(--text-secondary); }
374
+ .badge.bg-primary { background: var(--accent-subtle); color: var(--accent); }
375
+
376
+ /* Platform badges */
377
+ .badge-ios { background: var(--surface-tertiary); color: var(--text-primary); }
378
+ .badge-android { background: var(--status-success-bg); color: var(--status-success); }
379
+ .badge-web { background: var(--status-info-bg); color: var(--status-info); }
380
+ .badge-api { background: var(--accent-subtle); color: var(--accent); }
1250
381
 
1251
- [data-theme="dark"] .timeline-marker-current,
1252
- body.dark-mode .timeline-marker-current {
1253
- border-color: var(--ctp-red);
1254
- background: rgba(243, 139, 168, 0.2);
1255
- box-shadow: 0 0 0 4px rgba(243, 139, 168, 0.1);
1256
- }
382
+ /* Buttons */
383
+ .btn { display: inline-flex; align-items: center; gap: 4px; padding: 7px 14px; font-size: 13px; font-weight: 500; border-radius: var(--radius-sm); border: 1px solid var(--border-primary); background: var(--surface-primary); color: var(--text-secondary); cursor: pointer; transition: all var(--transition-fast); text-decoration: none; line-height: 1.4; }
384
+ .btn:hover { background: var(--surface-hover); color: var(--text-primary); text-decoration: none; }
385
+ .btn:disabled, .btn.disabled { opacity: 0.5; pointer-events: none; }
386
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
387
+ .btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
388
+ .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; }
389
+ .btn-danger { background: var(--status-critical); border-color: var(--status-critical); color: #fff; }
390
+ .btn-danger:hover { opacity: 0.9; color: #fff; }
391
+ .btn-success { background: var(--status-success); border-color: var(--status-success); color: #fff; }
392
+ .btn-success:hover { opacity: 0.9; color: #fff; }
393
+ .btn-secondary { background: var(--surface-secondary); border-color: var(--border-primary); color: var(--text-secondary); }
394
+ .btn-secondary:hover { background: var(--surface-tertiary); }
395
+ .btn-outline-primary { background: transparent; border-color: var(--accent); color: var(--accent); }
396
+ .btn-outline-primary:hover { background: var(--accent); color: #fff; }
397
+ .btn-outline-secondary { background: transparent; border-color: var(--border-primary); color: var(--text-secondary); }
398
+ .btn-outline-secondary:hover { background: var(--surface-hover); }
399
+ .btn-outline-danger { background: transparent; border-color: var(--status-critical); color: var(--status-critical); }
400
+ .btn-outline-danger:hover { background: var(--status-critical); color: #fff; }
401
+ .btn-link { background: none; border: none; color: var(--accent); padding: 0; }
402
+ .btn-link:hover { color: var(--accent-hover); }
403
+ .btn-close { display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; background: none; border: none; font-size: 18px; color: var(--text-tertiary); cursor: pointer; border-radius: var(--radius-sm); }
404
+ .btn-close:hover { background: var(--surface-secondary); color: var(--text-primary); }
405
+ .btn-close::before { content: '\00d7'; }
406
+ .btn-close-white { color: rgba(255,255,255,0.7); }
407
+ .btn-close-white:hover { color: #fff; }
1257
408
 
1258
- .timeline-marker i {
1259
- font-size: 12px;
409
+ /* Alerts */
410
+ .alert { padding: var(--space-4); border-radius: var(--radius-md); border: 1px solid var(--border-primary); margin-bottom: var(--space-4); font-size: 13px; }
411
+ .alert-danger { background: var(--status-critical-bg); border-color: var(--status-critical); color: var(--status-critical); }
412
+ .alert-warning { background: var(--status-warning-bg); border-color: var(--status-warning); color: var(--status-warning); }
413
+ .alert-success { background: var(--status-success-bg); border-color: var(--status-success); color: var(--status-success); }
414
+ .alert-info { background: var(--status-info-bg); border-color: var(--status-info); color: var(--status-info); }
415
+ .alert-secondary { background: var(--surface-secondary); border-color: var(--border-primary); color: var(--text-secondary); }
416
+ .alert code { color: inherit; background: rgba(0,0,0,0.06); }
417
+
418
+ /* Tables — polished */
419
+ .table { width: 100%; border-collapse: collapse; font-size: 13px; color: var(--text-primary); }
420
+ .table th {
421
+ padding: var(--space-3) var(--space-4); text-align: left;
422
+ font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
423
+ color: var(--text-tertiary); border-bottom: 2px solid var(--border-primary);
424
+ background: var(--surface-primary); white-space: nowrap;
425
+ }
426
+ .table td {
427
+ padding: var(--space-3) var(--space-4);
428
+ border-bottom: 1px solid var(--border-secondary);
429
+ vertical-align: middle;
1260
430
  }
431
+ .table tbody tr { transition: all 0.12s ease; position: relative; }
432
+ .table tbody tr:last-child td { border-bottom: none; }
433
+ .table-hover tbody tr { cursor: pointer; }
434
+ .table-hover tbody tr:hover { background: var(--surface-hover); }
435
+ .table-hover tbody tr:hover td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
436
+ .table-hover tbody tr:active { background: var(--accent-subtle); }
437
+ .table-striped tbody tr:nth-child(even) { background: var(--surface-base); }
438
+ .table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; }
439
+ .table-responsive thead th { position: sticky; top: 0; z-index: 10; background: var(--surface-primary); }
440
+ .table-responsive::-webkit-scrollbar { height: 4px; }
441
+ .table-responsive::-webkit-scrollbar-thumb { background: var(--border-primary); border-radius: 2px; }
1261
442
 
1262
- .timeline-content {
1263
- background: #ffffff;
1264
- padding: 12px;
1265
- border-radius: 8px;
1266
- border: 1px solid #dee2e6;
1267
- }
443
+ /* Forms */
444
+ .form-control, .form-select { width: 100%; padding: 6px 12px; font-size: 13px; border: 1px solid var(--border-primary); border-radius: var(--radius-sm); background: var(--surface-base); color: var(--text-primary); outline: none; font-family: var(--font-sans); }
445
+ .form-control:focus, .form-select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-subtle); }
446
+ .form-control::placeholder { color: var(--text-tertiary); }
447
+ .form-select { appearance: none; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='rgba(0,0,0,.4)' d='M0 0h10L5 6z'/></svg>"); background-repeat: no-repeat; background-position: right 10px center; padding-right: 28px; }
448
+ .form-check { display: flex; align-items: center; gap: 6px; }
449
+ .form-check-input { width: 16px; height: 16px; accent-color: var(--accent); }
450
+ .form-check-label { font-size: 13px; }
451
+ .form-label { font-size: 13px; font-weight: 500; margin-bottom: var(--space-1); display: block; }
452
+ .input-group { display: flex; }
453
+ .input-group .form-control { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
454
+ .input-group .btn { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
455
+
456
+ /* Modal — polished */
457
+ @keyframes modalSlideIn { from { opacity: 0; transform: translateY(-12px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
458
+ .modal { position: fixed; top: 0; left: 0; z-index: 1050; display: none; width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; outline: 0; }
459
+ .modal.show { display: block; }
460
+ .modal-dialog { position: relative; width: auto; margin: 1.75rem auto; max-width: 500px; pointer-events: none; animation: modalSlideIn 0.2s ease-out; }
461
+ .modal-dialog-centered { display: flex; align-items: center; min-height: calc(100% - 3.5rem); }
462
+ .modal-content {
463
+ position: relative; display: flex; flex-direction: column; width: 100%;
464
+ pointer-events: auto; background: var(--surface-primary);
465
+ border: 1px solid var(--border-primary); border-radius: var(--radius-lg);
466
+ box-shadow: 0 16px 48px rgba(0,0,0,0.12), 0 4px 12px rgba(0,0,0,0.08);
467
+ }
468
+ [data-theme="dark"] .modal-content { box-shadow: 0 16px 48px rgba(0,0,0,0.4), 0 4px 12px rgba(0,0,0,0.3); }
469
+ .modal-header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-6) var(--space-6) var(--space-4); border-bottom: none; }
470
+ .modal-title { font-size: 17px; font-weight: 700; color: var(--text-primary); }
471
+ .modal-body { padding: 0 var(--space-6) var(--space-5); }
472
+ .modal-body .form-control, .modal-body .form-select, .modal-body textarea {
473
+ border-radius: var(--radius-md); padding: 10px 14px; font-size: 14px;
474
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
475
+ }
476
+ .modal-body .form-control:focus, .modal-body textarea:focus {
477
+ border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-subtle);
478
+ }
479
+ .modal-body label, .modal-body .form-label { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: var(--space-2); }
480
+ .modal-body small, .modal-body .form-text { font-size: 12px; color: var(--text-tertiary); margin-top: var(--space-1); }
481
+ .modal-footer {
482
+ display: flex; justify-content: flex-end; gap: var(--space-3);
483
+ padding: var(--space-4) var(--space-6) var(--space-6); border-top: none;
484
+ }
485
+ .modal-footer .btn { min-width: 100px; justify-content: center; padding: 9px 20px; font-size: 14px; font-weight: 600; border-radius: var(--radius-md); }
486
+ .modal-footer .btn-primary, .modal-footer .btn-success { box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
487
+ .modal-backdrop { position: fixed; top: 0; left: 0; z-index: 1040; width: 100vw; height: 100vh; background: rgba(0,0,0,0.4); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); }
488
+ .modal-backdrop.show { opacity: 1; }
489
+ .fade { transition: opacity 0.15s linear; opacity: 0; }
490
+ .fade.show { opacity: 1; }
491
+
492
+ /* Accordion (Bootstrap JS support) */
493
+ .accordion { border-radius: var(--radius-md); overflow: hidden; }
494
+ .accordion-item { background: var(--surface-primary); border: 1px solid var(--border-primary); }
495
+ .accordion-item + .accordion-item { border-top: 0; }
496
+ .accordion-button { display: flex; align-items: center; width: 100%; padding: var(--space-4) var(--space-5); font-size: 14px; font-weight: 600; color: var(--text-primary); background: var(--surface-hover); border: none; cursor: pointer; transition: background var(--transition-fast); }
497
+ .accordion-button:hover { background: var(--surface-secondary); }
498
+ .accordion-button::after { content: '\25BE'; margin-left: auto; transition: transform var(--transition-normal); }
499
+ .accordion-button.collapsed::after { transform: rotate(-90deg); }
500
+ .accordion-body { padding: var(--space-5); background: var(--surface-primary); }
501
+ .accordion-collapse { display: none; }
502
+ .accordion-collapse.show { display: block; }
503
+ .collapse:not(.show) { display: none; }
504
+
505
+ /* Dropdown (Bootstrap JS support) */
506
+ .dropdown { position: relative; }
507
+ .dropdown-toggle::after { content: '\25BE'; margin-left: 4px; font-size: 10px; }
508
+ .dropdown-menu { position: absolute; z-index: 1000; display: none; min-width: 180px; padding: var(--space-2) 0; background: var(--surface-primary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); }
509
+ .dropdown-menu.show { display: block; }
510
+ .dropdown-menu-end { right: 0; left: auto; }
511
+ .dropdown-item { display: block; width: 100%; padding: var(--space-2) var(--space-4); font-size: 13px; color: var(--text-secondary); background: none; border: none; cursor: pointer; text-align: left; text-decoration: none; }
512
+ .dropdown-item:hover, .dropdown-item:focus { background: var(--surface-hover); color: var(--text-primary); }
513
+ .dropdown-item.active { background: var(--accent-subtle); color: var(--accent); }
514
+ .dropdown-divider { margin: var(--space-2) 0; border-top: 1px solid var(--border-primary); }
515
+
516
+ /* Offcanvas (Bootstrap JS support — mobile sidebar) */
517
+ .offcanvas { position: fixed; top: 0; bottom: 0; z-index: 1045; display: flex; flex-direction: column; max-width: 280px; background: var(--surface-primary); transition: transform 0.3s ease-in-out; }
518
+ .offcanvas-start { left: 0; transform: translateX(-100%); }
519
+ .offcanvas.show { transform: translateX(0); }
520
+ .offcanvas-header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-5); border-bottom: 1px solid var(--border-primary); }
521
+ .offcanvas-body { flex-grow: 1; padding: var(--space-4); overflow-y: auto; }
522
+ .offcanvas-backdrop { position: fixed; top: 0; left: 0; z-index: 1040; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); }
523
+
524
+ /* Nav */
525
+ .nav { display: flex; flex-direction: column; list-style: none; padding: 0; margin: 0; }
526
+ .nav-item { }
527
+ .nav-link { display: flex; align-items: center; gap: 10px; padding: var(--space-2) var(--space-5); font-size: 13px; font-weight: 400; color: var(--text-secondary); text-decoration: none; border-right: 2px solid transparent; transition: all var(--transition-fast); cursor: pointer; }
528
+ .nav-link i { font-size: 15px; width: 20px; text-align: center; }
529
+ .nav-link:hover { background: var(--surface-hover); color: var(--text-primary); text-decoration: none; }
530
+ .nav-link.active { color: var(--accent); background: var(--accent-subtle); border-right-color: var(--accent); font-weight: 600; }
531
+
532
+ /* Tooltip (Bootstrap JS support) */
533
+ .tooltip { position: absolute; z-index: 1080; display: block; font-size: 12px; }
534
+ .tooltip-inner { max-width: 200px; padding: var(--space-2) var(--space-3); color: var(--text-primary); background: var(--surface-primary); border: 1px solid var(--border-primary); border-radius: var(--radius-sm); box-shadow: var(--shadow-md); }
1268
535
 
1269
- [data-theme="dark"] .timeline-content,
1270
- body.dark-mode .timeline-content {
1271
- background: var(--ctp-surface0);
1272
- border-color: var(--border-color);
1273
- color: var(--text-color);
1274
- }
536
+ /* Pagination */
537
+ .pagination { display: flex; gap: 2px; list-style: none; padding: 0; margin: 0; }
538
+ .page-link { display: flex; align-items: center; justify-content: center; min-width: 32px; height: 32px; padding: 0 var(--space-2); font-size: 13px; color: var(--text-secondary); background: var(--surface-primary); border: 1px solid var(--border-primary); border-radius: var(--radius-sm); text-decoration: none; }
539
+ .page-link:hover { background: var(--surface-hover); color: var(--text-primary); }
540
+ .page-item.active .page-link { background: var(--accent); border-color: var(--accent); color: #fff; }
541
+ .page-item.disabled .page-link { opacity: 0.4; pointer-events: none; }
542
+
543
+ /* Progress */
544
+ .progress { height: 6px; background: var(--surface-secondary); border-radius: var(--radius-full); overflow: hidden; }
545
+ .progress-bar { height: 100%; background: var(--accent); border-radius: var(--radius-full); }
546
+
547
+ /* Breadcrumb */
548
+ .breadcrumb { display: flex; flex-wrap: wrap; padding: 0; margin: 0; list-style: none; font-size: 13px; }
549
+ .breadcrumb-item + .breadcrumb-item::before { content: '/'; padding: 0 var(--space-2); color: var(--text-tertiary); }
550
+ .breadcrumb-item a { color: var(--text-tertiary); }
551
+ .breadcrumb-item a:hover { color: var(--accent); }
552
+ .breadcrumb-item.active { color: var(--text-secondary); }
553
+
554
+ /* List group */
555
+ .list-group { display: flex; flex-direction: column; padding: 0; margin: 0; list-style: none; }
556
+ .list-group-item { padding: var(--space-3) var(--space-4); background: var(--surface-primary); border: 1px solid var(--border-primary); color: var(--text-primary); }
557
+ .list-group-item + .list-group-item { border-top: 0; }
558
+ .list-group-item:first-child { border-top-left-radius: var(--radius-md); border-top-right-radius: var(--radius-md); }
559
+ .list-group-item:last-child { border-bottom-left-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md); }
560
+ .list-group-flush .list-group-item { border-left: 0; border-right: 0; border-radius: 0; }
561
+
562
+ /* Toast */
563
+ .toast { min-width: 250px; padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); color: #fff; display: none; }
564
+ .toast.show { display: flex; }
565
+ .toast-container { max-width: 350px; display: flex; flex-direction: column; gap: var(--space-2); }
566
+ .toast-body { flex: 1; }
567
+
568
+ /* ============================================
569
+ Layout Shell
570
+ ============================================ */
1275
571
 
1276
- .timeline-item:hover .timeline-content {
1277
- border-color: #0d6efd;
1278
- background: #cfe2ff;
1279
- }
572
+ /* Navbar */
573
+ .red-navbar {
574
+ display: flex; align-items: center; justify-content: space-between;
575
+ padding: 0 var(--space-6);
576
+ height: 48px; flex-shrink: 0;
577
+ background: var(--surface-primary);
578
+ border-bottom: 1px solid var(--border-primary);
579
+ }
580
+ .red-search-input {
581
+ width: 240px; padding: 5px 10px 5px 30px; font-size: 13px;
582
+ border: 1px solid var(--border-primary); border-radius: var(--radius-sm);
583
+ background: var(--surface-base); color: var(--text-primary); outline: none;
584
+ font-family: var(--font-sans);
585
+ }
586
+ .red-search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-subtle); }
587
+ .red-search-input::placeholder { color: var(--text-tertiary); }
1280
588
 
1281
- [data-theme="dark"] .timeline-item:hover .timeline-content,
1282
- body.dark-mode .timeline-item:hover .timeline-content {
1283
- border-color: var(--ctp-mauve);
1284
- background: var(--ctp-surface1);
589
+ /* Sidebar */
590
+ .red-sidebar {
591
+ width: 240px; flex-shrink: 0;
592
+ height: 100vh; position: sticky; top: 0;
593
+ background: var(--surface-primary);
594
+ border-right: 1px solid var(--border-primary);
595
+ display: flex; flex-direction: column;
596
+ overflow: hidden; z-index: 20;
597
+ }
598
+ .red-sidebar-logo {
599
+ padding: var(--space-5);
600
+ border-bottom: 1px solid var(--border-primary);
601
+ display: flex; align-items: center; gap: 10px;
602
+ }
603
+ .red-sidebar-logo-icon {
604
+ width: 28px; height: 28px; border-radius: var(--radius-sm);
605
+ background: var(--accent); display: flex; align-items: center; justify-content: center;
606
+ color: #fff; font-weight: 800; font-size: 13px; font-family: var(--font-mono); flex-shrink: 0;
607
+ }
608
+ .red-sidebar-section-label {
609
+ display: flex; align-items: center; justify-content: space-between; width: 100%;
610
+ padding: var(--space-2) var(--space-5);
611
+ font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
612
+ color: var(--text-tertiary); background: none; border: none; cursor: pointer;
613
+ }
614
+ .red-sidebar-section-label:hover { color: var(--text-secondary); }
615
+ .red-sidebar-bottom {
616
+ border-top: 1px solid var(--border-primary);
617
+ padding: var(--space-3) 0;
618
+ margin-top: auto;
619
+ }
620
+ .red-nav-badge {
621
+ font-size: 11px; font-weight: 600; padding: 1px 7px;
622
+ border-radius: var(--radius-full);
623
+ background: var(--accent-subtle); color: var(--accent);
624
+ font-variant-numeric: tabular-nums;
1285
625
  }
1286
626
 
1287
- /* Collapsible Sidebar */
1288
- .sidebar {
1289
- transition: all 0.3s ease-in-out;
1290
- overflow: hidden;
1291
- }
627
+ /* Sidebar collapse */
628
+ .red-sidebar.collapsed { width: 0; min-width: 0; padding: 0; border-right: none; }
629
+ .red-sidebar.collapsed > * { opacity: 0; pointer-events: none; }
630
+ .red-sidebar { transition: width 0.2s ease, opacity 0.2s ease; }
631
+ .red-main.expanded { width: 100%; }
1292
632
 
1293
- /* Collapsed sidebar state */
1294
- .sidebar.sidebar-collapsed {
1295
- width: 0 !important;
1296
- min-width: 0 !important;
1297
- padding: 0 !important;
1298
- margin: 0 !important;
633
+ /* Mobile sidebar toggle */
634
+ @media (max-width: 767.98px) {
635
+ .red-sidebar { display: none; }
636
+ .red-sidebar.collapsed { display: none; }
1299
637
  }
1300
638
 
1301
- /* Expanded main content when sidebar is collapsed */
1302
- .content-expanded {
1303
- width: 100% !important;
1304
- max-width: 100% !important;
1305
- flex: 0 0 100% !important;
1306
- }
639
+ /* Error detail responsive grid */
640
+ .red-detail-grid { display: grid; grid-template-columns: 1fr 260px; gap: var(--space-4); }
641
+ @media (max-width: 1023.98px) { .red-detail-grid { grid-template-columns: 1fr; } }
1307
642
 
1308
- /* Hide sidebar content when collapsed */
1309
- .sidebar.sidebar-collapsed .position-sticky {
1310
- opacity: 0;
1311
- transition: opacity 0.2s ease-in-out;
643
+ /* Env badge */
644
+ .red-env-badge {
645
+ padding: 3px 10px; font-size: 11px; font-weight: 600;
646
+ border-radius: var(--radius-full);
647
+ text-transform: uppercase; letter-spacing: 0.05em;
1312
648
  }
1313
649
 
1314
- /* Smooth toggle button animation */
1315
- #sidebarToggle {
1316
- transition: transform 0.2s ease-in-out;
650
+ /* Kbd shortcut */
651
+ kbd {
652
+ display: inline-block; padding: 1px 5px; font-size: 10px;
653
+ font-family: var(--font-mono); font-weight: 500;
654
+ border-radius: 3px; border: 1px solid var(--border-secondary);
655
+ background: var(--surface-secondary); color: var(--text-tertiary);
1317
656
  }
1318
657
 
1319
- #sidebarToggle i {
1320
- transition: transform 0.2s ease-in-out;
658
+ /* Theme toggle */
659
+ .red-theme-toggle {
660
+ background: none; border: 1px solid var(--border-primary); border-radius: var(--radius-sm);
661
+ padding: 4px 8px; cursor: pointer; color: var(--text-secondary); font-size: 15px;
662
+ display: flex; align-items: center; transition: all var(--transition-fast);
1321
663
  }
664
+ .red-theme-toggle:hover { background: var(--surface-hover); color: var(--text-primary); }
1322
665
 
1323
- /* Responsive: Don't apply collapse on mobile */
1324
- @media (max-width: 767.98px) {
1325
- .sidebar.sidebar-collapsed {
1326
- width: auto !important;
1327
- }
666
+ /* App switcher */
667
+ .red-app-switcher {
668
+ background: var(--surface-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-sm);
669
+ padding: 4px 10px; cursor: pointer; color: var(--text-secondary); font-size: 12px; font-weight: 500;
670
+ display: flex; align-items: center; gap: 4px;
1328
671
  }
672
+ .red-app-switcher:hover { background: var(--surface-tertiary); }
1329
673
 
1330
- /* Add visual feedback on hover */
1331
- #sidebarToggle:hover i {
1332
- transform: scale(1.1);
1333
- }
674
+ /* ============================================
675
+ Feature-Specific Styles
676
+ ============================================ */
1334
677
 
1335
- /* Skeleton Loading Animations */
1336
- @keyframes shimmer {
1337
- 0% { background-position: -200% 0; }
1338
- 100% { background-position: 200% 0; }
678
+ /* Filter pills */
679
+ .filter-pill {
680
+ display: inline-flex; align-items: center; gap: 4px;
681
+ padding: 5px 12px; font-size: 12px; font-weight: 500;
682
+ border-radius: var(--radius-full);
683
+ border: 1px solid var(--border-primary);
684
+ background: transparent; color: var(--text-secondary);
685
+ cursor: pointer; transition: all var(--transition-fast);
686
+ }
687
+ .filter-pill:hover { border-color: var(--accent); color: var(--accent); }
688
+ .filter-pill.active, .filter-pill.bg-primary { background: var(--accent-subtle); border-color: var(--accent); color: var(--accent); }
689
+ .filter-pill .bi-x { font-size: 1.1rem; font-weight: bold; }
690
+
691
+ /* Source Code Viewer */
692
+ .source-code-viewer {
693
+ border-radius: var(--radius-lg); overflow: hidden;
694
+ box-shadow: var(--shadow-md); border: 1px solid var(--border-primary);
695
+ margin-bottom: var(--space-6);
1339
696
  }
1340
-
1341
- .skeleton {
1342
- background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
1343
- background-size: 200% 100%;
1344
- animation: shimmer 1.5s ease-in-out infinite;
1345
- border-radius: 4px;
697
+ .source-code-viewer .source-code-header {
698
+ background: var(--surface-hover); font-weight: 500;
699
+ border-bottom: 2px solid var(--border-primary);
1346
700
  }
1347
-
1348
- body.dark-mode .skeleton {
1349
- background: linear-gradient(90deg, #313244 25%, #45475a 50%, #313244 75%);
1350
- background-size: 200% 100%;
701
+ .source-code-viewer .git-blame-info {
702
+ background: var(--surface-base); border-top: 1px solid var(--border-primary);
1351
703
  }
1352
-
1353
- .skeleton-text {
1354
- height: 1em;
1355
- width: 60%;
1356
- margin-bottom: 0.5em;
704
+ .source-code-content { background: var(--surface-primary); }
705
+ .source-code-content pre { margin: 0; padding: 0; background: transparent; border: none; overflow: auto; }
706
+ .source-code-content pre code {
707
+ display: block; padding: 0; font-size: 0.9rem; line-height: 1.8;
708
+ font-family: var(--font-mono); color: var(--text-primary); background: transparent;
709
+ }
710
+ .source-code-content table.hljs-ln { width: 100%; border-collapse: collapse; margin: 0; background: transparent; }
711
+ .source-code-content .hljs-ln-line:hover, .source-code-content tr:hover { background: var(--accent-subtle); }
712
+ .source-code-content tr.error-line, .source-code-content .hljs-ln-line.error-line {
713
+ background: linear-gradient(90deg, var(--status-warning-bg) 0%, rgba(250,179,135,0.04) 100%);
714
+ border-left: 4px solid var(--status-warning);
715
+ animation: error-pulse 2s ease-in-out infinite;
1357
716
  }
1358
-
1359
- .skeleton-text-short {
1360
- width: 40%;
717
+ .source-code-content tr.error-line td { background: transparent; }
718
+ .source-code-content tr.error-line .hljs-ln-numbers {
719
+ background: linear-gradient(90deg, var(--status-warning) 0%, rgba(250,179,135,0.4) 100%);
720
+ color: #1a1a2e; font-weight: 700;
1361
721
  }
1362
-
1363
- .skeleton-card {
1364
- height: 80px;
722
+ .source-code-content tr.error-line .hljs-ln-numbers::before {
723
+ content: '\2192'; position: absolute; left: 4px; color: var(--status-critical); font-weight: bold; font-size: 1rem;
1365
724
  }
1366
-
1367
- .skeleton-row {
1368
- height: 48px;
1369
- margin-bottom: 2px;
725
+ .source-code-content .hljs-ln-numbers {
726
+ width: 70px; min-width: 70px; text-align: right; padding: 0.6rem 1rem;
727
+ color: var(--text-tertiary); user-select: none;
728
+ background: var(--surface-hover); border-right: 3px solid var(--border-primary);
729
+ font-weight: 500; font-size: 0.8125rem; vertical-align: top; font-variant-numeric: tabular-nums;
730
+ }
731
+ .source-code-content .hljs-ln-code { padding: 0.6rem 1.25rem; white-space: pre; vertical-align: top; }
732
+ .source-code-content td.hljs-ln-numbers, .source-code-content td.hljs-ln-code { border: none; }
733
+ .source-code-content code.hljs { background: transparent; padding: 0; }
734
+
735
+ /* Syntax token colors — light mode */
736
+ .source-code-content .hljs-keyword, .source-code-content .hljs-selector-tag,
737
+ .source-code-content .hljs-literal, .source-code-content .hljs-section { color: #7c3aed; }
738
+ .source-code-content .hljs-string, .source-code-content .hljs-attr { color: #059669; }
739
+ .source-code-content .hljs-name, .source-code-content .hljs-type, .source-code-content .hljs-title { color: #dc2626; }
740
+ .source-code-content .hljs-comment, .source-code-content .hljs-quote { color: #6b7280; font-style: italic; }
741
+ .source-code-content .hljs-number, .source-code-content .hljs-symbol { color: #ea580c; }
742
+ .source-code-content .hljs-built_in { color: #0891b2; }
743
+
744
+ /* Source file path */
745
+ .source-file-path { color: var(--text-primary); }
746
+
747
+ /* Backtrace */
748
+ .backtrace-frame-number {
749
+ display: inline-block; min-width: 2em; text-align: right; margin-right: 0.5em;
750
+ color: var(--text-tertiary); font-family: var(--font-mono); font-size: 0.85em; user-select: none;
1370
751
  }
752
+ span.backtrace-method-name { color: var(--status-info); }
1371
753
 
1372
- .skeleton-chart {
1373
- height: 250px;
754
+ /* Inline code */
755
+ .inline-code-highlight {
756
+ padding: 2px 6px; margin: 0 2px; font-family: var(--font-mono); font-size: 0.9em;
757
+ background: var(--surface-secondary); color: var(--accent);
758
+ border: 1px solid var(--border-primary); border-radius: 4px; white-space: nowrap;
1374
759
  }
1375
760
 
1376
- /* Hide skeleton containers by default */
1377
- .loading-skeleton {
1378
- display: none;
1379
- }
761
+ /* File path links */
762
+ .file-path-link { cursor: pointer; transition: all var(--transition-normal); }
763
+ .file-path-link:hover { background: var(--accent-subtle); color: var(--accent); box-shadow: 0 0 0 3px var(--accent-subtle); }
1380
764
 
1381
- /* Loading spinner for buttons */
1382
- .btn .loading-spinner {
1383
- display: inline-block;
1384
- width: 1em;
1385
- height: 1em;
1386
- border: 2px solid currentColor;
1387
- border-right-color: transparent;
1388
- border-radius: 50%;
1389
- animation: spinner-border 0.75s linear infinite;
1390
- vertical-align: middle;
1391
- margin-right: 0.25em;
765
+ /* Timeline */
766
+ .timeline { position: relative; padding-left: 0; }
767
+ .timeline-item { position: relative; padding-left: 40px; padding-bottom: 30px; }
768
+ .timeline-item-last { padding-bottom: 0; }
769
+ .timeline-item::before { content: ''; position: absolute; left: 11px; top: 30px; bottom: 0; width: 2px; background: var(--border-primary); }
770
+ .timeline-item-last::before { display: none; }
771
+ .timeline-marker {
772
+ position: absolute; left: 0; top: 0; width: 24px; height: 24px;
773
+ display: flex; align-items: center; justify-content: center;
774
+ background: var(--surface-primary); border: 2px solid var(--border-primary); border-radius: 50%; z-index: 1;
1392
775
  }
776
+ .timeline-marker-current { border-color: var(--status-critical); background: var(--status-critical-bg); box-shadow: 0 0 0 4px var(--status-critical-bg); }
777
+ .timeline-marker i { font-size: 12px; }
778
+ .timeline-content { background: var(--surface-primary); padding: 12px; border-radius: var(--radius-md); border: 1px solid var(--border-primary); }
779
+ .timeline-item:hover .timeline-content { border-color: var(--accent); background: var(--accent-subtle); }
1393
780
 
1394
- /* Section Navigation */
781
+ /* Section nav pills */
1395
782
  #section-nav-wrapper {
1396
- position: sticky;
1397
- top: 0;
1398
- z-index: 1020;
1399
- background: #f3f4f6;
1400
- margin: 0 -12px;
1401
- padding: 0 12px;
1402
- transition: box-shadow 0.2s ease, background-color 0.3s;
1403
- }
1404
-
1405
- #section-nav-wrapper.is-stuck {
1406
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
1407
- border-bottom: 1px solid #e5e7eb;
1408
- }
1409
-
1410
- .section-nav-scroll {
1411
- scrollbar-width: none;
1412
- -ms-overflow-style: none;
1413
- mask-image: linear-gradient(to right, black 0, black calc(100% - 24px), transparent 100%);
1414
- -webkit-mask-image: linear-gradient(to right, black 0, black calc(100% - 24px), transparent 100%);
1415
- padding-right: 16px;
1416
- }
1417
- .section-nav-scroll::-webkit-scrollbar {
1418
- display: none;
1419
- }
1420
-
1421
- .section-nav-label {
1422
- font-size: 0.85rem;
1423
- white-space: nowrap;
1424
- color: #9ca3af !important;
1425
- }
1426
-
783
+ position: sticky; top: 0; z-index: 1020;
784
+ background: var(--surface-base);
785
+ margin: 0 -12px; padding: 0 12px;
786
+ transition: box-shadow var(--transition-normal);
787
+ }
788
+ #section-nav-wrapper.is-stuck { box-shadow: var(--shadow-md); border-bottom: 1px solid var(--border-primary); }
789
+ .section-nav-scroll { scrollbar-width: none; -ms-overflow-style: none; mask-image: linear-gradient(to right, black 0, black calc(100% - 24px), transparent 100%); }
790
+ .section-nav-scroll::-webkit-scrollbar { display: none; }
791
+ .section-nav-label { font-size: 0.85rem; white-space: nowrap; color: var(--text-tertiary); }
1427
792
  .section-nav-pill {
1428
- display: inline-flex;
1429
- align-items: center;
1430
- gap: 4px;
1431
- padding: 4px 10px;
1432
- border-radius: 20px;
1433
- font-size: 0.78rem;
1434
- font-weight: 500;
1435
- white-space: nowrap;
1436
- text-decoration: none;
1437
- color: #4b5563;
1438
- background: white;
1439
- border: 1px solid #e5e7eb;
1440
- border: 1px solid transparent;
1441
- transition: all 0.15s ease;
1442
- cursor: pointer;
1443
- }
1444
-
1445
- .section-nav-pill:hover {
1446
- color: #1f2937;
1447
- background: #f3f4f6;
1448
- text-decoration: none;
1449
- }
1450
-
1451
- .section-nav-pill.active {
1452
- color: white;
1453
- background: #3b82f6;
1454
- border-color: #3b82f6;
1455
- }
1456
-
1457
- .section-nav-pill i {
1458
- font-size: 0.75rem;
1459
- }
1460
-
1461
- /* Dark mode overrides for section nav */
1462
- body.dark-mode #section-nav-wrapper { background: var(--ctp-base); }
1463
- body.dark-mode #section-nav-wrapper.is-stuck { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); border-bottom-color: var(--ctp-surface0); }
1464
- body.dark-mode .section-nav-label { color: var(--ctp-overlay2) !important; }
1465
- body.dark-mode .section-nav-pill { color: var(--ctp-subtext0); background: var(--ctp-surface0); }
1466
- body.dark-mode .section-nav-pill:hover { color: var(--ctp-text); background: var(--ctp-surface1); }
1467
- body.dark-mode .section-nav-pill.active { color: var(--ctp-base); background: var(--ctp-blue); border-color: var(--ctp-blue); }
1468
-
1469
- /* Sidebar sub-section panels */
1470
- .sidebar-section {
1471
- border-left: 3px solid #9ca3af;
1472
- padding-left: 10px;
1473
- margin-top: 16px;
1474
- margin-bottom: 8px;
1475
- }
1476
- .sidebar-section-blue { border-left-color: #3b82f6; }
1477
- .sidebar-section-red { border-left-color: #ef4444; }
1478
-
1479
- .sidebar-section-title {
1480
- font-size: 0.8rem;
1481
- font-weight: 600;
1482
- text-transform: uppercase;
1483
- letter-spacing: 0.04em;
1484
- color: #4b5563;
1485
- margin-bottom: 2px;
1486
- }
793
+ display: inline-flex; align-items: center; gap: 4px;
794
+ padding: 4px 10px; border-radius: 20px;
795
+ font-size: 0.78rem; font-weight: 500; white-space: nowrap;
796
+ text-decoration: none; color: var(--text-secondary);
797
+ background: var(--surface-primary); border: 1px solid transparent;
798
+ transition: all 0.15s ease; cursor: pointer;
799
+ }
800
+ .section-nav-pill:hover { color: var(--text-primary); background: var(--surface-hover); text-decoration: none; }
801
+ .section-nav-pill.active { color: #fff; background: var(--accent); border-color: var(--accent); }
802
+ .section-nav-pill i { font-size: 0.75rem; }
803
+
804
+ /* Sidebar metadata sections */
805
+ .sidebar-section { border-left: 3px solid var(--border-primary); padding-left: 10px; margin-top: 16px; margin-bottom: 8px; }
806
+ .sidebar-section-blue { border-left-color: var(--status-info); }
807
+ .sidebar-section-red { border-left-color: var(--status-critical); }
808
+ .sidebar-section-title { font-size: 0.8rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: var(--text-secondary); margin-bottom: 2px; }
1487
809
  .sidebar-section-title i { margin-right: 4px; }
1488
-
1489
- .sidebar-section-hint {
1490
- font-size: 0.72rem;
1491
- color: #6b7280;
1492
- margin-bottom: 6px;
1493
- display: block;
1494
- }
1495
-
1496
- .sidebar-section-body {
1497
- background: #f9fafb;
1498
- border-radius: 6px;
1499
- padding: 8px 10px;
1500
- }
1501
-
1502
- /* Metadata field labels */
1503
- .metadata-label {
1504
- font-size: 0.7rem;
1505
- font-weight: 600;
1506
- text-transform: uppercase;
1507
- letter-spacing: 0.05em;
1508
- color: #4b5563;
1509
- }
1510
- body.dark-mode .metadata-label { color: var(--ctp-overlay2); }
1511
-
1512
- /* Dark mode overrides */
1513
- body.dark-mode .sidebar-section { border-left-color: var(--ctp-overlay2); }
1514
- body.dark-mode .sidebar-section-blue { border-left-color: var(--ctp-blue); }
1515
- body.dark-mode .sidebar-section-red { border-left-color: var(--ctp-red); }
1516
- body.dark-mode .sidebar-section-title { color: var(--ctp-subtext1); }
1517
- body.dark-mode .sidebar-section-hint { color: var(--ctp-overlay2); }
1518
- body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
810
+ .sidebar-section-hint { font-size: 0.72rem; color: var(--text-tertiary); margin-bottom: 6px; display: block; }
811
+ .sidebar-section-body { background: var(--surface-base); border-radius: var(--radius-sm); padding: 8px 10px; }
812
+ .metadata-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-tertiary); }
813
+
814
+ /* Heatmap */
815
+ .heatmap-grid { background: var(--surface-primary); }
816
+ .heatmap-cell { border-color: var(--border-primary); color: var(--text-primary); }
817
+ .heatmap-hour { color: var(--accent); font-weight: 600; font-size: 0.75rem; }
818
+ .heatmap-count { color: var(--status-warning); font-weight: 600; }
819
+
820
+ /* Skeletons */
821
+ .skeleton { background: linear-gradient(90deg, var(--surface-secondary) 25%, var(--surface-tertiary) 50%, var(--surface-secondary) 75%); background-size: 200% 100%; animation: shimmer 1.5s ease-in-out infinite; border-radius: 4px; }
822
+ .skeleton-text { height: 1em; width: 60%; margin-bottom: 0.5em; }
823
+ .skeleton-text-short { width: 40%; }
824
+ .skeleton-card { height: 80px; }
825
+ .skeleton-row { height: 48px; margin-bottom: 2px; }
826
+ .skeleton-chart { height: 250px; }
827
+ .loading-skeleton { display: none; }
828
+
829
+ /* Button loading spinner */
830
+ .btn .loading-spinner {
831
+ display: inline-block; width: 1em; height: 1em;
832
+ border: 2px solid currentColor; border-right-color: transparent;
833
+ border-radius: 50%; animation: spinner-border 0.75s linear infinite;
834
+ vertical-align: middle; margin-right: 0.25em;
835
+ }
836
+
837
+ /* Empty States delightful */
838
+ @keyframes emptyFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
839
+ .red-empty-state {
840
+ text-align: center; padding: var(--space-12) var(--space-8);
841
+ animation: emptyFadeIn 0.3s ease-out;
842
+ }
843
+ .red-empty-state-icon {
844
+ width: 64px; height: 64px; margin: 0 auto var(--space-5);
845
+ display: flex; align-items: center; justify-content: center;
846
+ border-radius: 50%; background: var(--accent-subtle);
847
+ color: var(--accent); font-size: 28px;
848
+ }
849
+ .red-empty-state-title {
850
+ font-size: 16px; font-weight: 600; color: var(--text-primary);
851
+ margin-bottom: var(--space-2);
852
+ }
853
+ .red-empty-state-message {
854
+ font-size: 13px; color: var(--text-tertiary); max-width: 360px;
855
+ margin: 0 auto var(--space-5); line-height: 1.6;
856
+ }
857
+ .red-empty-state-cta {
858
+ display: inline-flex; align-items: center; gap: 6px;
859
+ padding: 8px 18px; font-size: 13px; font-weight: 500;
860
+ border-radius: var(--radius-full); border: 1px solid var(--accent);
861
+ background: var(--accent-subtle); color: var(--accent);
862
+ text-decoration: none; transition: all var(--transition-fast);
863
+ }
864
+ .red-empty-state-cta:hover { background: var(--accent); color: #fff; text-decoration: none; }
865
+
866
+ /* Page content fade-in */
867
+ @keyframes contentFadeIn { from { opacity: 0; } to { opacity: 1; } }
868
+ main { animation: contentFadeIn 0.15s ease; }
869
+
870
+ /* Footer */
871
+ .red-footer { margin-top: var(--space-12); padding: var(--space-6) 0; text-align: center; border-top: 1px solid var(--border-primary); color: var(--text-tertiary); font-size: 13px; }
872
+
873
+ /* Nav tabs (used in some pages) */
874
+ .nav-tabs { display: flex; border-bottom: 1px solid var(--border-primary); gap: 0; list-style: none; padding: 0; margin: 0; }
875
+ .nav-tabs .nav-link { padding: 10px 16px; font-size: 13px; color: var(--text-secondary); background: none; border: none; border-bottom: 2px solid transparent; cursor: pointer; margin-bottom: -1px; border-radius: 0; }
876
+ .nav-tabs .nav-link:hover { color: var(--text-primary); border-bottom-color: var(--border-primary); }
877
+ .nav-tabs .nav-link.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; background: none; }
878
+
879
+ /* Details/Summary */
880
+ details { background: var(--surface-primary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); }
881
+ summary { padding: var(--space-3) var(--space-4); cursor: pointer; font-weight: 500; color: var(--text-primary); background: var(--surface-hover); }
882
+ details[open] summary { border-bottom: 1px solid var(--border-primary); }
1519
883
  </style>
1520
884
  </head>
1521
885
 
1522
886
  <body data-turbo="false">
1523
887
  <!-- Toast Container -->
1524
888
  <div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 9999;">
1525
- <!-- Toasts will be dynamically inserted here -->
1526
889
  </div>
1527
890
 
1528
- <!-- Top Navbar -->
1529
- <nav class="navbar navbar-dark">
1530
- <div class="container-fluid">
1531
- <div class="d-flex align-items-center">
1532
- <!-- Mobile menu toggle -->
1533
- <button class="btn btn-link text-white d-md-none me-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu">
1534
- <i class="bi bi-list fs-4"></i>
1535
- </button>
1536
- <!-- Desktop sidebar toggle -->
1537
- <button class="btn btn-link text-white d-none d-md-block me-2" type="button" id="sidebarToggle" title="Toggle sidebar">
1538
- <i class="bi bi-layout-sidebar-inset fs-5"></i>
1539
- </button>
891
+ <div style="display: flex; min-height: 100vh; background: var(--surface-base);">
892
+ <!-- Sidebar -->
893
+ <nav class="red-sidebar d-none d-md-flex" id="sidebar">
894
+ <!-- Logo -->
895
+ <a href="/" class="red-sidebar-logo" title="Back to <%= Rails.application.class.module_parent_name %>" style="text-decoration: none; color: inherit;">
896
+ <div class="red-sidebar-logo-icon">R</div>
897
+ <div>
898
+ <div style="font-size: 14px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.01em;">RED</div>
899
+ <% sidebar_app_name = if defined?(@current_application_id) && @current_application_id.present? && defined?(@applications)
900
+ @applications.find { |name, id| id.to_s == @current_application_id.to_s }&.first
901
+ end %>
902
+ <div style="font-size: 10px; color: var(--text-tertiary); margin-top: -2px;"><%= sidebar_app_name || Rails.application.class.module_parent_name %> &middot; <%= Rails.env %></div>
903
+ </div>
904
+ </a>
905
+
906
+ <!-- Nav groups -->
907
+ <div style="flex: 1; overflow: auto; padding: var(--space-3) 0;">
1540
908
  <%
1541
- # Check if main app has a root route defined
1542
- begin
1543
- root_url = main_app.root_path
1544
- has_root = true
1545
- rescue NoMethodError
1546
- has_root = false
1547
- end
909
+ nav_params = params[:application_id].present? ? { application_id: params[:application_id] } : {}
1548
910
  %>
1549
- <% if has_root %>
1550
- <a class="navbar-brand d-flex align-items-center" href="<%= root_url %>" title="Back to <%= Rails.application.class.module_parent_name %>">
1551
- <i class="bi bi-bug-fill"></i>
1552
- <span class="d-none d-sm-inline"><%= Rails.application.class.module_parent_name %></span>
1553
- <span class="d-none d-md-inline text-white-50 mx-2">|</span>
1554
- <span class="d-none d-md-inline">
1555
- <strong>RED</strong>
1556
- <small class="text-white-50 ms-1" style="font-size: 0.65em; vertical-align: middle;">Rails Error Dashboard</small>
1557
- </span>
1558
- </a>
1559
- <% else %>
1560
- <span class="navbar-brand d-flex align-items-center">
1561
- <i class="bi bi-bug-fill"></i>
1562
- <span class="d-none d-sm-inline"><%= Rails.application.class.module_parent_name %></span>
1563
- <span class="d-none d-md-inline text-white-50 mx-2">|</span>
1564
- <span class="d-none d-md-inline">
1565
- <strong>RED</strong>
1566
- <small class="text-white-50 ms-1" style="font-size: 0.65em; vertical-align: middle;">Rails Error Dashboard</small>
1567
- </span>
1568
- </span>
1569
- <% end %>
1570
- </div>
1571
- <div class="d-flex align-items-center gap-3">
1572
- <!-- Application Switcher (only show if multiple apps) -->
1573
- <% if defined?(@applications) && @applications&.size.to_i > 1 %>
1574
- <div class="dropdown">
1575
- <button class="btn btn-sm dropdown-toggle app-switcher-btn"
1576
- type="button"
1577
- id="appSwitcher"
1578
- data-bs-toggle="dropdown"
1579
- aria-expanded="false">
1580
- <i class="bi bi-layers"></i>
1581
- <% if params[:application_id].present? %>
1582
- <%= @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first || 'Unknown' %>
1583
- <% else %>
1584
- All Apps
1585
- <% end %>
1586
- </button>
1587
- <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="appSwitcher">
1588
- <%
1589
- base_route_params = request.path_parameters.slice(:controller, :action)
1590
- current_filter_params = permitted_filter_params
1591
- %>
1592
- <li>
1593
- <%= link_to "All Applications",
1594
- url_for(base_route_params.merge(current_filter_params.except(:application_id))),
1595
- class: "dropdown-item #{'active' unless params[:application_id].present?}" %>
1596
- </li>
1597
- <li><hr class="dropdown-divider"></li>
1598
- <% @applications.each do |app_name, app_id| %>
1599
- <li>
1600
- <%= link_to app_name,
1601
- url_for(base_route_params.merge(current_filter_params.merge(application_id: app_id))),
1602
- class: "dropdown-item #{params[:application_id].to_s == app_id.to_s ? 'active' : ''}" %>
1603
- </li>
1604
- <% end %>
1605
- </ul>
1606
- </div>
1607
- <% end %>
1608
911
 
1609
- <button class="theme-toggle" id="themeToggle">
1610
- <i class="bi bi-moon-fill" id="themeIcon"></i>
1611
- </button>
1612
- <div class="text-white d-none d-md-block">
1613
- <small><%= Rails.env.titleize %></small>
1614
- </div>
1615
- </div>
1616
- </div>
1617
- </nav>
1618
-
1619
- <div class="container-fluid">
1620
- <div class="row">
1621
- <!-- Sidebar -->
1622
- <nav class="col-md-2 d-none d-md-block sidebar" id="sidebar">
1623
- <div class="position-sticky pt-3">
1624
- <%
1625
- # Preserve application_id across navigation
1626
- nav_params = params[:application_id].present? ? { application_id: params[:application_id] } : {}
1627
- %>
912
+ <!-- CORE section -->
913
+ <div style="margin-bottom: var(--space-2);">
914
+ <div class="red-sidebar-section-label">Core</div>
1628
915
  <ul class="nav flex-column">
1629
916
  <li class="nav-item">
1630
917
  <%= link_to root_path(nav_params), class: "nav-link #{request.path == root_path ? 'active' : ''}" do %>
1631
- <i class="bi bi-speedometer2"></i> Overview
918
+ <i class="bi bi-grid-1x2"></i> Overview
1632
919
  <% end %>
1633
920
  </li>
1634
921
  <li class="nav-item">
1635
922
  <%= link_to errors_path(nav_params), class: "nav-link #{(controller_name == 'errors' && action_name == 'index') ? 'active' : ''}" do %>
1636
- <i class="bi bi-list-ul"></i> All Errors
923
+ <i class="bi bi-bug"></i>
924
+ <span style="flex: 1;">Errors</span>
925
+ <% if unresolved_badge_count && unresolved_badge_count > 0 %>
926
+ <span class="red-nav-badge"><%= unresolved_badge_count %></span>
927
+ <% end %>
1637
928
  <% end %>
1638
929
  </li>
1639
930
  <li class="nav-item">
1640
931
  <%= link_to analytics_errors_path(nav_params), class: "nav-link #{request.path == analytics_errors_path ? 'active' : ''}" do %>
1641
- <i class="bi bi-graph-up"></i> Analytics
932
+ <i class="bi bi-bar-chart-line"></i> Analytics
1642
933
  <% end %>
1643
934
  </li>
1644
- <li class="nav-item">
1645
- <%= link_to platform_comparison_errors_path(nav_params), class: "nav-link #{request.path == platform_comparison_errors_path ? 'active' : ''}" do %>
1646
- <i class="bi bi-heart-pulse"></i> Platform Health
935
+ </ul>
936
+ </div>
937
+
938
+ <!-- HEALTH section (feature-gated) -->
939
+ <% health_items = [] %>
940
+ <% health_items << { path: platform_comparison_errors_path(nav_params), icon: 'bi-cpu', label: 'Platform' } %>
941
+ <% if RailsErrorDashboard.configuration.enable_breadcrumbs %>
942
+ <% health_items << { path: cache_health_summary_errors_path(nav_params), icon: 'bi-hdd-stack', label: 'Cache' } %>
943
+ <% end %>
944
+ <% if RailsErrorDashboard.configuration.enable_system_health %>
945
+ <% health_items << { path: job_health_summary_errors_path(nav_params), icon: 'bi-gear-wide-connected', label: 'Jobs' } %>
946
+ <% health_items << { path: database_health_summary_errors_path(nav_params), icon: 'bi-database', label: 'Database' } %>
947
+ <% end %>
948
+ <% if RailsErrorDashboard.configuration.enable_rack_attack_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
949
+ <% health_items << { path: rack_attack_summary_errors_path(nav_params), icon: 'bi-shield-exclamation', label: 'Rate Limits' } %>
950
+ <% end %>
951
+ <% if RailsErrorDashboard.configuration.enable_actioncable_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
952
+ <% health_items << { path: actioncable_health_summary_errors_path(nav_params), icon: 'bi-broadcast', label: 'ActionCable' } %>
953
+ <% end %>
954
+ <% if RailsErrorDashboard.configuration.enable_activestorage_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
955
+ <% health_items << { path: activestorage_health_summary_errors_path(nav_params), icon: 'bi-cloud-arrow-up', label: 'ActiveStorage' } %>
956
+ <% end %>
957
+
958
+ <% if health_items.any? %>
959
+ <div style="margin-bottom: var(--space-2);" id="navHealthSection">
960
+ <button class="red-sidebar-section-label" onclick="toggleNavSection('navHealthItems')">
961
+ Health
962
+ <i class="bi bi-chevron-down" style="font-size: 10px;" id="navHealthChevron"></i>
963
+ </button>
964
+ <ul class="nav flex-column" id="navHealthItems">
965
+ <% health_items.each do |item| %>
966
+ <li class="nav-item">
967
+ <%= link_to item[:path], class: "nav-link #{request.path == item[:path] ? 'active' : ''}" do %>
968
+ <i class="bi <%= item[:icon] %>"></i> <%= item[:label] %>
969
+ <% end %>
970
+ </li>
1647
971
  <% end %>
1648
- </li>
972
+ </ul>
973
+ </div>
974
+ <% end %>
975
+
976
+ <!-- DIAGNOSTICS section (feature-gated) -->
977
+ <% diag_items = [] %>
978
+ <% if RailsErrorDashboard.configuration.enable_breadcrumbs %>
979
+ <% diag_items << { path: n_plus_one_summary_errors_path(nav_params), icon: 'bi-layers', label: 'N+1 Queries' } %>
980
+ <% diag_items << { path: deprecations_errors_path(nav_params), icon: 'bi-exclamation-triangle', label: 'Deprecations' } %>
981
+ <% end %>
982
+ <% if RailsErrorDashboard.configuration.detect_swallowed_exceptions %>
983
+ <% diag_items << { path: swallowed_exceptions_errors_path(nav_params), icon: 'bi-eye-slash', label: 'Swallowed' } %>
984
+ <% end %>
985
+ <% if RailsErrorDashboard.configuration.enable_diagnostic_dump %>
986
+ <% diag_items << { path: diagnostic_dumps_errors_path(nav_params), icon: 'bi-clipboard-pulse', label: 'Diagnostics' } %>
987
+ <% end %>
988
+
989
+ <% if diag_items.any? %>
990
+ <div style="margin-bottom: var(--space-2);" id="navDiagSection">
991
+ <button class="red-sidebar-section-label" onclick="toggleNavSection('navDiagItems')">
992
+ Diagnostics
993
+ <i class="bi bi-chevron-down" style="font-size: 10px;" id="navDiagChevron"></i>
994
+ </button>
995
+ <ul class="nav flex-column" id="navDiagItems">
996
+ <% diag_items.each do |item| %>
997
+ <li class="nav-item">
998
+ <%= link_to item[:path], class: "nav-link #{request.path == item[:path] ? 'active' : ''}" do %>
999
+ <i class="bi <%= item[:icon] %>"></i> <%= item[:label] %>
1000
+ <% end %>
1001
+ </li>
1002
+ <% end %>
1003
+ </ul>
1004
+ </div>
1005
+ <% end %>
1006
+
1007
+ <!-- INSIGHTS section -->
1008
+ <div style="margin-bottom: var(--space-2);" id="navInsightsSection">
1009
+ <button class="red-sidebar-section-label" onclick="toggleNavSection('navInsightsItems')">
1010
+ Insights
1011
+ <i class="bi bi-chevron-down" style="font-size: 10px;" id="navInsightsChevron"></i>
1012
+ </button>
1013
+ <ul class="nav flex-column" id="navInsightsItems">
1649
1014
  <li class="nav-item">
1650
1015
  <%= link_to correlation_errors_path(nav_params), class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}" do %>
1651
1016
  <i class="bi bi-diagram-3"></i> Correlation
@@ -1661,139 +1026,143 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1661
1026
  <i class="bi bi-people"></i> User Impact
1662
1027
  <% end %>
1663
1028
  </li>
1664
- <li class="nav-item">
1665
- <%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
1666
- <i class="bi bi-gear"></i> Settings
1667
- <% end %>
1668
- </li>
1669
- <% if RailsErrorDashboard.configuration.enable_breadcrumbs %>
1670
- <li class="nav-item">
1671
- <%= link_to deprecations_errors_path(nav_params), class: "nav-link #{request.path == deprecations_errors_path ? 'active' : ''}" do %>
1672
- <i class="bi bi-exclamation-triangle"></i> Deprecations
1673
- <% end %>
1674
- </li>
1675
- <li class="nav-item">
1676
- <%= link_to n_plus_one_summary_errors_path(nav_params), class: "nav-link #{request.path == n_plus_one_summary_errors_path ? 'active' : ''}" do %>
1677
- <i class="bi bi-arrow-repeat"></i> N+1 Queries
1678
- <% end %>
1679
- </li>
1680
- <li class="nav-item">
1681
- <%= link_to cache_health_summary_errors_path(nav_params), class: "nav-link #{request.path == cache_health_summary_errors_path ? 'active' : ''}" do %>
1682
- <i class="bi bi-lightning-charge"></i> Cache Health
1683
- <% end %>
1684
- </li>
1685
- <% end %>
1686
- <% if RailsErrorDashboard.configuration.enable_rack_attack_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
1687
- <li class="nav-item">
1688
- <%= link_to rack_attack_summary_errors_path(nav_params), class: "nav-link #{request.path == rack_attack_summary_errors_path ? 'active' : ''}" do %>
1689
- <i class="bi bi-shield-exclamation"></i> Rate Limits
1690
- <% end %>
1691
- </li>
1692
- <% end %>
1693
- <% if RailsErrorDashboard.configuration.enable_actioncable_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
1694
- <li class="nav-item">
1695
- <%= link_to actioncable_health_summary_errors_path(nav_params), class: "nav-link #{request.path == actioncable_health_summary_errors_path ? 'active' : ''}" do %>
1696
- <i class="bi bi-broadcast"></i> ActionCable
1697
- <% end %>
1698
- </li>
1699
- <% end %>
1700
- <% if RailsErrorDashboard.configuration.enable_activestorage_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
1701
- <li class="nav-item">
1702
- <%= link_to activestorage_health_summary_errors_path(nav_params), class: "nav-link #{request.path == activestorage_health_summary_errors_path ? 'active' : ''}" do %>
1703
- <i class="bi bi-cloud-arrow-up"></i> ActiveStorage
1704
- <% end %>
1705
- </li>
1706
- <% end %>
1707
- <% if RailsErrorDashboard.configuration.enable_system_health %>
1708
- <li class="nav-item">
1709
- <%= link_to job_health_summary_errors_path(nav_params), class: "nav-link #{request.path == job_health_summary_errors_path ? 'active' : ''}" do %>
1710
- <i class="bi bi-cpu"></i> Job Health
1711
- <% end %>
1712
- </li>
1713
- <li class="nav-item">
1714
- <%= link_to database_health_summary_errors_path(nav_params), class: "nav-link #{request.path == database_health_summary_errors_path ? 'active' : ''}" do %>
1715
- <i class="bi bi-database"></i> DB Health
1716
- <% end %>
1717
- </li>
1029
+ </ul>
1030
+ </div>
1031
+ </div>
1032
+
1033
+ <!-- Bottom pinned -->
1034
+ <div class="red-sidebar-bottom">
1035
+ <ul class="nav flex-column">
1036
+ <li class="nav-item">
1037
+ <%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
1038
+ <i class="bi bi-sliders"></i> Settings
1718
1039
  <% end %>
1719
- <% if RailsErrorDashboard.configuration.detect_swallowed_exceptions %>
1720
- <li class="nav-item">
1721
- <%= link_to swallowed_exceptions_errors_path(nav_params), class: "nav-link #{request.path == swallowed_exceptions_errors_path ? 'active' : ''}" do %>
1722
- <i class="bi bi-eye-slash"></i> Swallowed
1040
+ </li>
1041
+ </ul>
1042
+ </div>
1043
+ </nav>
1044
+
1045
+ <!-- Main area -->
1046
+ <div style="flex: 1; display: flex; flex-direction: column; min-width: 0;" id="mainArea">
1047
+ <!-- Top Navbar -->
1048
+ <header class="red-navbar">
1049
+ <div style="display: flex; align-items: center; gap: var(--space-4);">
1050
+ <!-- Mobile menu toggle -->
1051
+ <button class="btn btn-link d-md-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu" style="font-size: 18px; color: var(--text-secondary);">
1052
+ <i class="bi bi-list"></i>
1053
+ </button>
1054
+ <!-- Desktop sidebar toggle -->
1055
+ <button class="btn btn-link d-none d-md-flex" type="button" id="sidebarToggle" title="Toggle sidebar (S)" style="font-size: 16px; color: var(--text-secondary);">
1056
+ <i class="bi bi-layout-sidebar-inset"></i>
1057
+ </button>
1058
+ <!-- Search -->
1059
+ <div style="position: relative;">
1060
+ <i class="bi bi-search" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 13px; color: var(--text-tertiary);"></i>
1061
+ <input class="red-search-input" placeholder="Search errors... /" id="globalSearch" />
1062
+ </div>
1063
+ </div>
1064
+ <div style="display: flex; align-items: center; gap: var(--space-3);">
1065
+ <!-- Application Switcher (multi-app) -->
1066
+ <% if defined?(@applications) && @applications&.size.to_i > 1 %>
1067
+ <div class="dropdown">
1068
+ <button class="red-app-switcher dropdown-toggle" type="button" id="appSwitcher" data-bs-toggle="dropdown" aria-expanded="false">
1069
+ <i class="bi bi-layers"></i>
1070
+ <% if params[:application_id].present? %>
1071
+ <%= @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first || 'Unknown' %>
1072
+ <% else %>
1073
+ All Apps (<%= @applications.size %>)
1723
1074
  <% end %>
1724
- </li>
1725
- <% end %>
1726
- <% if RailsErrorDashboard.configuration.enable_diagnostic_dump %>
1727
- <li class="nav-item">
1728
- <%= link_to diagnostic_dumps_errors_path(nav_params), class: "nav-link #{request.path == diagnostic_dumps_errors_path ? 'active' : ''}" do %>
1729
- <i class="bi bi-clipboard-pulse"></i> Diagnostics
1075
+ </button>
1076
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="appSwitcher">
1077
+ <%
1078
+ # On detail pages (show), switching apps should go to the errors list
1079
+ # since the current error may not belong to the selected app
1080
+ on_detail_page = request.path_parameters[:action] == "show"
1081
+ if on_detail_page
1082
+ switcher_base = { controller: request.path_parameters[:controller], action: "index" }
1083
+ switcher_filter_params = {}
1084
+ else
1085
+ switcher_base = request.path_parameters.slice(:controller, :action)
1086
+ switcher_filter_params = permitted_filter_params
1087
+ end
1088
+ %>
1089
+ <li>
1090
+ <%= link_to "All Applications",
1091
+ url_for(switcher_base.merge(switcher_filter_params.except(:application_id))),
1092
+ class: "dropdown-item #{'active' unless params[:application_id].present?}" %>
1093
+ </li>
1094
+ <li><hr class="dropdown-divider"></li>
1095
+ <% @applications.each do |app_name, app_id| %>
1096
+ <li>
1097
+ <%= link_to app_name,
1098
+ url_for(switcher_base.merge(switcher_filter_params.merge(application_id: app_id))),
1099
+ class: "dropdown-item #{params[:application_id].to_s == app_id.to_s ? 'active' : ''}" %>
1100
+ </li>
1730
1101
  <% end %>
1731
- </li>
1732
- <% end %>
1733
- </ul>
1734
-
1735
- <h6 class="mt-4">QUICK FILTERS</h6>
1736
- <ul class="nav flex-column">
1737
- <li class="nav-item">
1738
- <%= link_to errors_path(nav_params.merge(status: 'unresolved')), class: "nav-link" do %>
1739
- <i class="bi bi-exclamation-circle"></i> Unresolved
1740
- <% end %>
1741
- </li>
1742
- <li class="nav-item">
1743
- <%= link_to errors_path(nav_params.merge(severity: 'critical')), class: "nav-link" do %>
1744
- <i class="bi bi-exclamation-triangle-fill text-danger"></i> Critical
1745
- <% end %>
1746
- </li>
1747
- <li class="nav-item">
1748
- <%= link_to errors_path(nav_params.merge(priority_level: 'high')), class: "nav-link" do %>
1749
- <i class="bi bi-flag-fill text-warning"></i> High Priority
1750
- <% end %>
1751
- </li>
1752
- <li class="nav-item">
1753
- <%= link_to errors_path(nav_params.merge(platform: 'iOS')), class: "nav-link #{params[:platform] == 'iOS' ? 'active' : ''}" do %>
1754
- <i class="bi bi-apple"></i> iOS Errors
1755
- <% end %>
1756
- </li>
1757
- <li class="nav-item">
1758
- <%= link_to errors_path(nav_params.merge(platform: 'Android')), class: "nav-link #{params[:platform] == 'Android' ? 'active' : ''}" do %>
1759
- <i class="bi bi-android2"></i> Android Errors
1760
- <% end %>
1761
- </li>
1762
- <li class="nav-item">
1763
- <%= link_to errors_path(nav_params.merge(platform: 'Web')), class: "nav-link #{params[:platform] == 'Web' ? 'active' : ''}" do %>
1764
- <i class="bi bi-globe"></i> Web Errors
1765
- <% end %>
1766
- </li>
1767
- <li class="nav-item">
1768
- <%= link_to errors_path(nav_params.merge(platform: 'API')), class: "nav-link #{params[:platform] == 'API' ? 'active' : ''}" do %>
1769
- <i class="bi bi-server"></i> API Errors
1770
- <% end %>
1771
- </li>
1772
- <li class="nav-item">
1773
- <%= link_to errors_path(nav_params.merge(platform: 'Background Jobs')), class: "nav-link #{params[:platform] == 'Background Jobs' ? 'active' : ''}" do %>
1774
- <i class="bi bi-gear-fill"></i> Background Jobs
1775
- <% end %>
1776
- </li>
1777
- </ul>
1102
+ </ul>
1103
+ </div>
1104
+ <% end %>
1105
+ <button type="button" title="Keyboard shortcuts (?)" data-bs-toggle="modal" data-bs-target="#keyboardShortcutsModal" style="background: none; border: none; padding: 0; cursor: pointer; line-height: 1;"><kbd>?</kbd></button>
1106
+ <button class="red-theme-toggle" id="themeToggle" title="Toggle theme">
1107
+ <i class="bi bi-moon" id="themeIcon"></i>
1108
+ </button>
1109
+ <span class="red-env-badge" style="background: var(--status-success-bg); color: var(--status-success);"><%= Rails.env %></span>
1778
1110
  </div>
1779
- </nav>
1111
+ </header>
1780
1112
 
1781
1113
  <!-- Main content -->
1782
- <main class="col-md-10 ms-sm-auto px-md-4" id="mainContent">
1114
+ <main style="flex: 1; padding: var(--space-6) var(--space-8); max-width: 1200px; width: 100%; margin: 0 auto;">
1783
1115
  <% if @default_credentials_warning %>
1784
- <div class="alert alert-warning d-flex align-items-center mt-3 mb-0" role="alert" style="border-left: 4px solid #ffc107;">
1785
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="me-2 flex-shrink-0" viewBox="0 0 16 16">
1786
- <path d="M8.982 1.566a1.13 1.13 0 0 0-1.96 0L.165 13.233c-.457.778.091 1.767.98 1.767h13.713c.889 0 1.438-.99.98-1.767L8.982 1.566zM8 5c.535 0 .954.462.9.995l-.35 3.507a.552.552 0 0 1-1.1 0L7.1 5.995A.905.905 0 0 1 8 5zm.002 6a1 1 0 1 1 0 2 1 1 0 0 1 0-2z"/>
1787
- </svg>
1788
- <div>
1116
+ <div id="security-warning" class="alert alert-warning" style="display: flex; align-items: center; gap: 10px; margin-top: var(--space-2); margin-bottom: var(--space-4); border-left: 4px solid var(--status-warning);">
1117
+ <i class="bi bi-exclamation-triangle-fill" style="font-size: 18px; flex-shrink: 0;"></i>
1118
+ <div style="flex: 1;">
1789
1119
  <strong>Security Warning:</strong> You are using default credentials (gandalf/youshallnotpass).
1790
1120
  Set <code>ERROR_DASHBOARD_USER</code> and <code>ERROR_DASHBOARD_PASSWORD</code> environment variables,
1791
- or configure <code>authenticate_with</code> in your initializer. The app will not boot in production until this is changed.
1121
+ or configure <code>authenticate_with</code> in your initializer.
1792
1122
  </div>
1123
+ <button type="button" onclick="this.parentElement.style.display='none'; try { sessionStorage.setItem('red_dismiss_creds_warning','1') } catch(e) {}" style="background: none; border: none; cursor: pointer; color: var(--text-secondary); font-size: 18px; padding: 0 4px; flex-shrink: 0; line-height: 1;" title="Dismiss for this session">&times;</button>
1793
1124
  </div>
1125
+ <script>try { if (sessionStorage.getItem('red_dismiss_creds_warning') === '1') { document.getElementById('security-warning').style.display = 'none'; } } catch(e) {}</script>
1794
1126
  <% end %>
1795
1127
  <%= yield %>
1796
1128
  </main>
1129
+
1130
+ <!-- Footer -->
1131
+ <footer class="red-footer">
1132
+ <p style="margin-bottom: 4px;">
1133
+ <i class="bi bi-bug-fill" style="color: var(--accent);"></i>
1134
+ Built with <a href="https://github.com/AnjanJ/rails_error_dashboard" target="_blank"><strong>RED</strong> &mdash; Rails Error Dashboard</a>
1135
+ </p>
1136
+ <p style="font-size: 12px; color: var(--text-tertiary); margin: 0;">
1137
+ v<%= RailsErrorDashboard::VERSION %> &middot; Built with ❤️ by <a href="https://anjan.dev" target="_blank">Anjan Jagirdar</a>
1138
+ </p>
1139
+ </footer>
1140
+ </div>
1141
+ </div>
1142
+
1143
+ <!-- Mobile Sidebar (Offcanvas) -->
1144
+ <div class="offcanvas offcanvas-start" tabindex="-1" id="sidebarMenu" aria-labelledby="sidebarMenuLabel">
1145
+ <div class="offcanvas-header">
1146
+ <a href="/" style="display: flex; align-items: center; gap: 10px; text-decoration: none; color: inherit;" title="Back to app home">
1147
+ <div class="red-sidebar-logo-icon">R</div>
1148
+ <strong>RED</strong>
1149
+ </a>
1150
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
1151
+ </div>
1152
+ <div class="offcanvas-body">
1153
+ <%
1154
+ nav_params = params[:application_id].present? ? { application_id: params[:application_id] } : {}
1155
+ %>
1156
+ <ul class="nav flex-column">
1157
+ <li class="nav-item"><%= link_to root_path(nav_params), class: "nav-link" do %><i class="bi bi-grid-1x2"></i> Overview<% end %></li>
1158
+ <li class="nav-item"><%= link_to errors_path(nav_params), class: "nav-link" do %><i class="bi bi-bug"></i> Errors<% end %></li>
1159
+ <li class="nav-item"><%= link_to analytics_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-bar-chart-line"></i> Analytics<% end %></li>
1160
+ <li class="nav-item"><%= link_to platform_comparison_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-cpu"></i> Platform<% end %></li>
1161
+ <li class="nav-item"><%= link_to correlation_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-diagram-3"></i> Correlation<% end %></li>
1162
+ <li class="nav-item"><%= link_to releases_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-rocket-takeoff"></i> Releases<% end %></li>
1163
+ <li class="nav-item"><%= link_to user_impact_errors_path(nav_params), class: "nav-link" do %><i class="bi bi-people"></i> User Impact<% end %></li>
1164
+ <li class="nav-item"><%= link_to settings_path(nav_params), class: "nav-link" do %><i class="bi bi-sliders"></i> Settings<% end %></li>
1165
+ </ul>
1797
1166
  </div>
1798
1167
  </div>
1799
1168
 
@@ -1810,48 +1179,32 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1810
1179
  <div class="modal-body">
1811
1180
  <div class="list-group list-group-flush">
1812
1181
  <div class="list-group-item d-flex justify-content-between align-items-center">
1813
- <span><i class="bi bi-arrow-clockwise text-primary"></i> Refresh page</span>
1814
- <kbd class="bg-secondary text-white px-2 py-1 rounded">R</kbd>
1182
+ <span><i class="bi bi-arrow-clockwise" style="color: var(--accent);"></i> Refresh page</span>
1183
+ <kbd>R</kbd>
1815
1184
  </div>
1816
1185
  <div class="list-group-item d-flex justify-content-between align-items-center">
1817
- <span><i class="bi bi-search text-primary"></i> Focus search</span>
1818
- <kbd class="bg-secondary text-white px-2 py-1 rounded">/</kbd>
1186
+ <span><i class="bi bi-search" style="color: var(--accent);"></i> Focus search</span>
1187
+ <kbd>/</kbd>
1819
1188
  </div>
1820
1189
  <div class="list-group-item d-flex justify-content-between align-items-center">
1821
- <span><i class="bi bi-graph-up text-primary"></i> Go to analytics</span>
1822
- <kbd class="bg-secondary text-white px-2 py-1 rounded">A</kbd>
1190
+ <span><i class="bi bi-bar-chart-line" style="color: var(--accent);"></i> Analytics</span>
1191
+ <kbd>A</kbd>
1823
1192
  </div>
1824
1193
  <div class="list-group-item d-flex justify-content-between align-items-center">
1825
- <span><i class="bi bi-layout-sidebar text-primary"></i> Toggle sidebar</span>
1826
- <kbd class="bg-secondary text-white px-2 py-1 rounded">S</kbd>
1194
+ <span><i class="bi bi-layout-sidebar" style="color: var(--accent);"></i> Toggle sidebar</span>
1195
+ <kbd>S</kbd>
1827
1196
  </div>
1828
1197
  <div class="list-group-item d-flex justify-content-between align-items-center">
1829
- <span><i class="bi bi-question-circle text-primary"></i> Show this help</span>
1830
- <kbd class="bg-secondary text-white px-2 py-1 rounded">?</kbd>
1198
+ <span><i class="bi bi-keyboard" style="color: var(--accent);"></i> Show shortcuts</span>
1199
+ <kbd>?</kbd>
1831
1200
  </div>
1832
1201
  </div>
1833
1202
  </div>
1834
- <div class="modal-footer">
1835
- <small class="text-muted">Press <kbd class="bg-secondary text-white px-2 py-1 rounded">?</kbd> anytime to show shortcuts</small>
1836
- </div>
1837
1203
  </div>
1838
1204
  </div>
1839
1205
  </div>
1840
1206
 
1841
- <!-- Footer -->
1842
- <footer class="mt-5 py-4 text-center border-top">
1843
- <div class="container">
1844
- <p class="text-muted mb-1">
1845
- <i class="bi bi-bug-fill text-primary"></i>
1846
- Built with <a href="https://github.com/AnjanJ/rails_error_dashboard" target="_blank" class="text-decoration-none"><strong>RED</strong> — Rails Error Dashboard</a>
1847
- </p>
1848
- <p class="text-muted small mb-0">
1849
- Created by <a href="https://www.anjan.dev/" target="_blank" class="text-decoration-none">Anjan Jagirdar</a>
1850
- </p>
1851
- </div>
1852
- </footer>
1853
-
1854
- <!-- Bootstrap JS -->
1207
+ <!-- Bootstrap JS (for modals, tooltips, dropdowns, offcanvas) -->
1855
1208
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
1856
1209
 
1857
1210
  <!-- Stimulus (for loading state management) -->
@@ -1882,26 +1235,20 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1882
1235
  if (this._safetyTimeout) clearTimeout(this._safetyTimeout);
1883
1236
  }
1884
1237
 
1885
- // Called on filter form submit
1886
1238
  submit() {
1887
1239
  this.showSkeletons();
1888
1240
  this.disableSubmitButtons();
1889
1241
  this.startSafetyTimeout();
1890
1242
  }
1891
1243
 
1892
- // Called on async action button click
1893
1244
  click(event) {
1894
1245
  var button = event.currentTarget;
1895
1246
  if (button.disabled) return;
1896
-
1897
1247
  button.disabled = true;
1898
1248
  var originalHTML = button.dataset.loadingOriginalHtml;
1899
- if (!originalHTML) {
1900
- button.dataset.loadingOriginalHtml = button.innerHTML;
1901
- }
1249
+ if (!originalHTML) { button.dataset.loadingOriginalHtml = button.innerHTML; }
1902
1250
  var spinnerHTML = '<span class="loading-spinner"></span>';
1903
- var buttonText = button.textContent.trim();
1904
- button.innerHTML = spinnerHTML + ' ' + buttonText;
1251
+ button.innerHTML = spinnerHTML + ' ' + button.textContent.trim();
1905
1252
  }
1906
1253
 
1907
1254
  showSkeletons() {
@@ -1919,11 +1266,8 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1919
1266
  disableSubmitButtons() {
1920
1267
  this.submitButtonTargets.forEach(function(btn) {
1921
1268
  btn.disabled = true;
1922
- if (!btn.dataset.loadingOriginalHtml) {
1923
- btn.dataset.loadingOriginalHtml = btn.innerHTML;
1924
- }
1925
- var spinnerHTML = '<span class="loading-spinner"></span>';
1926
- btn.innerHTML = spinnerHTML + ' Loading...';
1269
+ if (!btn.dataset.loadingOriginalHtml) { btn.dataset.loadingOriginalHtml = btn.innerHTML; }
1270
+ btn.innerHTML = '<span class="loading-spinner"></span> Loading...';
1927
1271
  });
1928
1272
  }
1929
1273
 
@@ -1939,16 +1283,12 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1939
1283
 
1940
1284
  startSafetyTimeout() {
1941
1285
  var self = this;
1942
- this._safetyTimeout = setTimeout(function() {
1943
- self.hideSkeletons();
1944
- }, 10000);
1286
+ this._safetyTimeout = setTimeout(function() { self.hideSkeletons(); }, 10000);
1945
1287
  }
1946
1288
  });
1947
1289
  })();
1948
1290
  </script>
1949
1291
 
1950
- <!-- Dashboard JavaScript (inline for production compatibility) -->
1951
-
1952
1292
  <!-- Syntax Highlighting -->
1953
1293
  <script>
1954
1294
  (function() {
@@ -1973,15 +1313,11 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1973
1313
  codeBlocks.forEach(function(codeBlock) {
1974
1314
  var errorLine = parseInt(codeBlock.dataset.errorLine);
1975
1315
  var startLine = parseInt(codeBlock.dataset.startLine) || 1;
1976
-
1977
1316
  if (errorLine && !isNaN(errorLine)) {
1978
1317
  var table = codeBlock.querySelector('table.hljs-ln');
1979
1318
  if (table) {
1980
- var rows = table.querySelectorAll('tr');
1981
- rows.forEach(function(row, index) {
1982
- if ((startLine + index) === errorLine) {
1983
- row.classList.add('error-line');
1984
- }
1319
+ table.querySelectorAll('tr').forEach(function(row, index) {
1320
+ if ((startLine + index) === errorLine) { row.classList.add('error-line'); }
1985
1321
  });
1986
1322
  }
1987
1323
  }
@@ -1997,109 +1333,84 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1997
1333
  })();
1998
1334
  </script>
1999
1335
 
2000
- <!-- Theme Toggle -->
1336
+ <!-- Theme Toggle + Chart Theme -->
2001
1337
  <script>
2002
- // Load saved theme on page load (before DOMContentLoaded to prevent flash)
2003
- (function() {
2004
- var savedTheme = localStorage.getItem('theme');
2005
- if (savedTheme === 'dark') {
2006
- document.body.classList.add('dark-mode');
2007
- }
2008
- })();
2009
-
2010
- // Theme toggle after DOM loads
2011
1338
  document.addEventListener('DOMContentLoaded', function() {
2012
1339
  var themeToggle = document.getElementById('themeToggle');
2013
1340
  var themeIcon = document.getElementById('themeIcon');
2014
1341
 
1342
+ function isDark() { return document.documentElement.getAttribute('data-theme') === 'dark'; }
1343
+
2015
1344
  function updateIcon() {
2016
- if (document.body.classList.contains('dark-mode')) {
2017
- themeIcon.className = 'bi bi-sun-fill';
2018
- } else {
2019
- themeIcon.className = 'bi bi-moon-fill';
2020
- }
1345
+ if (themeIcon) themeIcon.className = isDark() ? 'bi bi-sun' : 'bi bi-moon';
2021
1346
  }
2022
1347
 
2023
1348
  updateIcon();
2024
1349
 
2025
- themeToggle.addEventListener('click', function() {
2026
- document.body.classList.toggle('dark-mode');
2027
- var isDark = document.body.classList.contains('dark-mode');
2028
- localStorage.setItem('theme', isDark ? 'dark' : 'light');
2029
- updateIcon();
2030
- applyChartTheme();
2031
- setTimeout(function() { location.reload(); }, 300);
2032
- });
1350
+ if (themeToggle) {
1351
+ themeToggle.addEventListener('click', function() {
1352
+ var next = isDark() ? 'light' : 'dark';
1353
+ document.documentElement.setAttribute('data-theme', next);
1354
+ localStorage.setItem('red-theme', next);
1355
+ updateIcon();
1356
+ applyChartTheme();
1357
+ // Reload to ensure all chart instances pick up the new theme
1358
+ setTimeout(function() { location.reload(); }, 200);
1359
+ });
1360
+ }
2033
1361
 
2034
1362
  function applyChartTheme() {
2035
- if (typeof Chart !== 'undefined') {
2036
- var isDark = document.body.classList.contains('dark-mode');
2037
- var textColor = isDark ? '#cdd6f4' : '#1f2937';
2038
- var gridColor = isDark ? 'rgba(88, 91, 112, 0.2)' : 'rgba(0, 0, 0, 0.1)';
2039
-
2040
- Chart.defaults.color = textColor;
2041
- Chart.defaults.borderColor = gridColor;
2042
- Chart.defaults.font = Chart.defaults.font || {};
2043
- Chart.defaults.font.color = textColor;
2044
-
2045
- if (Chart.defaults.scale) {
2046
- Chart.defaults.scale.ticks = Chart.defaults.scale.ticks || {};
2047
- Chart.defaults.scale.ticks.color = textColor;
2048
- Chart.defaults.scale.grid = Chart.defaults.scale.grid || {};
2049
- Chart.defaults.scale.grid.color = gridColor;
2050
- Chart.defaults.scale.title = Chart.defaults.scale.title || {};
2051
- Chart.defaults.scale.title.color = textColor;
2052
- }
1363
+ if (typeof Chart === 'undefined') return;
1364
+ var dark = isDark();
1365
+ var textColor = dark ? '#cdd6f4' : '#1a1a2e';
1366
+ var gridColor = dark ? 'rgba(88, 91, 112, 0.2)' : 'rgba(0, 0, 0, 0.08)';
1367
+
1368
+ Chart.defaults.color = textColor;
1369
+ Chart.defaults.borderColor = gridColor;
1370
+ Chart.defaults.font = Chart.defaults.font || {};
1371
+ Chart.defaults.font.color = textColor;
1372
+ Chart.defaults.font.family = "'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif";
1373
+
1374
+ if (Chart.defaults.scales) {
1375
+ ['x', 'y'].forEach(function(axis) {
1376
+ Chart.defaults.scales[axis] = Chart.defaults.scales[axis] || {};
1377
+ Chart.defaults.scales[axis].ticks = Chart.defaults.scales[axis].ticks || {};
1378
+ Chart.defaults.scales[axis].ticks.color = textColor;
1379
+ Chart.defaults.scales[axis].grid = Chart.defaults.scales[axis].grid || {};
1380
+ Chart.defaults.scales[axis].grid.color = gridColor;
1381
+ });
1382
+ }
2053
1383
 
2054
- if (Chart.defaults.scales) {
2055
- Chart.defaults.scales.x = Chart.defaults.scales.x || {};
2056
- Chart.defaults.scales.x.ticks = Chart.defaults.scales.x.ticks || {};
2057
- Chart.defaults.scales.x.ticks.color = textColor;
2058
- Chart.defaults.scales.x.grid = Chart.defaults.scales.x.grid || {};
2059
- Chart.defaults.scales.x.grid.color = gridColor;
2060
-
2061
- Chart.defaults.scales.y = Chart.defaults.scales.y || {};
2062
- Chart.defaults.scales.y.ticks = Chart.defaults.scales.y.ticks || {};
2063
- Chart.defaults.scales.y.ticks.color = textColor;
2064
- Chart.defaults.scales.y.grid = Chart.defaults.scales.y.grid || {};
2065
- Chart.defaults.scales.y.grid.color = gridColor;
1384
+ if (Chart.defaults.plugins) {
1385
+ if (Chart.defaults.plugins.legend) {
1386
+ Chart.defaults.plugins.legend.labels = Chart.defaults.plugins.legend.labels || {};
1387
+ Chart.defaults.plugins.legend.labels.color = textColor;
2066
1388
  }
2067
-
2068
- if (Chart.defaults.plugins) {
2069
- if (Chart.defaults.plugins.legend) {
2070
- Chart.defaults.plugins.legend.labels = Chart.defaults.plugins.legend.labels || {};
2071
- Chart.defaults.plugins.legend.labels.color = textColor;
2072
- }
2073
- if (Chart.defaults.plugins.tooltip) {
2074
- Chart.defaults.plugins.tooltip.backgroundColor = isDark ? '#313244' : 'rgba(255, 255, 255, 0.95)';
2075
- Chart.defaults.plugins.tooltip.titleColor = isDark ? textColor : '#1f2937';
2076
- Chart.defaults.plugins.tooltip.bodyColor = isDark ? textColor : '#1f2937';
2077
- Chart.defaults.plugins.tooltip.borderColor = isDark ? '#585b70' : 'rgba(0, 0, 0, 0.2)';
2078
- Chart.defaults.plugins.tooltip.borderWidth = 1;
2079
- }
2080
- if (Chart.defaults.plugins.title) {
2081
- Chart.defaults.plugins.title.color = textColor;
2082
- }
1389
+ if (Chart.defaults.plugins.tooltip) {
1390
+ Chart.defaults.plugins.tooltip.backgroundColor = dark ? '#313244' : 'rgba(255, 255, 255, 0.95)';
1391
+ Chart.defaults.plugins.tooltip.titleColor = textColor;
1392
+ Chart.defaults.plugins.tooltip.bodyColor = textColor;
1393
+ Chart.defaults.plugins.tooltip.borderColor = dark ? '#585b70' : 'rgba(0, 0, 0, 0.15)';
1394
+ Chart.defaults.plugins.tooltip.borderWidth = 1;
2083
1395
  }
2084
1396
  }
2085
1397
  }
2086
1398
 
2087
1399
  applyChartTheme();
2088
1400
 
1401
+ // Ensure charts created after page load also get themed
2089
1402
  var observer = new MutationObserver(function(mutations) {
2090
1403
  mutations.forEach(function(mutation) {
2091
1404
  mutation.addedNodes.forEach(function(node) {
2092
- if (node.tagName === 'CANVAS') {
2093
- setTimeout(applyChartTheme, 100);
2094
- }
1405
+ if (node.tagName === 'CANVAS') { setTimeout(applyChartTheme, 100); }
2095
1406
  });
2096
1407
  });
2097
1408
  });
2098
-
2099
1409
  observer.observe(document.body, { childList: true, subtree: true });
2100
1410
 
2101
1411
  document.addEventListener('chartkick:load', function() { applyChartTheme(); });
2102
1412
 
1413
+ // Force-apply a few times on page load for lazy-loaded charts
2103
1414
  var attempts = 0;
2104
1415
  var forceInterval = setInterval(function() {
2105
1416
  attempts++;
@@ -2109,22 +1420,22 @@ document.addEventListener('DOMContentLoaded', function() {
2109
1420
  });
2110
1421
  </script>
2111
1422
 
2112
- <!-- Utilities -->
1423
+ <!-- Utilities (tooltips, clipboard, time conversion, keyboard shortcuts) -->
2113
1424
  <script>
2114
1425
  document.addEventListener('DOMContentLoaded', function() {
2115
1426
 
1427
+ // Tooltips
2116
1428
  function initializeTooltips() {
2117
1429
  var tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
2118
1430
  tooltipTriggerList.forEach(function(el) { new bootstrap.Tooltip(el); });
2119
1431
  }
2120
-
2121
1432
  initializeTooltips();
2122
-
2123
1433
  if (typeof Turbo !== 'undefined') {
2124
1434
  document.addEventListener('turbo:load', initializeTooltips);
2125
1435
  document.addEventListener('turbo:frame-load', initializeTooltips);
2126
1436
  }
2127
1437
 
1438
+ // Clipboard
2128
1439
  window.copyToClipboard = function(text, button) {
2129
1440
  navigator.clipboard.writeText(text).then(function() {
2130
1441
  var originalHTML = button.innerHTML;
@@ -2137,51 +1448,41 @@ document.addEventListener('DOMContentLoaded', function() {
2137
1448
  button.classList.remove('btn-success');
2138
1449
  button.classList.add('btn-outline-secondary');
2139
1450
  }, 2000);
2140
- }).catch(function(err) {
1451
+ }).catch(function() {
2141
1452
  button.innerHTML = '<i class="bi bi-x"></i> Failed';
2142
1453
  showToast('Failed to copy to clipboard', 'danger');
2143
1454
  });
2144
1455
  };
2145
1456
 
1457
+ // Toasts
2146
1458
  window.showToast = function(message, type) {
2147
1459
  type = type || 'success';
1460
+ var iconClass = type === 'success' ? 'bi-check-circle-fill' : type === 'danger' ? 'bi-exclamation-circle-fill' : 'bi-info-circle-fill';
1461
+ var bgClass = type === 'success' ? 'bg-success' : type === 'danger' ? 'bg-danger' : 'bg-info';
2148
1462
  var toastId = 'toast-' + Date.now();
2149
- var iconClass = type === 'success' ? 'bi-check-circle-fill' :
2150
- type === 'danger' ? 'bi-exclamation-circle-fill' :
2151
- 'bi-info-circle-fill';
2152
- var bgClass = type === 'success' ? 'bg-success' :
2153
- type === 'danger' ? 'bg-danger' :
2154
- 'bg-info';
2155
-
2156
1463
  var toastHTML =
2157
1464
  '<div id="' + toastId + '" class="toast align-items-center text-white ' + bgClass + ' border-0" role="alert" aria-live="assertive" aria-atomic="true">' +
2158
1465
  '<div class="d-flex">' +
2159
- '<div class="toast-body">' +
2160
- '<i class="bi ' + iconClass + ' me-2"></i>' +
2161
- message +
2162
- '</div>' +
1466
+ '<div class="toast-body"><i class="bi ' + iconClass + ' me-2"></i>' + message + '</div>' +
2163
1467
  '<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>' +
2164
1468
  '</div>' +
2165
1469
  '</div>';
2166
-
2167
1470
  var container = document.querySelector('.toast-container');
2168
- container.insertAdjacentHTML('beforeend', toastHTML);
2169
-
2170
- var toastElement = document.getElementById(toastId);
2171
- var toast = new bootstrap.Toast(toastElement, { delay: 4000 });
2172
- toast.show();
2173
-
2174
- toastElement.addEventListener('hidden.bs.toast', function() {
2175
- toastElement.remove();
2176
- });
1471
+ if (container) {
1472
+ container.insertAdjacentHTML('beforeend', toastHTML);
1473
+ var toastElement = document.getElementById(toastId);
1474
+ var toast = new bootstrap.Toast(toastElement, { delay: 4000 });
1475
+ toast.show();
1476
+ toastElement.addEventListener('hidden.bs.toast', function() { toastElement.remove(); });
1477
+ }
2177
1478
  };
2178
1479
 
1480
+ // Local time conversion
2179
1481
  function convertToLocalTime() {
2180
1482
  document.querySelectorAll('.local-time').forEach(function(element) {
2181
1483
  var utcString = element.dataset.utc;
2182
1484
  var formatString = element.dataset.format;
2183
1485
  if (!utcString) return;
2184
-
2185
1486
  try {
2186
1487
  var date = new Date(utcString);
2187
1488
  if (isNaN(date.getTime())) return;
@@ -2193,12 +1494,10 @@ document.addEventListener('DOMContentLoaded', function() {
2193
1494
  element.dataset.originalUtc = utcString;
2194
1495
  element.dataset.localFormatted = formatted + ' ' + timezone;
2195
1496
  element.dataset.showingLocal = 'true';
2196
-
2197
1497
  element.addEventListener('click', function() {
2198
1498
  if (this.dataset.showingLocal === 'true') {
2199
1499
  var utcDate = new Date(this.dataset.originalUtc);
2200
- var utcFormatted = formatDateTime(utcDate, formatString);
2201
- this.textContent = utcFormatted + ' UTC';
1500
+ this.textContent = formatDateTime(utcDate, formatString) + ' UTC';
2202
1501
  this.title = 'UTC time (click to see local time)';
2203
1502
  this.dataset.showingLocal = 'false';
2204
1503
  } else {
@@ -2213,31 +1512,22 @@ document.addEventListener('DOMContentLoaded', function() {
2213
1512
  document.querySelectorAll('.local-time-ago').forEach(function(element) {
2214
1513
  var utcString = element.dataset.utc;
2215
1514
  if (!utcString) return;
2216
-
2217
1515
  try {
2218
1516
  var date = new Date(utcString);
2219
1517
  if (isNaN(date.getTime())) return;
2220
- var now = new Date();
2221
- var diffMs = now - date;
2222
- var formatted = formatRelativeTime(diffMs);
2223
- element.textContent = formatted;
1518
+ element.textContent = formatRelativeTime(new Date() - date);
2224
1519
  element.title = 'Click to see exact time';
2225
1520
  element.style.cursor = 'pointer';
2226
1521
  element.dataset.originalUtc = utcString;
2227
1522
  element.dataset.showingRelative = 'true';
2228
-
2229
1523
  element.addEventListener('click', function() {
2230
1524
  if (this.dataset.showingRelative === 'true') {
2231
- var absoluteDate = new Date(this.dataset.originalUtc);
2232
- var absoluteFormatted = formatDateTime(absoluteDate, '%B %d, %Y %I:%M:%S %p');
2233
- var tz = getTimezoneAbbreviation(absoluteDate);
2234
- this.textContent = absoluteFormatted + ' ' + tz;
1525
+ var d = new Date(this.dataset.originalUtc);
1526
+ this.textContent = formatDateTime(d, '%B %d, %Y %I:%M:%S %p') + ' ' + getTimezoneAbbreviation(d);
2235
1527
  this.title = 'Click to see relative time';
2236
1528
  this.dataset.showingRelative = 'false';
2237
1529
  } else {
2238
- var now = new Date();
2239
- var d = new Date(this.dataset.originalUtc);
2240
- this.textContent = formatRelativeTime(now - d);
1530
+ this.textContent = formatRelativeTime(new Date() - new Date(this.dataset.originalUtc));
2241
1531
  this.title = 'Click to see exact time';
2242
1532
  this.dataset.showingRelative = 'true';
2243
1533
  }
@@ -2246,109 +1536,112 @@ document.addEventListener('DOMContentLoaded', function() {
2246
1536
  });
2247
1537
  }
2248
1538
 
2249
- function formatDateTime(date, formatString) {
2250
- var months = ['January', 'February', 'March', 'April', 'May', 'June',
2251
- 'July', 'August', 'September', 'October', 'November', 'December'];
2252
- var monthsShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
2253
- 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
2254
- var days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
2255
- var daysShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
2256
-
2257
- var year = date.getFullYear();
2258
- var month = date.getMonth();
2259
- var day = date.getDate();
2260
- var hours = date.getHours();
2261
- var minutes = date.getMinutes();
2262
- var seconds = date.getSeconds();
2263
- var dayOfWeek = date.getDay();
2264
- var hours12 = hours % 12 || 12;
2265
- var ampm = hours >= 12 ? 'PM' : 'AM';
1539
+ function formatDateTime(date, fmt) {
1540
+ var months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
1541
+ var monthsShort = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
1542
+ var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
1543
+ var daysShort = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
1544
+ var y = date.getFullYear(), mo = date.getMonth(), d = date.getDate();
1545
+ var h = date.getHours(), mi = date.getMinutes(), s = date.getSeconds(), dow = date.getDay();
1546
+ var h12 = h % 12 || 12, ampm = h >= 12 ? 'PM' : 'AM';
2266
1547
  var pad = function(n) { return n.toString().padStart(2, '0'); };
2267
-
2268
- return formatString
2269
- .replace('%Y', year)
2270
- .replace('%y', year.toString().substr(2))
2271
- .replace('%B', months[month])
2272
- .replace('%b', monthsShort[month])
2273
- .replace('%m', pad(month + 1))
2274
- .replace('%d', pad(day))
2275
- .replace('%e', day)
2276
- .replace('%A', days[dayOfWeek])
2277
- .replace('%a', daysShort[dayOfWeek])
2278
- .replace('%H', pad(hours))
2279
- .replace('%I', pad(hours12))
2280
- .replace('%M', pad(minutes))
2281
- .replace('%S', pad(seconds))
2282
- .replace('%p', ampm)
2283
- .replace('%P', ampm.toLowerCase());
1548
+ return fmt.replace('%Y',y).replace('%y',y.toString().substr(2)).replace('%B',months[mo]).replace('%b',monthsShort[mo])
1549
+ .replace('%m',pad(mo+1)).replace('%d',pad(d)).replace('%e',d).replace('%A',days[dow]).replace('%a',daysShort[dow])
1550
+ .replace('%H',pad(h)).replace('%I',pad(h12)).replace('%M',pad(mi)).replace('%S',pad(s)).replace('%p',ampm).replace('%P',ampm.toLowerCase());
2284
1551
  }
2285
1552
 
2286
1553
  function getTimezoneAbbreviation(date) {
2287
- var timeZoneString = date.toLocaleTimeString('en-US', { timeZoneName: 'short' });
2288
- var parts = timeZoneString.split(' ');
1554
+ var parts = date.toLocaleTimeString('en-US', { timeZoneName: 'short' }).split(' ');
2289
1555
  return parts[parts.length - 1];
2290
1556
  }
2291
1557
 
2292
1558
  function formatRelativeTime(diffMs) {
2293
- var seconds = Math.floor(diffMs / 1000);
2294
- var minutes = Math.floor(seconds / 60);
2295
- var hours = Math.floor(minutes / 60);
2296
- var days = Math.floor(hours / 24);
2297
- var months = Math.floor(days / 30);
2298
- var years = Math.floor(days / 365);
2299
-
2300
- if (seconds < 60) return seconds <= 1 ? '1 second ago' : seconds + ' seconds ago';
2301
- if (minutes < 60) return minutes === 1 ? '1 minute ago' : minutes + ' minutes ago';
2302
- if (hours < 24) return hours === 1 ? '1 hour ago' : hours + ' hours ago';
2303
- if (days < 30) return days === 1 ? '1 day ago' : days + ' days ago';
2304
- if (months < 12) return months === 1 ? '1 month ago' : months + ' months ago';
2305
- return years === 1 ? '1 year ago' : years + ' years ago';
1559
+ var sec = Math.floor(diffMs / 1000), min = Math.floor(sec / 60), hr = Math.floor(min / 60), d = Math.floor(hr / 24), mo = Math.floor(d / 30), yr = Math.floor(d / 365);
1560
+ if (sec < 60) return sec <= 1 ? '1 second ago' : sec + ' seconds ago';
1561
+ if (min < 60) return min === 1 ? '1 minute ago' : min + ' minutes ago';
1562
+ if (hr < 24) return hr === 1 ? '1 hour ago' : hr + ' hours ago';
1563
+ if (d < 30) return d === 1 ? '1 day ago' : d + ' days ago';
1564
+ if (mo < 12) return mo === 1 ? '1 month ago' : mo + ' months ago';
1565
+ return yr === 1 ? '1 year ago' : yr + ' years ago';
2306
1566
  }
2307
1567
 
2308
1568
  convertToLocalTime();
2309
-
2310
1569
  if (typeof Turbo !== 'undefined') {
2311
1570
  document.addEventListener('turbo:load', convertToLocalTime);
2312
1571
  document.addEventListener('turbo:frame-load', convertToLocalTime);
2313
1572
  }
1573
+
1574
+ // Keyboard Shortcuts
1575
+ document.addEventListener('keydown', function(e) {
1576
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
1577
+ var key = e.key;
1578
+ if (key === '?' || (key === '/' && e.shiftKey)) {
1579
+ e.preventDefault();
1580
+ var modal = document.getElementById('keyboardShortcutsModal');
1581
+ if (modal) { new bootstrap.Modal(modal).show(); }
1582
+ } else if (key === '/') {
1583
+ e.preventDefault();
1584
+ var search = document.getElementById('globalSearch');
1585
+ if (search) search.focus();
1586
+ } else if (key === 'r' || key === 'R') {
1587
+ e.preventDefault();
1588
+ location.reload();
1589
+ } else if (key === 'a' || key === 'A') {
1590
+ e.preventDefault();
1591
+ window.location.href = '<%= analytics_errors_path %>';
1592
+ }
1593
+ });
2314
1594
  });
2315
1595
  </script>
2316
1596
 
2317
- <!-- Sidebar Toggle -->
1597
+ <!-- Sidebar Toggle + Nav Section Toggle -->
2318
1598
  <script>
2319
1599
  document.addEventListener('DOMContentLoaded', function() {
2320
1600
  var sidebarToggle = document.getElementById('sidebarToggle');
2321
1601
  var sidebar = document.getElementById('sidebar');
2322
- var mainContent = document.getElementById('mainContent');
2323
- var STORAGE_KEY = 'rails_error_dashboard_sidebar_collapsed';
1602
+ var STORAGE_KEY = 'red_sidebar_collapsed';
2324
1603
 
2325
- if (!sidebarToggle || !sidebar || !mainContent) return;
2326
-
2327
- var isCollapsed = localStorage.getItem(STORAGE_KEY) === 'true';
2328
- if (isCollapsed) {
2329
- sidebar.classList.add('sidebar-collapsed');
2330
- mainContent.classList.add('content-expanded');
2331
- }
2332
-
2333
- sidebarToggle.addEventListener('click', function() {
2334
- var willBeCollapsed = !sidebar.classList.contains('sidebar-collapsed');
2335
- sidebar.classList.toggle('sidebar-collapsed');
2336
- mainContent.classList.toggle('content-expanded');
2337
- localStorage.setItem(STORAGE_KEY, willBeCollapsed);
2338
-
2339
- var icon = sidebarToggle.querySelector('i');
2340
- if (icon) {
2341
- icon.style.transform = 'rotate(180deg)';
2342
- setTimeout(function() { icon.style.transform = ''; }, 200);
1604
+ if (sidebarToggle && sidebar) {
1605
+ if (localStorage.getItem(STORAGE_KEY) === 'true') {
1606
+ sidebar.classList.add('collapsed');
2343
1607
  }
2344
- });
2345
1608
 
2346
- document.addEventListener('keydown', function(e) {
2347
- if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA' && !e.target.isContentEditable) {
1609
+ sidebarToggle.addEventListener('click', function() {
1610
+ sidebar.classList.toggle('collapsed');
1611
+ localStorage.setItem(STORAGE_KEY, sidebar.classList.contains('collapsed'));
1612
+ });
1613
+
1614
+ document.addEventListener('keydown', function(e) {
1615
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
2348
1616
  if (e.key === 's' || e.key === 'S') {
2349
1617
  e.preventDefault();
2350
1618
  sidebarToggle.click();
2351
1619
  }
1620
+ });
1621
+ }
1622
+ });
1623
+
1624
+ // Nav section collapse/expand
1625
+ function toggleNavSection(itemsId) {
1626
+ var items = document.getElementById(itemsId);
1627
+ var chevron = document.getElementById(itemsId.replace('Items', 'Chevron'));
1628
+ if (items) {
1629
+ var hidden = items.style.display === 'none';
1630
+ items.style.display = hidden ? '' : 'none';
1631
+ if (chevron) chevron.className = hidden ? 'bi bi-chevron-down' : 'bi bi-chevron-right';
1632
+ localStorage.setItem('red_nav_' + itemsId, hidden ? 'open' : 'closed');
1633
+ }
1634
+ }
1635
+
1636
+ // Restore nav section states
1637
+ document.addEventListener('DOMContentLoaded', function() {
1638
+ ['navHealthItems', 'navDiagItems', 'navInsightsItems'].forEach(function(id) {
1639
+ var state = localStorage.getItem('red_nav_' + id);
1640
+ if (state === 'closed') {
1641
+ var items = document.getElementById(id);
1642
+ var chevron = document.getElementById(id.replace('Items', 'Chevron'));
1643
+ if (items) items.style.display = 'none';
1644
+ if (chevron) chevron.className = 'bi bi-chevron-right';
2352
1645
  }
2353
1646
  });
2354
1647
  });
@@ -2356,7 +1649,6 @@ document.addEventListener('DOMContentLoaded', function() {
2356
1649
 
2357
1650
  <!-- Flash Messages -->
2358
1651
  <script>
2359
- // Show flash messages as toasts
2360
1652
  <% if defined?(flash) && flash.present? %>
2361
1653
  <% if flash[:notice] %>
2362
1654
  showToast('<%= j flash[:notice] %>', 'success');