rails_error_dashboard 0.5.15 → 0.6.0

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/rails_error_dashboard/errors_controller.rb +31 -26
  3. data/app/helpers/rails_error_dashboard/application_helper.rb +12 -5
  4. data/app/views/layouts/rails_error_dashboard.html.erb +1217 -1935
  5. data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +4 -4
  6. data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +1 -1
  7. data/app/views/rails_error_dashboard/errors/_discussion.html.erb +3 -3
  8. data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +1 -1
  9. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +69 -79
  10. data/app/views/rails_error_dashboard/errors/_instance_variables.html.erb +1 -1
  11. data/app/views/rails_error_dashboard/errors/_issue_section.html.erb +1 -1
  12. data/app/views/rails_error_dashboard/errors/_local_variables.html.erb +1 -1
  13. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +2 -2
  14. data/app/views/rails_error_dashboard/errors/_request_context.html.erb +1 -1
  15. data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +1 -1
  16. data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +1 -1
  17. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +1 -1
  18. data/app/views/rails_error_dashboard/errors/actioncable_health_summary.html.erb +6 -6
  19. data/app/views/rails_error_dashboard/errors/activestorage_health_summary.html.erb +6 -6
  20. data/app/views/rails_error_dashboard/errors/analytics.html.erb +34 -50
  21. data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +7 -7
  22. data/app/views/rails_error_dashboard/errors/correlation.html.erb +11 -11
  23. data/app/views/rails_error_dashboard/errors/database_health_summary.html.erb +114 -172
  24. data/app/views/rails_error_dashboard/errors/deprecations.html.erb +7 -7
  25. data/app/views/rails_error_dashboard/errors/diagnostic_dumps.html.erb +6 -6
  26. data/app/views/rails_error_dashboard/errors/index.html.erb +292 -620
  27. data/app/views/rails_error_dashboard/errors/job_health_summary.html.erb +7 -7
  28. data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +7 -7
  29. data/app/views/rails_error_dashboard/errors/overview.html.erb +192 -363
  30. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +11 -11
  31. data/app/views/rails_error_dashboard/errors/rack_attack_summary.html.erb +6 -6
  32. data/app/views/rails_error_dashboard/errors/releases.html.erb +6 -6
  33. data/app/views/rails_error_dashboard/errors/settings.html.erb +32 -52
  34. data/app/views/rails_error_dashboard/errors/show.html.erb +200 -203
  35. data/app/views/rails_error_dashboard/errors/swallowed_exceptions.html.erb +7 -7
  36. data/app/views/rails_error_dashboard/errors/user_impact.html.erb +6 -6
  37. data/lib/rails_error_dashboard/configuration.rb +6 -0
  38. data/lib/rails_error_dashboard/version.rb +1 -1
  39. metadata +2 -2
@@ -1,1651 +1,1008 @@
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
- <meta name="viewport" content="width=device-width,initial-scale=1">
6
- <meta name="turbo-visit-control" content="reload">
7
- <% if respond_to?(:csrf_meta_tags) %>
8
- <%= csrf_meta_tags %>
9
- <% end %>
10
- <% if respond_to?(:csp_meta_tag) %>
11
- <%= csp_meta_tag %>
12
- <% end %>
13
-
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
- }
1204
-
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
- }
1214
-
1215
- [data-theme="dark"] .timeline-item::before,
1216
- body.dark-mode .timeline-item::before {
1217
- background: var(--border-color);
1218
- }
1219
-
1220
- .timeline-item-last::before {
1221
- display: none;
1222
- }
1223
-
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
- }
32
+ <meta name="viewport" content="width=device-width,initial-scale=1">
33
+ <meta name="turbo-visit-control" content="reload">
34
+ <% if respond_to?(:csrf_meta_tags) %>
35
+ <%= csrf_meta_tags %>
36
+ <% end %>
37
+ <% if respond_to?(:csp_meta_tag) %>
38
+ <%= csp_meta_tag %>
39
+ <% end %>
1238
40
 
1239
- [data-theme="dark"] .timeline-marker,
1240
- body.dark-mode .timeline-marker {
1241
- background: var(--ctp-surface0);
1242
- border-color: var(--border-color);
1243
- }
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>
1244
50
 
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
- }
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">
1250
56
 
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
- }
57
+ <!-- Bootstrap Icons -->
58
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
1257
59
 
1258
- .timeline-marker i {
1259
- font-size: 12px;
1260
- }
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>
1261
64
 
1262
- .timeline-content {
1263
- background: #ffffff;
1264
- padding: 12px;
1265
- border-radius: 8px;
1266
- border: 1px solid #dee2e6;
1267
- }
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>
1268
69
 
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
- }
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: #cdd6f4;
139
+ --text-secondary: #a6adc8;
140
+ --text-tertiary: #6c7086;
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); }
1275
183
 
1276
- .timeline-item:hover .timeline-content {
1277
- border-color: #0d6efd;
1278
- background: #cfe2ff;
1279
- }
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 */
283
+ .d-none { display: none; }
284
+ .d-block { display: block; }
285
+ .d-inline { display: inline; }
286
+ .d-inline-block { display: inline-block; }
287
+ @media (min-width: 576px) { .d-sm-inline { display: inline; } .d-sm-block { display: block; } }
288
+ @media (min-width: 768px) { .d-md-block { display: block; } .d-md-none { display: none; } .d-md-flex { display: flex; } .d-md-inline { display: inline; } .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-tertiary); }
292
+ .text-secondary { color: var(--text-secondary); }
293
+ .text-center { text-align: center; }
294
+ .text-end { text-align: right; }
295
+ .text-start { text-align: left; }
296
+ .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
297
+ .text-nowrap { white-space: nowrap; }
298
+ .text-decoration-none { text-decoration: none; }
299
+ .text-white { color: #fff; }
300
+ .text-white-50 { color: rgba(255,255,255,0.5); }
301
+ .text-danger { color: var(--status-critical); }
302
+ .text-success { color: var(--status-success); }
303
+ .text-warning { color: var(--status-warning); }
304
+ .text-info { color: var(--status-info); }
305
+ .text-primary { color: var(--accent); }
306
+ .fw-bold { font-weight: 700; }
307
+ .fw-semibold { font-weight: 600; }
308
+ .fs-4 { font-size: 1.25rem; }
309
+ .fs-5 { font-size: 1.1rem; }
310
+ .small, small { font-size: 0.85em; }
311
+ .font-monospace { font-family: var(--font-mono); }
312
+ .text-uppercase { text-transform: uppercase; }
313
+
314
+ /* Backgrounds */
315
+ .bg-white { background: var(--surface-primary); }
316
+ .bg-light { background: var(--surface-secondary); }
317
+ .bg-danger { background: var(--status-critical); color: #fff; }
318
+ .bg-success { background: var(--status-success); color: #fff; }
319
+ .bg-warning { background: var(--status-warning); color: #fff; }
320
+ .bg-info { background: var(--status-info); color: #fff; }
321
+ .bg-secondary { background: var(--text-tertiary); color: #fff; }
322
+ .bg-primary { background: var(--accent); color: #fff; }
323
+ .bg-transparent { background: transparent; }
324
+
325
+ /* Borders & misc */
326
+ .border { border: 1px solid var(--border-primary); }
327
+ .border-top { border-top: 1px solid var(--border-primary); }
328
+ .border-bottom { border-bottom: 1px solid var(--border-primary); }
329
+ .border-start { border-left: 1px solid var(--border-primary); }
330
+ .rounded { border-radius: var(--radius-md); }
331
+ .rounded-circle { border-radius: 50%; }
332
+ .shadow-sm { box-shadow: var(--shadow-sm); }
333
+ .h-100 { height: 100%; }
334
+ .w-100 { width: 100%; }
335
+ .overflow-auto { overflow: auto; }
336
+ .overflow-hidden { overflow: hidden; }
337
+ .position-fixed { position: fixed; }
338
+ .position-sticky { position: sticky; }
339
+ .position-relative { position: relative; }
340
+ .position-absolute { position: absolute; }
341
+ .top-0 { top: 0; }
342
+ .end-0 { right: 0; }
343
+ .img-fluid { max-width: 100%; height: auto; }
344
+ .visually-hidden { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0,0,0,0); }
345
+
346
+ /* ============================================
347
+ Components
348
+ ============================================ */
349
+
350
+ /* Card */
351
+ .card { background: var(--surface-primary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); transition: box-shadow var(--transition-normal); }
352
+ .card-body { padding: var(--space-5); }
353
+ .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; }
354
+ .card-footer { padding: var(--space-3) var(--space-5); border-top: 1px solid var(--border-primary); }
355
+ .card-title { font-size: 15px; font-weight: 600; margin-bottom: var(--space-2); }
356
+
357
+ /* Stat Card */
358
+ .stat-card { border-radius: var(--radius-md); transition: transform var(--transition-normal), box-shadow var(--transition-normal); }
359
+ .stat-card:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
360
+ .stat-label { font-size: 12px; font-weight: 500; color: var(--text-tertiary); text-transform: uppercase; letter-spacing: 0.05em; }
361
+ .stat-value { font-size: 28px; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--text-primary); }
362
+
363
+ /* Badge */
364
+ .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); }
365
+ .badge.bg-danger { background: var(--status-critical-bg); color: var(--status-critical); }
366
+ .badge.bg-warning { background: var(--status-warning-bg); color: var(--status-warning); }
367
+ .badge.bg-info { background: var(--status-info-bg); color: var(--status-info); }
368
+ .badge.bg-success { background: var(--status-success-bg); color: var(--status-success); }
369
+ .badge.bg-secondary { background: var(--surface-tertiary); color: var(--text-secondary); }
370
+ .badge.bg-primary { background: var(--accent-subtle); color: var(--accent); }
371
+
372
+ /* Platform badges */
373
+ .badge-ios { background: var(--surface-tertiary); color: var(--text-primary); }
374
+ .badge-android { background: var(--status-success-bg); color: var(--status-success); }
375
+ .badge-web { background: var(--status-info-bg); color: var(--status-info); }
376
+ .badge-api { background: var(--accent-subtle); color: var(--accent); }
1280
377
 
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);
1285
- }
378
+ /* Buttons */
379
+ .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; }
380
+ .btn:hover { background: var(--surface-hover); color: var(--text-primary); text-decoration: none; }
381
+ .btn:disabled, .btn.disabled { opacity: 0.5; pointer-events: none; }
382
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
383
+ .btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
384
+ .btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); color: #fff; }
385
+ .btn-danger { background: var(--status-critical); border-color: var(--status-critical); color: #fff; }
386
+ .btn-danger:hover { opacity: 0.9; color: #fff; }
387
+ .btn-success { background: var(--status-success); border-color: var(--status-success); color: #fff; }
388
+ .btn-success:hover { opacity: 0.9; color: #fff; }
389
+ .btn-secondary { background: var(--surface-secondary); border-color: var(--border-primary); color: var(--text-secondary); }
390
+ .btn-secondary:hover { background: var(--surface-tertiary); }
391
+ .btn-outline-primary { background: transparent; border-color: var(--accent); color: var(--accent); }
392
+ .btn-outline-primary:hover { background: var(--accent); color: #fff; }
393
+ .btn-outline-secondary { background: transparent; border-color: var(--border-primary); color: var(--text-secondary); }
394
+ .btn-outline-secondary:hover { background: var(--surface-hover); }
395
+ .btn-outline-danger { background: transparent; border-color: var(--status-critical); color: var(--status-critical); }
396
+ .btn-outline-danger:hover { background: var(--status-critical); color: #fff; }
397
+ .btn-link { background: none; border: none; color: var(--accent); padding: 0; }
398
+ .btn-link:hover { color: var(--accent-hover); }
399
+ .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); }
400
+ .btn-close:hover { background: var(--surface-secondary); color: var(--text-primary); }
401
+ .btn-close::before { content: '\00d7'; }
402
+ .btn-close-white { color: rgba(255,255,255,0.7); }
403
+ .btn-close-white:hover { color: #fff; }
1286
404
 
1287
- /* Collapsible Sidebar */
1288
- .sidebar {
1289
- transition: all 0.3s ease-in-out;
1290
- overflow: hidden;
405
+ /* Alerts */
406
+ .alert { padding: var(--space-4); border-radius: var(--radius-md); border: 1px solid var(--border-primary); margin-bottom: var(--space-4); font-size: 13px; }
407
+ .alert-danger { background: var(--status-critical-bg); border-color: var(--status-critical); color: var(--status-critical); }
408
+ .alert-warning { background: var(--status-warning-bg); border-color: var(--status-warning); color: var(--status-warning); }
409
+ .alert-success { background: var(--status-success-bg); border-color: var(--status-success); color: var(--status-success); }
410
+ .alert-info { background: var(--status-info-bg); border-color: var(--status-info); color: var(--status-info); }
411
+ .alert-secondary { background: var(--surface-secondary); border-color: var(--border-primary); color: var(--text-secondary); }
412
+ .alert code { color: inherit; background: rgba(0,0,0,0.06); }
413
+
414
+ /* Tables — polished */
415
+ .table { width: 100%; border-collapse: collapse; font-size: 13px; color: var(--text-primary); }
416
+ .table th {
417
+ padding: var(--space-3) var(--space-4); text-align: left;
418
+ font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em;
419
+ color: var(--text-tertiary); border-bottom: 2px solid var(--border-primary);
420
+ background: var(--surface-primary); white-space: nowrap;
421
+ }
422
+ .table td {
423
+ padding: var(--space-3) var(--space-4);
424
+ border-bottom: 1px solid var(--border-secondary);
425
+ vertical-align: middle;
1291
426
  }
427
+ .table tbody tr { transition: all 0.12s ease; position: relative; }
428
+ .table tbody tr:last-child td { border-bottom: none; }
429
+ .table-hover tbody tr { cursor: pointer; }
430
+ .table-hover tbody tr:hover { background: var(--surface-hover); }
431
+ .table-hover tbody tr:hover td:first-child { box-shadow: inset 3px 0 0 var(--accent); }
432
+ .table-hover tbody tr:active { background: var(--accent-subtle); }
433
+ .table-striped tbody tr:nth-child(even) { background: var(--surface-base); }
434
+ .table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; }
435
+ .table-responsive thead th { position: sticky; top: 0; z-index: 10; background: var(--surface-primary); }
436
+ .table-responsive::-webkit-scrollbar { height: 4px; }
437
+ .table-responsive::-webkit-scrollbar-thumb { background: var(--border-primary); border-radius: 2px; }
1292
438
 
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;
1299
- }
439
+ /* Forms */
440
+ .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); }
441
+ .form-control:focus, .form-select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-subtle); }
442
+ .form-control::placeholder { color: var(--text-tertiary); }
443
+ .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; }
444
+ .form-check { display: flex; align-items: center; gap: 6px; }
445
+ .form-check-input { width: 16px; height: 16px; accent-color: var(--accent); }
446
+ .form-check-label { font-size: 13px; }
447
+ .form-label { font-size: 13px; font-weight: 500; margin-bottom: var(--space-1); display: block; }
448
+ .input-group { display: flex; }
449
+ .input-group .form-control { border-radius: var(--radius-sm) 0 0 var(--radius-sm); }
450
+ .input-group .btn { border-radius: 0 var(--radius-sm) var(--radius-sm) 0; }
451
+
452
+ /* Modal — polished */
453
+ @keyframes modalSlideIn { from { opacity: 0; transform: translateY(-12px) scale(0.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
454
+ .modal { position: fixed; top: 0; left: 0; z-index: 1050; display: none; width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; outline: 0; }
455
+ .modal.show { display: block; }
456
+ .modal-dialog { position: relative; width: auto; margin: 1.75rem auto; max-width: 500px; pointer-events: none; animation: modalSlideIn 0.2s ease-out; }
457
+ .modal-dialog-centered { display: flex; align-items: center; min-height: calc(100% - 3.5rem); }
458
+ .modal-content {
459
+ position: relative; display: flex; flex-direction: column; width: 100%;
460
+ pointer-events: auto; background: var(--surface-primary);
461
+ border: 1px solid var(--border-primary); border-radius: var(--radius-lg);
462
+ box-shadow: 0 16px 48px rgba(0,0,0,0.12), 0 4px 12px rgba(0,0,0,0.08);
463
+ }
464
+ [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); }
465
+ .modal-header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-6) var(--space-6) var(--space-4); border-bottom: none; }
466
+ .modal-title { font-size: 17px; font-weight: 700; color: var(--text-primary); }
467
+ .modal-body { padding: 0 var(--space-6) var(--space-5); }
468
+ .modal-body .form-control, .modal-body .form-select, .modal-body textarea {
469
+ border-radius: var(--radius-md); padding: 10px 14px; font-size: 14px;
470
+ transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
471
+ }
472
+ .modal-body .form-control:focus, .modal-body textarea:focus {
473
+ border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-subtle);
474
+ }
475
+ .modal-body label, .modal-body .form-label { font-size: 13px; font-weight: 600; color: var(--text-primary); margin-bottom: var(--space-2); }
476
+ .modal-body small, .modal-body .form-text { font-size: 12px; color: var(--text-tertiary); margin-top: var(--space-1); }
477
+ .modal-footer {
478
+ display: flex; justify-content: flex-end; gap: var(--space-3);
479
+ padding: var(--space-4) var(--space-6) var(--space-6); border-top: none;
480
+ }
481
+ .modal-footer .btn { min-width: 100px; justify-content: center; padding: 9px 20px; font-size: 14px; font-weight: 600; border-radius: var(--radius-md); }
482
+ .modal-footer .btn-primary, .modal-footer .btn-success { box-shadow: 0 1px 3px rgba(0,0,0,0.1); }
483
+ .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); }
484
+ .modal-backdrop.show { opacity: 1; }
485
+ .fade { transition: opacity 0.15s linear; opacity: 0; }
486
+ .fade.show { opacity: 1; }
487
+
488
+ /* Accordion (Bootstrap JS support) */
489
+ .accordion { border-radius: var(--radius-md); overflow: hidden; }
490
+ .accordion-item { background: var(--surface-primary); border: 1px solid var(--border-primary); }
491
+ .accordion-item + .accordion-item { border-top: 0; }
492
+ .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); }
493
+ .accordion-button:hover { background: var(--surface-secondary); }
494
+ .accordion-button::after { content: '\25BE'; margin-left: auto; transition: transform var(--transition-normal); }
495
+ .accordion-button.collapsed::after { transform: rotate(-90deg); }
496
+ .accordion-body { padding: var(--space-5); background: var(--surface-primary); }
497
+ .accordion-collapse { display: none; }
498
+ .accordion-collapse.show { display: block; }
499
+ .collapse:not(.show) { display: none; }
500
+
501
+ /* Dropdown (Bootstrap JS support) */
502
+ .dropdown { position: relative; }
503
+ .dropdown-toggle::after { content: '\25BE'; margin-left: 4px; font-size: 10px; }
504
+ .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); }
505
+ .dropdown-menu.show { display: block; }
506
+ .dropdown-menu-end { right: 0; left: auto; }
507
+ .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; }
508
+ .dropdown-item:hover, .dropdown-item:focus { background: var(--surface-hover); color: var(--text-primary); }
509
+ .dropdown-item.active { background: var(--accent-subtle); color: var(--accent); }
510
+ .dropdown-divider { margin: var(--space-2) 0; border-top: 1px solid var(--border-primary); }
511
+
512
+ /* Offcanvas (Bootstrap JS support — mobile sidebar) */
513
+ .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; }
514
+ .offcanvas-start { left: 0; transform: translateX(-100%); }
515
+ .offcanvas.show { transform: translateX(0); }
516
+ .offcanvas-header { display: flex; align-items: center; justify-content: space-between; padding: var(--space-5); border-bottom: 1px solid var(--border-primary); }
517
+ .offcanvas-body { flex-grow: 1; padding: var(--space-4); overflow-y: auto; }
518
+ .offcanvas-backdrop { position: fixed; top: 0; left: 0; z-index: 1040; width: 100vw; height: 100vh; background: rgba(0,0,0,0.5); }
519
+
520
+ /* Nav */
521
+ .nav { display: flex; flex-direction: column; list-style: none; padding: 0; margin: 0; }
522
+ .nav-item { }
523
+ .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; }
524
+ .nav-link i { font-size: 15px; width: 20px; text-align: center; }
525
+ .nav-link:hover { background: var(--surface-hover); color: var(--text-primary); text-decoration: none; }
526
+ .nav-link.active { color: var(--accent); background: var(--accent-subtle); border-right-color: var(--accent); font-weight: 600; }
527
+
528
+ /* Tooltip (Bootstrap JS support) */
529
+ .tooltip { position: absolute; z-index: 1080; display: block; font-size: 12px; }
530
+ .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); }
1300
531
 
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
- }
532
+ /* Pagination */
533
+ .pagination { display: flex; gap: 2px; list-style: none; padding: 0; margin: 0; }
534
+ .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; }
535
+ .page-link:hover { background: var(--surface-hover); color: var(--text-primary); }
536
+ .page-item.active .page-link { background: var(--accent); border-color: var(--accent); color: #fff; }
537
+ .page-item.disabled .page-link { opacity: 0.4; pointer-events: none; }
538
+
539
+ /* Progress */
540
+ .progress { height: 6px; background: var(--surface-secondary); border-radius: var(--radius-full); overflow: hidden; }
541
+ .progress-bar { height: 100%; background: var(--accent); border-radius: var(--radius-full); }
542
+
543
+ /* Breadcrumb */
544
+ .breadcrumb { display: flex; flex-wrap: wrap; padding: 0; margin: 0; list-style: none; font-size: 13px; }
545
+ .breadcrumb-item + .breadcrumb-item::before { content: '/'; padding: 0 var(--space-2); color: var(--text-tertiary); }
546
+ .breadcrumb-item a { color: var(--text-tertiary); }
547
+ .breadcrumb-item a:hover { color: var(--accent); }
548
+ .breadcrumb-item.active { color: var(--text-secondary); }
549
+
550
+ /* List group */
551
+ .list-group { display: flex; flex-direction: column; padding: 0; margin: 0; list-style: none; }
552
+ .list-group-item { padding: var(--space-3) var(--space-4); background: var(--surface-primary); border: 1px solid var(--border-primary); color: var(--text-primary); }
553
+ .list-group-item + .list-group-item { border-top: 0; }
554
+ .list-group-item:first-child { border-top-left-radius: var(--radius-md); border-top-right-radius: var(--radius-md); }
555
+ .list-group-item:last-child { border-bottom-left-radius: var(--radius-md); border-bottom-right-radius: var(--radius-md); }
556
+ .list-group-flush .list-group-item { border-left: 0; border-right: 0; border-radius: 0; }
557
+
558
+ /* Toast */
559
+ .toast { min-width: 250px; padding: var(--space-3) var(--space-4); border-radius: var(--radius-md); color: #fff; display: none; }
560
+ .toast.show { display: flex; }
561
+ .toast-container { max-width: 350px; display: flex; flex-direction: column; gap: var(--space-2); }
562
+ .toast-body { flex: 1; }
563
+
564
+ /* ============================================
565
+ Layout Shell
566
+ ============================================ */
1307
567
 
1308
- /* Hide sidebar content when collapsed */
1309
- .sidebar.sidebar-collapsed .position-sticky {
1310
- opacity: 0;
1311
- transition: opacity 0.2s ease-in-out;
1312
- }
568
+ /* Navbar */
569
+ .red-navbar {
570
+ display: flex; align-items: center; justify-content: space-between;
571
+ padding: 0 var(--space-6);
572
+ height: 48px; flex-shrink: 0;
573
+ background: var(--surface-primary);
574
+ border-bottom: 1px solid var(--border-primary);
575
+ }
576
+ .red-search-input {
577
+ width: 240px; padding: 5px 10px 5px 30px; font-size: 13px;
578
+ border: 1px solid var(--border-primary); border-radius: var(--radius-sm);
579
+ background: var(--surface-base); color: var(--text-primary); outline: none;
580
+ font-family: var(--font-sans);
581
+ }
582
+ .red-search-input:focus { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent-subtle); }
583
+ .red-search-input::placeholder { color: var(--text-tertiary); }
1313
584
 
1314
- /* Smooth toggle button animation */
1315
- #sidebarToggle {
1316
- transition: transform 0.2s ease-in-out;
585
+ /* Sidebar */
586
+ .red-sidebar {
587
+ width: 240px; flex-shrink: 0;
588
+ height: 100vh; position: sticky; top: 0;
589
+ background: var(--surface-primary);
590
+ border-right: 1px solid var(--border-primary);
591
+ display: flex; flex-direction: column;
592
+ overflow: hidden; z-index: 20;
593
+ }
594
+ .red-sidebar-logo {
595
+ padding: var(--space-5);
596
+ border-bottom: 1px solid var(--border-primary);
597
+ display: flex; align-items: center; gap: 10px;
598
+ }
599
+ .red-sidebar-logo-icon {
600
+ width: 28px; height: 28px; border-radius: var(--radius-sm);
601
+ background: var(--accent); display: flex; align-items: center; justify-content: center;
602
+ color: #fff; font-weight: 800; font-size: 13px; font-family: var(--font-mono); flex-shrink: 0;
603
+ }
604
+ .red-sidebar-section-label {
605
+ display: flex; align-items: center; justify-content: space-between; width: 100%;
606
+ padding: var(--space-2) var(--space-5);
607
+ font-size: 10px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase;
608
+ color: var(--text-tertiary); background: none; border: none; cursor: pointer;
609
+ }
610
+ .red-sidebar-section-label:hover { color: var(--text-secondary); }
611
+ .red-sidebar-bottom {
612
+ border-top: 1px solid var(--border-primary);
613
+ padding: var(--space-3) 0;
614
+ margin-top: auto;
615
+ }
616
+ .red-nav-badge {
617
+ font-size: 11px; font-weight: 600; padding: 1px 7px;
618
+ border-radius: var(--radius-full);
619
+ background: var(--accent-subtle); color: var(--accent);
620
+ font-variant-numeric: tabular-nums;
1317
621
  }
1318
622
 
1319
- #sidebarToggle i {
1320
- transition: transform 0.2s ease-in-out;
1321
- }
623
+ /* Sidebar collapse */
624
+ .red-sidebar.collapsed { width: 0; min-width: 0; padding: 0; border-right: none; }
625
+ .red-sidebar.collapsed > * { opacity: 0; pointer-events: none; }
626
+ .red-sidebar { transition: width 0.2s ease, opacity 0.2s ease; }
627
+ .red-main.expanded { width: 100%; }
1322
628
 
1323
- /* Responsive: Don't apply collapse on mobile */
629
+ /* Mobile sidebar toggle */
1324
630
  @media (max-width: 767.98px) {
1325
- .sidebar.sidebar-collapsed {
1326
- width: auto !important;
1327
- }
631
+ .red-sidebar { display: none; }
632
+ .red-sidebar.collapsed { display: none; }
1328
633
  }
1329
634
 
1330
- /* Add visual feedback on hover */
1331
- #sidebarToggle:hover i {
1332
- transform: scale(1.1);
635
+ /* Env badge */
636
+ .red-env-badge {
637
+ padding: 3px 10px; font-size: 11px; font-weight: 600;
638
+ border-radius: var(--radius-full);
639
+ text-transform: uppercase; letter-spacing: 0.05em;
1333
640
  }
1334
641
 
1335
- /* Skeleton Loading Animations */
1336
- @keyframes shimmer {
1337
- 0% { background-position: -200% 0; }
1338
- 100% { background-position: 200% 0; }
642
+ /* Kbd shortcut */
643
+ kbd {
644
+ display: inline-block; padding: 1px 5px; font-size: 10px;
645
+ font-family: var(--font-mono); font-weight: 500;
646
+ border-radius: 3px; border: 1px solid var(--border-secondary);
647
+ background: var(--surface-secondary); color: var(--text-tertiary);
1339
648
  }
1340
649
 
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;
650
+ /* Theme toggle */
651
+ .red-theme-toggle {
652
+ background: none; border: 1px solid var(--border-primary); border-radius: var(--radius-sm);
653
+ padding: 4px 8px; cursor: pointer; color: var(--text-secondary); font-size: 15px;
654
+ display: flex; align-items: center; transition: all var(--transition-fast);
1346
655
  }
656
+ .red-theme-toggle:hover { background: var(--surface-hover); color: var(--text-primary); }
1347
657
 
1348
- body.dark-mode .skeleton {
1349
- background: linear-gradient(90deg, #313244 25%, #45475a 50%, #313244 75%);
1350
- background-size: 200% 100%;
658
+ /* App switcher */
659
+ .red-app-switcher {
660
+ background: var(--surface-secondary); border: 1px solid var(--border-primary); border-radius: var(--radius-sm);
661
+ padding: 4px 10px; cursor: pointer; color: var(--text-secondary); font-size: 12px; font-weight: 500;
662
+ display: flex; align-items: center; gap: 4px;
1351
663
  }
664
+ .red-app-switcher:hover { background: var(--surface-tertiary); }
1352
665
 
1353
- .skeleton-text {
1354
- height: 1em;
1355
- width: 60%;
1356
- margin-bottom: 0.5em;
1357
- }
666
+ /* ============================================
667
+ Feature-Specific Styles
668
+ ============================================ */
1358
669
 
1359
- .skeleton-text-short {
1360
- width: 40%;
670
+ /* Filter pills */
671
+ .filter-pill {
672
+ display: inline-flex; align-items: center; gap: 4px;
673
+ padding: 5px 12px; font-size: 12px; font-weight: 500;
674
+ border-radius: var(--radius-full);
675
+ border: 1px solid var(--border-primary);
676
+ background: transparent; color: var(--text-secondary);
677
+ cursor: pointer; transition: all var(--transition-fast);
678
+ }
679
+ .filter-pill:hover { border-color: var(--accent); color: var(--accent); }
680
+ .filter-pill.active, .filter-pill.bg-primary { background: var(--accent-subtle); border-color: var(--accent); color: var(--accent); }
681
+ .filter-pill .bi-x { font-size: 1.1rem; font-weight: bold; }
682
+
683
+ /* Source Code Viewer */
684
+ .source-code-viewer {
685
+ border-radius: var(--radius-lg); overflow: hidden;
686
+ box-shadow: var(--shadow-md); border: 1px solid var(--border-primary);
687
+ margin-bottom: var(--space-6);
1361
688
  }
1362
-
1363
- .skeleton-card {
1364
- height: 80px;
689
+ .source-code-viewer .source-code-header {
690
+ background: var(--surface-hover); font-weight: 500;
691
+ border-bottom: 2px solid var(--border-primary);
1365
692
  }
1366
-
1367
- .skeleton-row {
1368
- height: 48px;
1369
- margin-bottom: 2px;
693
+ .source-code-viewer .git-blame-info {
694
+ background: var(--surface-base); border-top: 1px solid var(--border-primary);
1370
695
  }
1371
-
1372
- .skeleton-chart {
1373
- height: 250px;
696
+ .source-code-content { background: var(--surface-primary); }
697
+ .source-code-content pre { margin: 0; padding: 0; background: transparent; border: none; overflow: auto; }
698
+ .source-code-content pre code {
699
+ display: block; padding: 0; font-size: 0.9rem; line-height: 1.8;
700
+ font-family: var(--font-mono); color: var(--text-primary); background: transparent;
701
+ }
702
+ .source-code-content table.hljs-ln { width: 100%; border-collapse: collapse; margin: 0; background: transparent; }
703
+ .source-code-content .hljs-ln-line:hover, .source-code-content tr:hover { background: var(--accent-subtle); }
704
+ .source-code-content tr.error-line, .source-code-content .hljs-ln-line.error-line {
705
+ background: linear-gradient(90deg, var(--status-warning-bg) 0%, rgba(250,179,135,0.04) 100%);
706
+ border-left: 4px solid var(--status-warning);
707
+ animation: error-pulse 2s ease-in-out infinite;
1374
708
  }
1375
-
1376
- /* Hide skeleton containers by default */
1377
- .loading-skeleton {
1378
- display: none;
709
+ .source-code-content tr.error-line td { background: transparent; }
710
+ .source-code-content tr.error-line .hljs-ln-numbers {
711
+ background: linear-gradient(90deg, var(--status-warning) 0%, rgba(250,179,135,0.4) 100%);
712
+ color: #1a1a2e; font-weight: 700;
1379
713
  }
1380
-
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;
714
+ .source-code-content tr.error-line .hljs-ln-numbers::before {
715
+ content: '\2192'; position: absolute; left: 4px; color: var(--status-critical); font-weight: bold; font-size: 1rem;
1392
716
  }
1393
-
1394
- /* Section Navigation */
1395
- #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;
717
+ .source-code-content .hljs-ln-numbers {
718
+ width: 70px; min-width: 70px; text-align: right; padding: 0.6rem 1rem;
719
+ color: var(--text-tertiary); user-select: none;
720
+ background: var(--surface-hover); border-right: 3px solid var(--border-primary);
721
+ font-weight: 500; font-size: 0.8125rem; vertical-align: top; font-variant-numeric: tabular-nums;
722
+ }
723
+ .source-code-content .hljs-ln-code { padding: 0.6rem 1.25rem; white-space: pre; vertical-align: top; }
724
+ .source-code-content td.hljs-ln-numbers, .source-code-content td.hljs-ln-code { border: none; }
725
+ .source-code-content code.hljs { background: transparent; padding: 0; }
726
+
727
+ /* Syntax token colors — light mode */
728
+ .source-code-content .hljs-keyword, .source-code-content .hljs-selector-tag,
729
+ .source-code-content .hljs-literal, .source-code-content .hljs-section { color: #7c3aed; }
730
+ .source-code-content .hljs-string, .source-code-content .hljs-attr { color: #059669; }
731
+ .source-code-content .hljs-name, .source-code-content .hljs-type, .source-code-content .hljs-title { color: #dc2626; }
732
+ .source-code-content .hljs-comment, .source-code-content .hljs-quote { color: #6b7280; font-style: italic; }
733
+ .source-code-content .hljs-number, .source-code-content .hljs-symbol { color: #ea580c; }
734
+ .source-code-content .hljs-built_in { color: #0891b2; }
735
+
736
+ /* Source file path */
737
+ .source-file-path { color: var(--text-primary); }
738
+
739
+ /* Backtrace */
740
+ .backtrace-frame-number {
741
+ display: inline-block; min-width: 2em; text-align: right; margin-right: 0.5em;
742
+ color: var(--text-tertiary); font-family: var(--font-mono); font-size: 0.85em; user-select: none;
1403
743
  }
744
+ span.backtrace-method-name { color: var(--status-info); }
1404
745
 
1405
- #section-nav-wrapper.is-stuck {
1406
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
1407
- border-bottom: 1px solid #e5e7eb;
746
+ /* Inline code */
747
+ .inline-code-highlight {
748
+ padding: 2px 6px; margin: 0 2px; font-family: var(--font-mono); font-size: 0.9em;
749
+ background: var(--surface-secondary); color: var(--accent);
750
+ border: 1px solid var(--border-primary); border-radius: 4px; white-space: nowrap;
1408
751
  }
1409
752
 
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
- }
753
+ /* File path links */
754
+ .file-path-link { cursor: pointer; transition: all var(--transition-normal); }
755
+ .file-path-link:hover { background: var(--accent-subtle); color: var(--accent); box-shadow: 0 0 0 3px var(--accent-subtle); }
1420
756
 
1421
- .section-nav-label {
1422
- font-size: 0.85rem;
1423
- white-space: nowrap;
1424
- color: #9ca3af !important;
757
+ /* Timeline */
758
+ .timeline { position: relative; padding-left: 0; }
759
+ .timeline-item { position: relative; padding-left: 40px; padding-bottom: 30px; }
760
+ .timeline-item-last { padding-bottom: 0; }
761
+ .timeline-item::before { content: ''; position: absolute; left: 11px; top: 30px; bottom: 0; width: 2px; background: var(--border-primary); }
762
+ .timeline-item-last::before { display: none; }
763
+ .timeline-marker {
764
+ position: absolute; left: 0; top: 0; width: 24px; height: 24px;
765
+ display: flex; align-items: center; justify-content: center;
766
+ background: var(--surface-primary); border: 2px solid var(--border-primary); border-radius: 50%; z-index: 1;
1425
767
  }
768
+ .timeline-marker-current { border-color: var(--status-critical); background: var(--status-critical-bg); box-shadow: 0 0 0 4px var(--status-critical-bg); }
769
+ .timeline-marker i { font-size: 12px; }
770
+ .timeline-content { background: var(--surface-primary); padding: 12px; border-radius: var(--radius-md); border: 1px solid var(--border-primary); }
771
+ .timeline-item:hover .timeline-content { border-color: var(--accent); background: var(--accent-subtle); }
1426
772
 
773
+ /* Section nav pills */
774
+ #section-nav-wrapper {
775
+ position: sticky; top: 0; z-index: 1020;
776
+ background: var(--surface-base);
777
+ margin: 0 -12px; padding: 0 12px;
778
+ transition: box-shadow var(--transition-normal);
779
+ }
780
+ #section-nav-wrapper.is-stuck { box-shadow: var(--shadow-md); border-bottom: 1px solid var(--border-primary); }
781
+ .section-nav-scroll { scrollbar-width: none; -ms-overflow-style: none; mask-image: linear-gradient(to right, black 0, black calc(100% - 24px), transparent 100%); }
782
+ .section-nav-scroll::-webkit-scrollbar { display: none; }
783
+ .section-nav-label { font-size: 0.85rem; white-space: nowrap; color: var(--text-tertiary); }
1427
784
  .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
- }
785
+ display: inline-flex; align-items: center; gap: 4px;
786
+ padding: 4px 10px; border-radius: 20px;
787
+ font-size: 0.78rem; font-weight: 500; white-space: nowrap;
788
+ text-decoration: none; color: var(--text-secondary);
789
+ background: var(--surface-primary); border: 1px solid transparent;
790
+ transition: all 0.15s ease; cursor: pointer;
791
+ }
792
+ .section-nav-pill:hover { color: var(--text-primary); background: var(--surface-hover); text-decoration: none; }
793
+ .section-nav-pill.active { color: #fff; background: var(--accent); border-color: var(--accent); }
794
+ .section-nav-pill i { font-size: 0.75rem; }
795
+
796
+ /* Sidebar metadata sections */
797
+ .sidebar-section { border-left: 3px solid var(--border-primary); padding-left: 10px; margin-top: 16px; margin-bottom: 8px; }
798
+ .sidebar-section-blue { border-left-color: var(--status-info); }
799
+ .sidebar-section-red { border-left-color: var(--status-critical); }
800
+ .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
801
  .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); }
802
+ .sidebar-section-hint { font-size: 0.72rem; color: var(--text-tertiary); margin-bottom: 6px; display: block; }
803
+ .sidebar-section-body { background: var(--surface-base); border-radius: var(--radius-sm); padding: 8px 10px; }
804
+ .metadata-label { font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-tertiary); }
805
+
806
+ /* Heatmap */
807
+ .heatmap-grid { background: var(--surface-primary); }
808
+ .heatmap-cell { border-color: var(--border-primary); color: var(--text-primary); }
809
+ .heatmap-hour { color: var(--accent); font-weight: 600; font-size: 0.75rem; }
810
+ .heatmap-count { color: var(--status-warning); font-weight: 600; }
811
+
812
+ /* Skeletons */
813
+ .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; }
814
+ .skeleton-text { height: 1em; width: 60%; margin-bottom: 0.5em; }
815
+ .skeleton-text-short { width: 40%; }
816
+ .skeleton-card { height: 80px; }
817
+ .skeleton-row { height: 48px; margin-bottom: 2px; }
818
+ .skeleton-chart { height: 250px; }
819
+ .loading-skeleton { display: none; }
820
+
821
+ /* Button loading spinner */
822
+ .btn .loading-spinner {
823
+ display: inline-block; width: 1em; height: 1em;
824
+ border: 2px solid currentColor; border-right-color: transparent;
825
+ border-radius: 50%; animation: spinner-border 0.75s linear infinite;
826
+ vertical-align: middle; margin-right: 0.25em;
827
+ }
828
+
829
+ /* Empty States delightful */
830
+ @keyframes emptyFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
831
+ .red-empty-state {
832
+ text-align: center; padding: var(--space-12) var(--space-8);
833
+ animation: emptyFadeIn 0.3s ease-out;
834
+ }
835
+ .red-empty-state-icon {
836
+ width: 64px; height: 64px; margin: 0 auto var(--space-5);
837
+ display: flex; align-items: center; justify-content: center;
838
+ border-radius: 50%; background: var(--accent-subtle);
839
+ color: var(--accent); font-size: 28px;
840
+ }
841
+ .red-empty-state-title {
842
+ font-size: 16px; font-weight: 600; color: var(--text-primary);
843
+ margin-bottom: var(--space-2);
844
+ }
845
+ .red-empty-state-message {
846
+ font-size: 13px; color: var(--text-tertiary); max-width: 360px;
847
+ margin: 0 auto var(--space-5); line-height: 1.6;
848
+ }
849
+ .red-empty-state-cta {
850
+ display: inline-flex; align-items: center; gap: 6px;
851
+ padding: 8px 18px; font-size: 13px; font-weight: 500;
852
+ border-radius: var(--radius-full); border: 1px solid var(--accent);
853
+ background: var(--accent-subtle); color: var(--accent);
854
+ text-decoration: none; transition: all var(--transition-fast);
855
+ }
856
+ .red-empty-state-cta:hover { background: var(--accent); color: #fff; text-decoration: none; }
857
+
858
+ /* Page content fade-in */
859
+ @keyframes contentFadeIn { from { opacity: 0; } to { opacity: 1; } }
860
+ main { animation: contentFadeIn 0.15s ease; }
861
+
862
+ /* Footer */
863
+ .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; }
864
+
865
+ /* Nav tabs (used in some pages) */
866
+ .nav-tabs { display: flex; border-bottom: 1px solid var(--border-primary); gap: 0; list-style: none; padding: 0; margin: 0; }
867
+ .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; }
868
+ .nav-tabs .nav-link:hover { color: var(--text-primary); border-bottom-color: var(--border-primary); }
869
+ .nav-tabs .nav-link.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; background: none; }
870
+
871
+ /* Details/Summary */
872
+ details { background: var(--surface-primary); border: 1px solid var(--border-primary); border-radius: var(--radius-md); }
873
+ summary { padding: var(--space-3) var(--space-4); cursor: pointer; font-weight: 500; color: var(--text-primary); background: var(--surface-hover); }
874
+ details[open] summary { border-bottom: 1px solid var(--border-primary); }
1519
875
  </style>
1520
876
  </head>
1521
877
 
1522
878
  <body data-turbo="false">
1523
879
  <!-- Toast Container -->
1524
880
  <div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 9999;">
1525
- <!-- Toasts will be dynamically inserted here -->
1526
881
  </div>
1527
882
 
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>
883
+ <div style="display: flex; min-height: 100vh; background: var(--surface-base);">
884
+ <!-- Sidebar -->
885
+ <nav class="red-sidebar d-none d-md-flex" id="sidebar">
886
+ <!-- Logo -->
887
+ <div class="red-sidebar-logo">
888
+ <div class="red-sidebar-logo-icon">R</div>
889
+ <div>
890
+ <div style="font-size: 14px; font-weight: 700; color: var(--text-primary); letter-spacing: -0.01em;">RED</div>
891
+ <% sidebar_app_name = if defined?(@current_application_id) && @current_application_id.present? && defined?(@applications)
892
+ @applications.find { |name, id| id.to_s == @current_application_id.to_s }&.first
893
+ end %>
894
+ <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>
895
+ </div>
896
+ </div>
897
+
898
+ <!-- Nav groups -->
899
+ <div style="flex: 1; overflow: auto; padding: var(--space-3) 0;">
1540
900
  <%
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
901
+ nav_params = params[:application_id].present? ? { application_id: params[:application_id] } : {}
1548
902
  %>
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
903
 
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
- %>
904
+ <!-- CORE section -->
905
+ <div style="margin-bottom: var(--space-2);">
906
+ <div class="red-sidebar-section-label">Core</div>
1628
907
  <ul class="nav flex-column">
1629
908
  <li class="nav-item">
1630
909
  <%= link_to root_path(nav_params), class: "nav-link #{request.path == root_path ? 'active' : ''}" do %>
1631
- <i class="bi bi-speedometer2"></i> Overview
910
+ <i class="bi bi-grid-1x2"></i> Overview
1632
911
  <% end %>
1633
912
  </li>
1634
913
  <li class="nav-item">
1635
914
  <%= 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
915
+ <i class="bi bi-bug"></i>
916
+ <span style="flex: 1;">Errors</span>
917
+ <% if unresolved_badge_count && unresolved_badge_count > 0 %>
918
+ <span class="red-nav-badge"><%= unresolved_badge_count %></span>
919
+ <% end %>
1637
920
  <% end %>
1638
921
  </li>
1639
922
  <li class="nav-item">
1640
923
  <%= 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
924
+ <i class="bi bi-bar-chart-line"></i> Analytics
1642
925
  <% end %>
1643
926
  </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
927
+ </ul>
928
+ </div>
929
+
930
+ <!-- HEALTH section (feature-gated) -->
931
+ <% health_items = [] %>
932
+ <% health_items << { path: platform_comparison_errors_path(nav_params), icon: 'bi-cpu', label: 'Platform' } %>
933
+ <% if RailsErrorDashboard.configuration.enable_breadcrumbs %>
934
+ <% health_items << { path: cache_health_summary_errors_path(nav_params), icon: 'bi-hdd-stack', label: 'Cache' } %>
935
+ <% end %>
936
+ <% if RailsErrorDashboard.configuration.enable_system_health %>
937
+ <% health_items << { path: job_health_summary_errors_path(nav_params), icon: 'bi-gear-wide-connected', label: 'Jobs' } %>
938
+ <% health_items << { path: database_health_summary_errors_path(nav_params), icon: 'bi-database', label: 'Database' } %>
939
+ <% end %>
940
+ <% if RailsErrorDashboard.configuration.enable_rack_attack_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
941
+ <% health_items << { path: rack_attack_summary_errors_path(nav_params), icon: 'bi-shield-exclamation', label: 'Rate Limits' } %>
942
+ <% end %>
943
+ <% if RailsErrorDashboard.configuration.enable_actioncable_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
944
+ <% health_items << { path: actioncable_health_summary_errors_path(nav_params), icon: 'bi-broadcast', label: 'ActionCable' } %>
945
+ <% end %>
946
+ <% if RailsErrorDashboard.configuration.enable_activestorage_tracking && RailsErrorDashboard.configuration.enable_breadcrumbs %>
947
+ <% health_items << { path: activestorage_health_summary_errors_path(nav_params), icon: 'bi-cloud-arrow-up', label: 'ActiveStorage' } %>
948
+ <% end %>
949
+
950
+ <% if health_items.any? %>
951
+ <div style="margin-bottom: var(--space-2);" id="navHealthSection">
952
+ <button class="red-sidebar-section-label" onclick="toggleNavSection('navHealthItems')">
953
+ Health
954
+ <i class="bi bi-chevron-down" style="font-size: 10px;" id="navHealthChevron"></i>
955
+ </button>
956
+ <ul class="nav flex-column" id="navHealthItems">
957
+ <% health_items.each do |item| %>
958
+ <li class="nav-item">
959
+ <%= link_to item[:path], class: "nav-link #{request.path == item[:path] ? 'active' : ''}" do %>
960
+ <i class="bi <%= item[:icon] %>"></i> <%= item[:label] %>
961
+ <% end %>
962
+ </li>
1647
963
  <% end %>
1648
- </li>
964
+ </ul>
965
+ </div>
966
+ <% end %>
967
+
968
+ <!-- DIAGNOSTICS section (feature-gated) -->
969
+ <% diag_items = [] %>
970
+ <% if RailsErrorDashboard.configuration.enable_breadcrumbs %>
971
+ <% diag_items << { path: n_plus_one_summary_errors_path(nav_params), icon: 'bi-layers', label: 'N+1 Queries' } %>
972
+ <% diag_items << { path: deprecations_errors_path(nav_params), icon: 'bi-exclamation-triangle', label: 'Deprecations' } %>
973
+ <% end %>
974
+ <% if RailsErrorDashboard.configuration.detect_swallowed_exceptions %>
975
+ <% diag_items << { path: swallowed_exceptions_errors_path(nav_params), icon: 'bi-eye-slash', label: 'Swallowed' } %>
976
+ <% end %>
977
+ <% if RailsErrorDashboard.configuration.enable_diagnostic_dump %>
978
+ <% diag_items << { path: diagnostic_dumps_errors_path(nav_params), icon: 'bi-clipboard-pulse', label: 'Diagnostics' } %>
979
+ <% end %>
980
+
981
+ <% if diag_items.any? %>
982
+ <div style="margin-bottom: var(--space-2);" id="navDiagSection">
983
+ <button class="red-sidebar-section-label" onclick="toggleNavSection('navDiagItems')">
984
+ Diagnostics
985
+ <i class="bi bi-chevron-down" style="font-size: 10px;" id="navDiagChevron"></i>
986
+ </button>
987
+ <ul class="nav flex-column" id="navDiagItems">
988
+ <% diag_items.each do |item| %>
989
+ <li class="nav-item">
990
+ <%= link_to item[:path], class: "nav-link #{request.path == item[:path] ? 'active' : ''}" do %>
991
+ <i class="bi <%= item[:icon] %>"></i> <%= item[:label] %>
992
+ <% end %>
993
+ </li>
994
+ <% end %>
995
+ </ul>
996
+ </div>
997
+ <% end %>
998
+
999
+ <!-- INSIGHTS section -->
1000
+ <div style="margin-bottom: var(--space-2);" id="navInsightsSection">
1001
+ <button class="red-sidebar-section-label" onclick="toggleNavSection('navInsightsItems')">
1002
+ Insights
1003
+ <i class="bi bi-chevron-down" style="font-size: 10px;" id="navInsightsChevron"></i>
1004
+ </button>
1005
+ <ul class="nav flex-column" id="navInsightsItems">
1649
1006
  <li class="nav-item">
1650
1007
  <%= link_to correlation_errors_path(nav_params), class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}" do %>
1651
1008
  <i class="bi bi-diagram-3"></i> Correlation
@@ -1661,139 +1018,141 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1661
1018
  <i class="bi bi-people"></i> User Impact
1662
1019
  <% end %>
1663
1020
  </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>
1021
+ </ul>
1022
+ </div>
1023
+ </div>
1024
+
1025
+ <!-- Bottom pinned -->
1026
+ <div class="red-sidebar-bottom">
1027
+ <ul class="nav flex-column">
1028
+ <li class="nav-item">
1029
+ <%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
1030
+ <i class="bi bi-sliders"></i> Settings
1718
1031
  <% 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
1032
+ </li>
1033
+ </ul>
1034
+ </div>
1035
+ </nav>
1036
+
1037
+ <!-- Main area -->
1038
+ <div style="flex: 1; display: flex; flex-direction: column; min-width: 0;" id="mainArea">
1039
+ <!-- Top Navbar -->
1040
+ <header class="red-navbar">
1041
+ <div style="display: flex; align-items: center; gap: var(--space-4);">
1042
+ <!-- Mobile menu toggle -->
1043
+ <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);">
1044
+ <i class="bi bi-list"></i>
1045
+ </button>
1046
+ <!-- Desktop sidebar toggle -->
1047
+ <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);">
1048
+ <i class="bi bi-layout-sidebar-inset"></i>
1049
+ </button>
1050
+ <!-- Search -->
1051
+ <div style="position: relative;">
1052
+ <i class="bi bi-search" style="position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 13px; color: var(--text-tertiary);"></i>
1053
+ <input class="red-search-input" placeholder="Search errors... /" id="globalSearch" />
1054
+ </div>
1055
+ </div>
1056
+ <div style="display: flex; align-items: center; gap: var(--space-3);">
1057
+ <!-- Application Switcher (multi-app) -->
1058
+ <% if defined?(@applications) && @applications&.size.to_i > 1 %>
1059
+ <div class="dropdown">
1060
+ <button class="red-app-switcher dropdown-toggle" type="button" id="appSwitcher" data-bs-toggle="dropdown" aria-expanded="false">
1061
+ <i class="bi bi-layers"></i>
1062
+ <% if params[:application_id].present? %>
1063
+ <%= @applications.find { |name, id| id.to_s == params[:application_id].to_s }&.first || 'Unknown' %>
1064
+ <% else %>
1065
+ All Apps
1723
1066
  <% 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
1067
+ </button>
1068
+ <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="appSwitcher">
1069
+ <%
1070
+ # On detail pages (show), switching apps should go to the errors list
1071
+ # since the current error may not belong to the selected app
1072
+ on_detail_page = request.path_parameters[:action] == "show"
1073
+ if on_detail_page
1074
+ switcher_base = { controller: request.path_parameters[:controller], action: "index" }
1075
+ switcher_filter_params = {}
1076
+ else
1077
+ switcher_base = request.path_parameters.slice(:controller, :action)
1078
+ switcher_filter_params = permitted_filter_params
1079
+ end
1080
+ %>
1081
+ <li>
1082
+ <%= link_to "All Applications",
1083
+ url_for(switcher_base.merge(switcher_filter_params.except(:application_id))),
1084
+ class: "dropdown-item #{'active' unless params[:application_id].present?}" %>
1085
+ </li>
1086
+ <li><hr class="dropdown-divider"></li>
1087
+ <% @applications.each do |app_name, app_id| %>
1088
+ <li>
1089
+ <%= link_to app_name,
1090
+ url_for(switcher_base.merge(switcher_filter_params.merge(application_id: app_id))),
1091
+ class: "dropdown-item #{params[:application_id].to_s == app_id.to_s ? 'active' : ''}" %>
1092
+ </li>
1730
1093
  <% 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>
1094
+ </ul>
1095
+ </div>
1096
+ <% end %>
1097
+ <kbd>?</kbd>
1098
+ <button class="red-theme-toggle" id="themeToggle" title="Toggle theme">
1099
+ <i class="bi bi-moon" id="themeIcon"></i>
1100
+ </button>
1101
+ <span class="red-env-badge" style="background: var(--status-success-bg); color: var(--status-success);"><%= Rails.env %></span>
1778
1102
  </div>
1779
- </nav>
1103
+ </header>
1780
1104
 
1781
1105
  <!-- Main content -->
1782
- <main class="col-md-10 ms-sm-auto px-md-4" id="mainContent">
1106
+ <main style="flex: 1; padding: var(--space-6) var(--space-8); max-width: 1200px; width: 100%; margin: 0 auto;">
1783
1107
  <% 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>
1108
+ <div 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);">
1109
+ <i class="bi bi-exclamation-triangle-fill" style="font-size: 18px;"></i>
1788
1110
  <div>
1789
1111
  <strong>Security Warning:</strong> You are using default credentials (gandalf/youshallnotpass).
1790
1112
  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.
1113
+ or configure <code>authenticate_with</code> in your initializer.
1792
1114
  </div>
1793
1115
  </div>
1794
1116
  <% end %>
1795
1117
  <%= yield %>
1796
1118
  </main>
1119
+
1120
+ <!-- Footer -->
1121
+ <footer class="red-footer">
1122
+ <p style="margin-bottom: 4px;">
1123
+ <i class="bi bi-bug-fill" style="color: var(--accent);"></i>
1124
+ Built with <a href="https://github.com/AnjanJ/rails_error_dashboard" target="_blank"><strong>RED</strong> &mdash; Rails Error Dashboard</a>
1125
+ </p>
1126
+ <p style="font-size: 12px; color: var(--text-tertiary); margin: 0;">
1127
+ Created by <a href="https://www.anjan.dev/" target="_blank">Anjan Jagirdar</a>
1128
+ </p>
1129
+ </footer>
1130
+ </div>
1131
+ </div>
1132
+
1133
+ <!-- Mobile Sidebar (Offcanvas) -->
1134
+ <div class="offcanvas offcanvas-start" tabindex="-1" id="sidebarMenu" aria-labelledby="sidebarMenuLabel">
1135
+ <div class="offcanvas-header">
1136
+ <div style="display: flex; align-items: center; gap: 10px;">
1137
+ <div class="red-sidebar-logo-icon">R</div>
1138
+ <strong>RED</strong>
1139
+ </div>
1140
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
1141
+ </div>
1142
+ <div class="offcanvas-body">
1143
+ <%
1144
+ nav_params = params[:application_id].present? ? { application_id: params[:application_id] } : {}
1145
+ %>
1146
+ <ul class="nav flex-column">
1147
+ <li class="nav-item"><%= link_to root_path(nav_params), class: "nav-link" do %><i class="bi bi-grid-1x2"></i> Overview<% end %></li>
1148
+ <li class="nav-item"><%= link_to errors_path(nav_params), class: "nav-link" do %><i class="bi bi-bug"></i> Errors<% end %></li>
1149
+ <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>
1150
+ <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>
1151
+ <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>
1152
+ <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>
1153
+ <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>
1154
+ <li class="nav-item"><%= link_to settings_path(nav_params), class: "nav-link" do %><i class="bi bi-sliders"></i> Settings<% end %></li>
1155
+ </ul>
1797
1156
  </div>
1798
1157
  </div>
1799
1158
 
@@ -1810,48 +1169,32 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1810
1169
  <div class="modal-body">
1811
1170
  <div class="list-group list-group-flush">
1812
1171
  <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>
1172
+ <span><i class="bi bi-arrow-clockwise" style="color: var(--accent);"></i> Refresh page</span>
1173
+ <kbd>R</kbd>
1815
1174
  </div>
1816
1175
  <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>
1176
+ <span><i class="bi bi-search" style="color: var(--accent);"></i> Focus search</span>
1177
+ <kbd>/</kbd>
1819
1178
  </div>
1820
1179
  <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>
1180
+ <span><i class="bi bi-bar-chart-line" style="color: var(--accent);"></i> Analytics</span>
1181
+ <kbd>A</kbd>
1823
1182
  </div>
1824
1183
  <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>
1184
+ <span><i class="bi bi-layout-sidebar" style="color: var(--accent);"></i> Toggle sidebar</span>
1185
+ <kbd>S</kbd>
1827
1186
  </div>
1828
1187
  <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>
1188
+ <span><i class="bi bi-keyboard" style="color: var(--accent);"></i> Show shortcuts</span>
1189
+ <kbd>?</kbd>
1831
1190
  </div>
1832
1191
  </div>
1833
1192
  </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
1193
  </div>
1838
1194
  </div>
1839
1195
  </div>
1840
1196
 
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 -->
1197
+ <!-- Bootstrap JS (for modals, tooltips, dropdowns, offcanvas) -->
1855
1198
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
1856
1199
 
1857
1200
  <!-- Stimulus (for loading state management) -->
@@ -1882,26 +1225,20 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1882
1225
  if (this._safetyTimeout) clearTimeout(this._safetyTimeout);
1883
1226
  }
1884
1227
 
1885
- // Called on filter form submit
1886
1228
  submit() {
1887
1229
  this.showSkeletons();
1888
1230
  this.disableSubmitButtons();
1889
1231
  this.startSafetyTimeout();
1890
1232
  }
1891
1233
 
1892
- // Called on async action button click
1893
1234
  click(event) {
1894
1235
  var button = event.currentTarget;
1895
1236
  if (button.disabled) return;
1896
-
1897
1237
  button.disabled = true;
1898
1238
  var originalHTML = button.dataset.loadingOriginalHtml;
1899
- if (!originalHTML) {
1900
- button.dataset.loadingOriginalHtml = button.innerHTML;
1901
- }
1239
+ if (!originalHTML) { button.dataset.loadingOriginalHtml = button.innerHTML; }
1902
1240
  var spinnerHTML = '<span class="loading-spinner"></span>';
1903
- var buttonText = button.textContent.trim();
1904
- button.innerHTML = spinnerHTML + ' ' + buttonText;
1241
+ button.innerHTML = spinnerHTML + ' ' + button.textContent.trim();
1905
1242
  }
1906
1243
 
1907
1244
  showSkeletons() {
@@ -1919,11 +1256,8 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1919
1256
  disableSubmitButtons() {
1920
1257
  this.submitButtonTargets.forEach(function(btn) {
1921
1258
  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...';
1259
+ if (!btn.dataset.loadingOriginalHtml) { btn.dataset.loadingOriginalHtml = btn.innerHTML; }
1260
+ btn.innerHTML = '<span class="loading-spinner"></span> Loading...';
1927
1261
  });
1928
1262
  }
1929
1263
 
@@ -1939,16 +1273,12 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1939
1273
 
1940
1274
  startSafetyTimeout() {
1941
1275
  var self = this;
1942
- this._safetyTimeout = setTimeout(function() {
1943
- self.hideSkeletons();
1944
- }, 10000);
1276
+ this._safetyTimeout = setTimeout(function() { self.hideSkeletons(); }, 10000);
1945
1277
  }
1946
1278
  });
1947
1279
  })();
1948
1280
  </script>
1949
1281
 
1950
- <!-- Dashboard JavaScript (inline for production compatibility) -->
1951
-
1952
1282
  <!-- Syntax Highlighting -->
1953
1283
  <script>
1954
1284
  (function() {
@@ -1973,15 +1303,11 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1973
1303
  codeBlocks.forEach(function(codeBlock) {
1974
1304
  var errorLine = parseInt(codeBlock.dataset.errorLine);
1975
1305
  var startLine = parseInt(codeBlock.dataset.startLine) || 1;
1976
-
1977
1306
  if (errorLine && !isNaN(errorLine)) {
1978
1307
  var table = codeBlock.querySelector('table.hljs-ln');
1979
1308
  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
- }
1309
+ table.querySelectorAll('tr').forEach(function(row, index) {
1310
+ if ((startLine + index) === errorLine) { row.classList.add('error-line'); }
1985
1311
  });
1986
1312
  }
1987
1313
  }
@@ -1997,109 +1323,84 @@ body.dark-mode .sidebar-section-body { background: var(--ctp-mantle); }
1997
1323
  })();
1998
1324
  </script>
1999
1325
 
2000
- <!-- Theme Toggle -->
1326
+ <!-- Theme Toggle + Chart Theme -->
2001
1327
  <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
1328
  document.addEventListener('DOMContentLoaded', function() {
2012
1329
  var themeToggle = document.getElementById('themeToggle');
2013
1330
  var themeIcon = document.getElementById('themeIcon');
2014
1331
 
1332
+ function isDark() { return document.documentElement.getAttribute('data-theme') === 'dark'; }
1333
+
2015
1334
  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
- }
1335
+ if (themeIcon) themeIcon.className = isDark() ? 'bi bi-sun' : 'bi bi-moon';
2021
1336
  }
2022
1337
 
2023
1338
  updateIcon();
2024
1339
 
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
- });
1340
+ if (themeToggle) {
1341
+ themeToggle.addEventListener('click', function() {
1342
+ var next = isDark() ? 'light' : 'dark';
1343
+ document.documentElement.setAttribute('data-theme', next);
1344
+ localStorage.setItem('red-theme', next);
1345
+ updateIcon();
1346
+ applyChartTheme();
1347
+ // Reload to ensure all chart instances pick up the new theme
1348
+ setTimeout(function() { location.reload(); }, 200);
1349
+ });
1350
+ }
2033
1351
 
2034
1352
  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
- }
1353
+ if (typeof Chart === 'undefined') return;
1354
+ var dark = isDark();
1355
+ var textColor = dark ? '#cdd6f4' : '#1a1a2e';
1356
+ var gridColor = dark ? 'rgba(88, 91, 112, 0.2)' : 'rgba(0, 0, 0, 0.08)';
1357
+
1358
+ Chart.defaults.color = textColor;
1359
+ Chart.defaults.borderColor = gridColor;
1360
+ Chart.defaults.font = Chart.defaults.font || {};
1361
+ Chart.defaults.font.color = textColor;
1362
+ Chart.defaults.font.family = "'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif";
1363
+
1364
+ if (Chart.defaults.scales) {
1365
+ ['x', 'y'].forEach(function(axis) {
1366
+ Chart.defaults.scales[axis] = Chart.defaults.scales[axis] || {};
1367
+ Chart.defaults.scales[axis].ticks = Chart.defaults.scales[axis].ticks || {};
1368
+ Chart.defaults.scales[axis].ticks.color = textColor;
1369
+ Chart.defaults.scales[axis].grid = Chart.defaults.scales[axis].grid || {};
1370
+ Chart.defaults.scales[axis].grid.color = gridColor;
1371
+ });
1372
+ }
2053
1373
 
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;
1374
+ if (Chart.defaults.plugins) {
1375
+ if (Chart.defaults.plugins.legend) {
1376
+ Chart.defaults.plugins.legend.labels = Chart.defaults.plugins.legend.labels || {};
1377
+ Chart.defaults.plugins.legend.labels.color = textColor;
2066
1378
  }
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
- }
1379
+ if (Chart.defaults.plugins.tooltip) {
1380
+ Chart.defaults.plugins.tooltip.backgroundColor = dark ? '#313244' : 'rgba(255, 255, 255, 0.95)';
1381
+ Chart.defaults.plugins.tooltip.titleColor = textColor;
1382
+ Chart.defaults.plugins.tooltip.bodyColor = textColor;
1383
+ Chart.defaults.plugins.tooltip.borderColor = dark ? '#585b70' : 'rgba(0, 0, 0, 0.15)';
1384
+ Chart.defaults.plugins.tooltip.borderWidth = 1;
2083
1385
  }
2084
1386
  }
2085
1387
  }
2086
1388
 
2087
1389
  applyChartTheme();
2088
1390
 
1391
+ // Ensure charts created after page load also get themed
2089
1392
  var observer = new MutationObserver(function(mutations) {
2090
1393
  mutations.forEach(function(mutation) {
2091
1394
  mutation.addedNodes.forEach(function(node) {
2092
- if (node.tagName === 'CANVAS') {
2093
- setTimeout(applyChartTheme, 100);
2094
- }
1395
+ if (node.tagName === 'CANVAS') { setTimeout(applyChartTheme, 100); }
2095
1396
  });
2096
1397
  });
2097
1398
  });
2098
-
2099
1399
  observer.observe(document.body, { childList: true, subtree: true });
2100
1400
 
2101
1401
  document.addEventListener('chartkick:load', function() { applyChartTheme(); });
2102
1402
 
1403
+ // Force-apply a few times on page load for lazy-loaded charts
2103
1404
  var attempts = 0;
2104
1405
  var forceInterval = setInterval(function() {
2105
1406
  attempts++;
@@ -2109,22 +1410,22 @@ document.addEventListener('DOMContentLoaded', function() {
2109
1410
  });
2110
1411
  </script>
2111
1412
 
2112
- <!-- Utilities -->
1413
+ <!-- Utilities (tooltips, clipboard, time conversion, keyboard shortcuts) -->
2113
1414
  <script>
2114
1415
  document.addEventListener('DOMContentLoaded', function() {
2115
1416
 
1417
+ // Tooltips
2116
1418
  function initializeTooltips() {
2117
1419
  var tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
2118
1420
  tooltipTriggerList.forEach(function(el) { new bootstrap.Tooltip(el); });
2119
1421
  }
2120
-
2121
1422
  initializeTooltips();
2122
-
2123
1423
  if (typeof Turbo !== 'undefined') {
2124
1424
  document.addEventListener('turbo:load', initializeTooltips);
2125
1425
  document.addEventListener('turbo:frame-load', initializeTooltips);
2126
1426
  }
2127
1427
 
1428
+ // Clipboard
2128
1429
  window.copyToClipboard = function(text, button) {
2129
1430
  navigator.clipboard.writeText(text).then(function() {
2130
1431
  var originalHTML = button.innerHTML;
@@ -2137,51 +1438,41 @@ document.addEventListener('DOMContentLoaded', function() {
2137
1438
  button.classList.remove('btn-success');
2138
1439
  button.classList.add('btn-outline-secondary');
2139
1440
  }, 2000);
2140
- }).catch(function(err) {
1441
+ }).catch(function() {
2141
1442
  button.innerHTML = '<i class="bi bi-x"></i> Failed';
2142
1443
  showToast('Failed to copy to clipboard', 'danger');
2143
1444
  });
2144
1445
  };
2145
1446
 
1447
+ // Toasts
2146
1448
  window.showToast = function(message, type) {
2147
1449
  type = type || 'success';
1450
+ var iconClass = type === 'success' ? 'bi-check-circle-fill' : type === 'danger' ? 'bi-exclamation-circle-fill' : 'bi-info-circle-fill';
1451
+ var bgClass = type === 'success' ? 'bg-success' : type === 'danger' ? 'bg-danger' : 'bg-info';
2148
1452
  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
1453
  var toastHTML =
2157
1454
  '<div id="' + toastId + '" class="toast align-items-center text-white ' + bgClass + ' border-0" role="alert" aria-live="assertive" aria-atomic="true">' +
2158
1455
  '<div class="d-flex">' +
2159
- '<div class="toast-body">' +
2160
- '<i class="bi ' + iconClass + ' me-2"></i>' +
2161
- message +
2162
- '</div>' +
1456
+ '<div class="toast-body"><i class="bi ' + iconClass + ' me-2"></i>' + message + '</div>' +
2163
1457
  '<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>' +
2164
1458
  '</div>' +
2165
1459
  '</div>';
2166
-
2167
1460
  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
- });
1461
+ if (container) {
1462
+ container.insertAdjacentHTML('beforeend', toastHTML);
1463
+ var toastElement = document.getElementById(toastId);
1464
+ var toast = new bootstrap.Toast(toastElement, { delay: 4000 });
1465
+ toast.show();
1466
+ toastElement.addEventListener('hidden.bs.toast', function() { toastElement.remove(); });
1467
+ }
2177
1468
  };
2178
1469
 
1470
+ // Local time conversion
2179
1471
  function convertToLocalTime() {
2180
1472
  document.querySelectorAll('.local-time').forEach(function(element) {
2181
1473
  var utcString = element.dataset.utc;
2182
1474
  var formatString = element.dataset.format;
2183
1475
  if (!utcString) return;
2184
-
2185
1476
  try {
2186
1477
  var date = new Date(utcString);
2187
1478
  if (isNaN(date.getTime())) return;
@@ -2193,12 +1484,10 @@ document.addEventListener('DOMContentLoaded', function() {
2193
1484
  element.dataset.originalUtc = utcString;
2194
1485
  element.dataset.localFormatted = formatted + ' ' + timezone;
2195
1486
  element.dataset.showingLocal = 'true';
2196
-
2197
1487
  element.addEventListener('click', function() {
2198
1488
  if (this.dataset.showingLocal === 'true') {
2199
1489
  var utcDate = new Date(this.dataset.originalUtc);
2200
- var utcFormatted = formatDateTime(utcDate, formatString);
2201
- this.textContent = utcFormatted + ' UTC';
1490
+ this.textContent = formatDateTime(utcDate, formatString) + ' UTC';
2202
1491
  this.title = 'UTC time (click to see local time)';
2203
1492
  this.dataset.showingLocal = 'false';
2204
1493
  } else {
@@ -2213,31 +1502,22 @@ document.addEventListener('DOMContentLoaded', function() {
2213
1502
  document.querySelectorAll('.local-time-ago').forEach(function(element) {
2214
1503
  var utcString = element.dataset.utc;
2215
1504
  if (!utcString) return;
2216
-
2217
1505
  try {
2218
1506
  var date = new Date(utcString);
2219
1507
  if (isNaN(date.getTime())) return;
2220
- var now = new Date();
2221
- var diffMs = now - date;
2222
- var formatted = formatRelativeTime(diffMs);
2223
- element.textContent = formatted;
1508
+ element.textContent = formatRelativeTime(new Date() - date);
2224
1509
  element.title = 'Click to see exact time';
2225
1510
  element.style.cursor = 'pointer';
2226
1511
  element.dataset.originalUtc = utcString;
2227
1512
  element.dataset.showingRelative = 'true';
2228
-
2229
1513
  element.addEventListener('click', function() {
2230
1514
  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;
1515
+ var d = new Date(this.dataset.originalUtc);
1516
+ this.textContent = formatDateTime(d, '%B %d, %Y %I:%M:%S %p') + ' ' + getTimezoneAbbreviation(d);
2235
1517
  this.title = 'Click to see relative time';
2236
1518
  this.dataset.showingRelative = 'false';
2237
1519
  } else {
2238
- var now = new Date();
2239
- var d = new Date(this.dataset.originalUtc);
2240
- this.textContent = formatRelativeTime(now - d);
1520
+ this.textContent = formatRelativeTime(new Date() - new Date(this.dataset.originalUtc));
2241
1521
  this.title = 'Click to see exact time';
2242
1522
  this.dataset.showingRelative = 'true';
2243
1523
  }
@@ -2246,109 +1526,112 @@ document.addEventListener('DOMContentLoaded', function() {
2246
1526
  });
2247
1527
  }
2248
1528
 
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';
1529
+ function formatDateTime(date, fmt) {
1530
+ var months = ['January','February','March','April','May','June','July','August','September','October','November','December'];
1531
+ var monthsShort = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
1532
+ var days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
1533
+ var daysShort = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
1534
+ var y = date.getFullYear(), mo = date.getMonth(), d = date.getDate();
1535
+ var h = date.getHours(), mi = date.getMinutes(), s = date.getSeconds(), dow = date.getDay();
1536
+ var h12 = h % 12 || 12, ampm = h >= 12 ? 'PM' : 'AM';
2266
1537
  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());
1538
+ return fmt.replace('%Y',y).replace('%y',y.toString().substr(2)).replace('%B',months[mo]).replace('%b',monthsShort[mo])
1539
+ .replace('%m',pad(mo+1)).replace('%d',pad(d)).replace('%e',d).replace('%A',days[dow]).replace('%a',daysShort[dow])
1540
+ .replace('%H',pad(h)).replace('%I',pad(h12)).replace('%M',pad(mi)).replace('%S',pad(s)).replace('%p',ampm).replace('%P',ampm.toLowerCase());
2284
1541
  }
2285
1542
 
2286
1543
  function getTimezoneAbbreviation(date) {
2287
- var timeZoneString = date.toLocaleTimeString('en-US', { timeZoneName: 'short' });
2288
- var parts = timeZoneString.split(' ');
1544
+ var parts = date.toLocaleTimeString('en-US', { timeZoneName: 'short' }).split(' ');
2289
1545
  return parts[parts.length - 1];
2290
1546
  }
2291
1547
 
2292
1548
  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';
1549
+ 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);
1550
+ if (sec < 60) return sec <= 1 ? '1 second ago' : sec + ' seconds ago';
1551
+ if (min < 60) return min === 1 ? '1 minute ago' : min + ' minutes ago';
1552
+ if (hr < 24) return hr === 1 ? '1 hour ago' : hr + ' hours ago';
1553
+ if (d < 30) return d === 1 ? '1 day ago' : d + ' days ago';
1554
+ if (mo < 12) return mo === 1 ? '1 month ago' : mo + ' months ago';
1555
+ return yr === 1 ? '1 year ago' : yr + ' years ago';
2306
1556
  }
2307
1557
 
2308
1558
  convertToLocalTime();
2309
-
2310
1559
  if (typeof Turbo !== 'undefined') {
2311
1560
  document.addEventListener('turbo:load', convertToLocalTime);
2312
1561
  document.addEventListener('turbo:frame-load', convertToLocalTime);
2313
1562
  }
1563
+
1564
+ // Keyboard Shortcuts
1565
+ document.addEventListener('keydown', function(e) {
1566
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
1567
+ var key = e.key;
1568
+ if (key === '?' || (key === '/' && e.shiftKey)) {
1569
+ e.preventDefault();
1570
+ var modal = document.getElementById('keyboardShortcutsModal');
1571
+ if (modal) { new bootstrap.Modal(modal).show(); }
1572
+ } else if (key === '/') {
1573
+ e.preventDefault();
1574
+ var search = document.getElementById('globalSearch');
1575
+ if (search) search.focus();
1576
+ } else if (key === 'r' || key === 'R') {
1577
+ e.preventDefault();
1578
+ location.reload();
1579
+ } else if (key === 'a' || key === 'A') {
1580
+ e.preventDefault();
1581
+ window.location.href = '<%= analytics_errors_path %>';
1582
+ }
1583
+ });
2314
1584
  });
2315
1585
  </script>
2316
1586
 
2317
- <!-- Sidebar Toggle -->
1587
+ <!-- Sidebar Toggle + Nav Section Toggle -->
2318
1588
  <script>
2319
1589
  document.addEventListener('DOMContentLoaded', function() {
2320
1590
  var sidebarToggle = document.getElementById('sidebarToggle');
2321
1591
  var sidebar = document.getElementById('sidebar');
2322
- var mainContent = document.getElementById('mainContent');
2323
- var STORAGE_KEY = 'rails_error_dashboard_sidebar_collapsed';
1592
+ var STORAGE_KEY = 'red_sidebar_collapsed';
2324
1593
 
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);
1594
+ if (sidebarToggle && sidebar) {
1595
+ if (localStorage.getItem(STORAGE_KEY) === 'true') {
1596
+ sidebar.classList.add('collapsed');
2343
1597
  }
2344
- });
2345
1598
 
2346
- document.addEventListener('keydown', function(e) {
2347
- if (e.target.tagName !== 'INPUT' && e.target.tagName !== 'TEXTAREA' && !e.target.isContentEditable) {
1599
+ sidebarToggle.addEventListener('click', function() {
1600
+ sidebar.classList.toggle('collapsed');
1601
+ localStorage.setItem(STORAGE_KEY, sidebar.classList.contains('collapsed'));
1602
+ });
1603
+
1604
+ document.addEventListener('keydown', function(e) {
1605
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
2348
1606
  if (e.key === 's' || e.key === 'S') {
2349
1607
  e.preventDefault();
2350
1608
  sidebarToggle.click();
2351
1609
  }
1610
+ });
1611
+ }
1612
+ });
1613
+
1614
+ // Nav section collapse/expand
1615
+ function toggleNavSection(itemsId) {
1616
+ var items = document.getElementById(itemsId);
1617
+ var chevron = document.getElementById(itemsId.replace('Items', 'Chevron'));
1618
+ if (items) {
1619
+ var hidden = items.style.display === 'none';
1620
+ items.style.display = hidden ? '' : 'none';
1621
+ if (chevron) chevron.className = hidden ? 'bi bi-chevron-down' : 'bi bi-chevron-right';
1622
+ localStorage.setItem('red_nav_' + itemsId, hidden ? 'open' : 'closed');
1623
+ }
1624
+ }
1625
+
1626
+ // Restore nav section states
1627
+ document.addEventListener('DOMContentLoaded', function() {
1628
+ ['navHealthItems', 'navDiagItems', 'navInsightsItems'].forEach(function(id) {
1629
+ var state = localStorage.getItem('red_nav_' + id);
1630
+ if (state === 'closed') {
1631
+ var items = document.getElementById(id);
1632
+ var chevron = document.getElementById(id.replace('Items', 'Chevron'));
1633
+ if (items) items.style.display = 'none';
1634
+ if (chevron) chevron.className = 'bi bi-chevron-right';
2352
1635
  }
2353
1636
  });
2354
1637
  });
@@ -2356,7 +1639,6 @@ document.addEventListener('DOMContentLoaded', function() {
2356
1639
 
2357
1640
  <!-- Flash Messages -->
2358
1641
  <script>
2359
- // Show flash messages as toasts
2360
1642
  <% if defined?(flash) && flash.present? %>
2361
1643
  <% if flash[:notice] %>
2362
1644
  showToast('<%= j flash[:notice] %>', 'success');