rails_error_dashboard 0.1.1 → 0.1.3

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +66 -21
  3. data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +107 -0
  4. data/app/assets/stylesheets/rails_error_dashboard/_components.scss +625 -0
  5. data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +257 -0
  6. data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +203 -0
  7. data/app/assets/stylesheets/rails_error_dashboard/application.css +926 -15
  8. data/app/assets/stylesheets/rails_error_dashboard/application.css.map +7 -0
  9. data/app/assets/stylesheets/rails_error_dashboard/application.scss +61 -0
  10. data/app/controllers/rails_error_dashboard/errors_controller.rb +94 -1
  11. data/app/helpers/rails_error_dashboard/application_helper.rb +42 -4
  12. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +91 -0
  13. data/app/helpers/rails_error_dashboard/overview_helper.rb +78 -0
  14. data/app/helpers/rails_error_dashboard/user_agent_helper.rb +118 -0
  15. data/app/models/rails_error_dashboard/error_comment.rb +27 -0
  16. data/app/models/rails_error_dashboard/error_log.rb +145 -0
  17. data/app/views/layouts/rails_error_dashboard.html.erb +796 -299
  18. data/app/views/layouts/rails_error_dashboard_old_backup.html.erb +383 -0
  19. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +2 -0
  20. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +4 -4
  21. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +167 -0
  22. data/app/views/rails_error_dashboard/errors/analytics.html.erb +138 -22
  23. data/app/views/rails_error_dashboard/errors/index.html.erb +83 -4
  24. data/app/views/rails_error_dashboard/errors/overview.html.erb +253 -0
  25. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +29 -18
  26. data/app/views/rails_error_dashboard/errors/show.html.erb +353 -54
  27. data/config/routes.rb +7 -0
  28. data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +27 -0
  29. data/db/migrate/20251226020100_create_error_comments.rb +18 -0
  30. data/lib/generators/rails_error_dashboard/install/install_generator.rb +8 -2
  31. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +21 -0
  32. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -1
  33. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -2
  34. data/lib/rails_error_dashboard/commands/log_error.rb +47 -9
  35. data/lib/rails_error_dashboard/commands/resolve_error.rb +1 -1
  36. data/lib/rails_error_dashboard/configuration.rb +8 -0
  37. data/lib/rails_error_dashboard/error_reporter.rb +4 -4
  38. data/lib/rails_error_dashboard/logger.rb +105 -0
  39. data/lib/rails_error_dashboard/middleware/error_catcher.rb +2 -2
  40. data/lib/rails_error_dashboard/plugin.rb +3 -3
  41. data/lib/rails_error_dashboard/plugin_registry.rb +2 -2
  42. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +1 -1
  43. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +1 -1
  44. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +109 -1
  45. data/lib/rails_error_dashboard/queries/errors_list.rb +61 -6
  46. data/lib/rails_error_dashboard/services/backtrace_parser.rb +113 -0
  47. data/lib/rails_error_dashboard/version.rb +1 -1
  48. data/lib/rails_error_dashboard.rb +2 -0
  49. metadata +18 -2
  50. data/lib/tasks/rails_error_dashboard_tasks.rake +0 -4
@@ -0,0 +1,383 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Error Dashboard - Audio Intelli API</title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <%= csrf_meta_tags %>
7
+ <%= csp_meta_tag %>
8
+
9
+ <!-- Apply theme immediately to prevent flash of wrong theme -->
10
+ <script>
11
+ // This runs BEFORE body renders to prevent flash
12
+ (function() {
13
+ const savedTheme = localStorage.getItem('theme');
14
+ if (savedTheme === 'dark') {
15
+ // Add to html element so we can style body
16
+ document.documentElement.setAttribute('data-theme', 'dark');
17
+ }
18
+ })();
19
+ </script>
20
+
21
+ <!-- Bootstrap CSS -->
22
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
23
+ <!-- Bootstrap Icons -->
24
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
25
+ <!-- Chart.js with date adapter -->
26
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
27
+ <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
28
+ <script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script>
29
+
30
+ <!-- Turbo for real-time updates -->
31
+ <script type="module">
32
+ import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.12/+esm'
33
+ </script>
34
+
35
+ <!-- Rails Error Dashboard Styles - Catppuccin Mocha Theme -->
36
+ <style>
37
+ <%= File.read(RailsErrorDashboard::Engine.root.join("app/assets/stylesheets/rails_error_dashboard/application.css")) %>
38
+ </style>
39
+ </head>
40
+
41
+ <body>
42
+ <!-- Loading Indicator -->
43
+ <div id="loading-indicator"></div>
44
+
45
+ <!-- Top Navbar -->
46
+ <nav class="navbar navbar-dark">
47
+ <div class="container-fluid">
48
+ <div class="d-flex align-items-center">
49
+ <!-- Mobile menu toggle -->
50
+ <button class="btn btn-link text-white d-md-none me-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu">
51
+ <i class="bi bi-list fs-4"></i>
52
+ </button>
53
+ <a class="navbar-brand fw-bold" href="<%= root_path %>">
54
+ <i class="bi bi-bug-fill"></i>
55
+ Error Dashboard
56
+ </a>
57
+ </div>
58
+ <div class="d-flex align-items-center gap-3">
59
+ <button class="theme-toggle" id="themeToggle" onclick="toggleTheme()">
60
+ <i class="bi bi-moon-fill" id="themeIcon"></i>
61
+ </button>
62
+ <div class="text-white d-none d-md-block">
63
+ <small><%= Rails.env.titleize %> Environment</small>
64
+ </div>
65
+ </div>
66
+ </div>
67
+ </nav>
68
+
69
+ <div class="container-fluid">
70
+ <div class="row">
71
+ <!-- Sidebar - Desktop -->
72
+ <nav class="col-md-2 d-none d-md-block sidebar" id="sidebarDesktop">
73
+ <div class="position-sticky pt-3">
74
+ <ul class="nav flex-column">
75
+ <li class="nav-item">
76
+ <%= link_to root_path, class: "nav-link #{request.path == root_path ? 'active' : ''}" do %>
77
+ <i class="bi bi-speedometer2"></i> Overview
78
+ <% end %>
79
+ </li>
80
+ <li class="nav-item">
81
+ <%= link_to errors_path, class: "nav-link #{request.path == errors_path ? 'active' : ''}" do %>
82
+ <i class="bi bi-list-ul"></i> All Errors
83
+ <% end %>
84
+ </li>
85
+ <li class="nav-item">
86
+ <%= link_to analytics_errors_path, class: "nav-link #{request.path == analytics_errors_path ? 'active' : ''}" do %>
87
+ <i class="bi bi-graph-up"></i> Analytics
88
+ <% end %>
89
+ </li>
90
+ <% if RailsErrorDashboard.configuration.enable_platform_comparison %>
91
+ <li class="nav-item">
92
+ <%= link_to platform_comparison_errors_path, class: "nav-link #{request.path == platform_comparison_errors_path ? 'active' : ''}" do %>
93
+ <i class="bi bi-phone"></i> Platform Health
94
+ <% end %>
95
+ </li>
96
+ <% end %>
97
+ <% if RailsErrorDashboard.configuration.enable_error_correlation %>
98
+ <li class="nav-item">
99
+ <%= link_to correlation_errors_path, class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}" do %>
100
+ <i class="bi bi-diagram-3"></i> Correlation
101
+ <% end %>
102
+ </li>
103
+ <% end %>
104
+ </ul>
105
+
106
+ <hr class="my-3">
107
+
108
+ <h6 class="sidebar-heading px-3 mt-4 mb-2 text-muted text-uppercase">
109
+ <small>Quick Filters</small>
110
+ </h6>
111
+ <ul class="nav flex-column">
112
+ <li class="nav-item">
113
+ <%= link_to errors_path(unresolved: true), class: "nav-link" do %>
114
+ <i class="bi bi-exclamation-circle text-danger"></i> Unresolved
115
+ <% end %>
116
+ </li>
117
+ <li class="nav-item">
118
+ <%= link_to errors_path(platform: 'iOS'), class: "nav-link" do %>
119
+ <i class="bi bi-phone"></i> iOS Errors
120
+ <% end %>
121
+ </li>
122
+ <li class="nav-item">
123
+ <%= link_to errors_path(platform: 'Android'), class: "nav-link" do %>
124
+ <i class="bi bi-phone"></i> Android Errors
125
+ <% end %>
126
+ </li>
127
+ </ul>
128
+ </div>
129
+ </nav>
130
+
131
+ <!-- Sidebar - Mobile (Offcanvas) -->
132
+ <div class="offcanvas offcanvas-start" tabindex="-1" id="sidebarMenu" aria-labelledby="sidebarMenuLabel">
133
+ <div class="offcanvas-header">
134
+ <h5 class="offcanvas-title" id="sidebarMenuLabel">
135
+ <i class="bi bi-bug-fill"></i> Error Dashboard
136
+ </h5>
137
+ <button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
138
+ </div>
139
+ <div class="offcanvas-body">
140
+ <ul class="nav flex-column">
141
+ <li class="nav-item">
142
+ <%= link_to root_path, class: "nav-link #{request.path == root_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
143
+ <i class="bi bi-speedometer2"></i> Overview
144
+ <% end %>
145
+ </li>
146
+ <li class="nav-item">
147
+ <%= link_to errors_path, class: "nav-link #{request.path == errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
148
+ <i class="bi bi-list-ul"></i> All Errors
149
+ <% end %>
150
+ </li>
151
+ <li class="nav-item">
152
+ <%= link_to analytics_errors_path, class: "nav-link #{request.path == analytics_errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
153
+ <i class="bi bi-graph-up"></i> Analytics
154
+ <% end %>
155
+ </li>
156
+ <% if RailsErrorDashboard.configuration.enable_platform_comparison %>
157
+ <li class="nav-item">
158
+ <%= link_to platform_comparison_errors_path, class: "nav-link #{request.path == platform_comparison_errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
159
+ <i class="bi bi-phone"></i> Platform Health
160
+ <% end %>
161
+ </li>
162
+ <% end %>
163
+ <% if RailsErrorDashboard.configuration.enable_error_correlation %>
164
+ <li class="nav-item">
165
+ <%= link_to correlation_errors_path, class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
166
+ <i class="bi bi-diagram-3"></i> Correlation
167
+ <% end %>
168
+ </li>
169
+ <% end %>
170
+ </ul>
171
+
172
+ <hr class="my-3">
173
+
174
+ <h6 class="sidebar-heading px-3 mt-4 mb-2 text-muted text-uppercase">
175
+ <small>Quick Filters</small>
176
+ </h6>
177
+ <ul class="nav flex-column">
178
+ <li class="nav-item">
179
+ <%= link_to errors_path(unresolved: true), class: "nav-link", data: { bs_dismiss: "offcanvas" } do %>
180
+ <i class="bi bi-exclamation-circle text-danger"></i> Unresolved
181
+ <% end %>
182
+ </li>
183
+ <li class="nav-item">
184
+ <%= link_to errors_path(platform: 'iOS'), class: "nav-link", data: { bs_dismiss: "offcanvas" } do %>
185
+ <i class="bi bi-phone"></i> iOS Errors
186
+ <% end %>
187
+ </li>
188
+ <li class="nav-item">
189
+ <%= link_to errors_path(platform: 'Android'), class: "nav-link", data: { bs_dismiss: "offcanvas" } do %>
190
+ <i class="bi bi-phone"></i> Android Errors
191
+ <% end %>
192
+ </li>
193
+ </ul>
194
+
195
+ <hr class="my-3">
196
+
197
+ <div class="px-3">
198
+ <small class="text-muted">Environment</small>
199
+ <div class="fw-bold"><%= Rails.env.titleize %></div>
200
+ </div>
201
+ </div>
202
+ </div>
203
+
204
+ <!-- Main content -->
205
+ <main class="col-md-10 ms-sm-auto px-md-4">
206
+ <%= yield %>
207
+ </main>
208
+ </div>
209
+ </div>
210
+
211
+ <!-- Bootstrap JS -->
212
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
213
+
214
+ <!-- Theme Toggle Script -->
215
+ <script>
216
+ // Catppuccin Mocha Chart Colors
217
+ function getCatppuccinChartConfig(isDark) {
218
+ if (!isDark) {
219
+ // Light theme - keep existing for now
220
+ return {
221
+ textColor: '#1F2937',
222
+ gridColor: 'rgba(0, 0, 0, 0.1)',
223
+ tooltipBg: 'rgba(0, 0, 0, 0.8)',
224
+ colors: {
225
+ red: '#EF4444',
226
+ green: '#10B981',
227
+ yellow: '#F59E0B',
228
+ blue: '#3B82F6',
229
+ purple: '#8B5CF6',
230
+ pink: '#EC4899',
231
+ teal: '#14B8A6',
232
+ orange: '#F97316',
233
+ sapphire: '#3B82F6'
234
+ }
235
+ };
236
+ }
237
+
238
+ // Dark theme - Catppuccin Mocha
239
+ return {
240
+ textColor: '#cdd6f4', // ctp-text
241
+ gridColor: 'rgba(88, 91, 112, 0.3)', // ctp-surface2 with opacity
242
+ tooltipBg: '#313244', // ctp-surface0
243
+ colors: {
244
+ red: '#f38ba8', // ctp-red
245
+ green: '#a6e3a1', // ctp-green
246
+ yellow: '#f9e2af', // ctp-yellow
247
+ blue: '#89b4fa', // ctp-blue
248
+ purple: '#cba6f7', // ctp-mauve
249
+ pink: '#f5c2e7', // ctp-pink
250
+ teal: '#94e2d5', // ctp-teal
251
+ orange: '#fab387', // ctp-peach
252
+ sapphire: '#74c7ec' // ctp-sapphire
253
+ }
254
+ };
255
+ }
256
+
257
+ // Get platform-specific color
258
+ function getPlatformColor(platform, isDark) {
259
+ const config = getCatppuccinChartConfig(isDark);
260
+ const platformMap = {
261
+ 'ios': isDark ? config.textColor : '#000000',
262
+ 'android': config.colors.green,
263
+ 'web': config.colors.blue,
264
+ 'api': config.colors.sapphire
265
+ };
266
+ return platformMap[platform.toLowerCase()] || config.colors.purple;
267
+ }
268
+
269
+ // Update Chart.js defaults for dark/light theme
270
+ function updateChartTheme(isDark) {
271
+ if (typeof Chart !== 'undefined') {
272
+ const config = getCatppuccinChartConfig(isDark);
273
+
274
+ Chart.defaults.color = config.textColor;
275
+ Chart.defaults.borderColor = config.gridColor;
276
+ Chart.defaults.scale.grid.color = config.gridColor;
277
+ Chart.defaults.scale.ticks.color = config.textColor;
278
+ Chart.defaults.plugins.legend.labels.color = config.textColor;
279
+ Chart.defaults.plugins.tooltip.backgroundColor = config.tooltipBg;
280
+ Chart.defaults.plugins.tooltip.titleColor = config.textColor;
281
+ Chart.defaults.plugins.tooltip.bodyColor = config.textColor;
282
+ Chart.defaults.plugins.tooltip.borderColor = config.gridColor;
283
+ }
284
+ }
285
+
286
+ // Load theme from localStorage on page load
287
+ document.addEventListener('DOMContentLoaded', function() {
288
+ console.log('📄 DOMContentLoaded - Loading theme from localStorage');
289
+
290
+ const savedTheme = localStorage.getItem('theme') || 'light';
291
+ const isDark = savedTheme === 'dark';
292
+
293
+ console.log('Saved theme:', savedTheme, '| isDark:', isDark);
294
+
295
+ if (isDark) {
296
+ document.body.classList.add('dark-mode');
297
+ document.documentElement.setAttribute('data-theme', 'dark');
298
+ console.log('✅ Applied dark theme (body.dark-mode + html[data-theme=dark])');
299
+ updateThemeIcon(true);
300
+ } else {
301
+ // Ensure light mode is clean
302
+ document.body.classList.remove('dark-mode');
303
+ document.documentElement.removeAttribute('data-theme');
304
+ console.log('✅ Applied light theme (removed classes)');
305
+ updateThemeIcon(false);
306
+ }
307
+
308
+ // Update Chart.js theme
309
+ updateChartTheme(isDark);
310
+ console.log('📊 Chart.js theme updated');
311
+ });
312
+
313
+ function toggleTheme() {
314
+ try {
315
+ console.log('🎨 Toggle theme clicked');
316
+
317
+ const body = document.body;
318
+ const isDark = body.classList.toggle('dark-mode');
319
+
320
+ console.log('Dark mode:', isDark);
321
+
322
+ // Sync with html data attribute
323
+ if (isDark) {
324
+ document.documentElement.setAttribute('data-theme', 'dark');
325
+ console.log('✅ Set data-theme=dark');
326
+ } else {
327
+ document.documentElement.removeAttribute('data-theme');
328
+ console.log('✅ Removed data-theme');
329
+ }
330
+
331
+ // Save preference
332
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
333
+ console.log('💾 Saved to localStorage:', isDark ? 'dark' : 'light');
334
+
335
+ // Update icon
336
+ updateThemeIcon(isDark);
337
+ console.log('🌙 Updated icon');
338
+
339
+ // Update Chart.js theme
340
+ updateChartTheme(isDark);
341
+ console.log('📊 Updated Chart.js');
342
+
343
+ // Reload page to apply chart theme (Chart.js requires re-render)
344
+ console.log('🔄 Reloading page...');
345
+ setTimeout(() => location.reload(), 100);
346
+ } catch (error) {
347
+ console.error('❌ Error in toggleTheme:', error);
348
+ alert('Error toggling theme: ' + error.message);
349
+ }
350
+ }
351
+
352
+ function updateThemeIcon(isDark) {
353
+ const icon = document.getElementById('themeIcon');
354
+ if (isDark) {
355
+ icon.className = 'bi bi-sun-fill';
356
+ } else {
357
+ icon.className = 'bi bi-moon-fill';
358
+ }
359
+ }
360
+
361
+ // Loading indicator for form submissions and link clicks
362
+ const loadingIndicator = document.getElementById('loading-indicator');
363
+
364
+ // Show loading on form submit
365
+ document.addEventListener('submit', function() {
366
+ loadingIndicator.classList.add('active');
367
+ });
368
+
369
+ // Show loading on link clicks (except anchors)
370
+ document.addEventListener('click', function(e) {
371
+ const link = e.target.closest('a');
372
+ if (link && link.href && !link.href.startsWith('#') && !link.target) {
373
+ loadingIndicator.classList.add('active');
374
+ }
375
+ });
376
+
377
+ // Hide loading when page loads
378
+ window.addEventListener('load', function() {
379
+ loadingIndicator.classList.remove('active');
380
+ });
381
+ </script>
382
+ </body>
383
+ </html>
@@ -43,6 +43,8 @@
43
43
  <span class="badge badge-ios">iOS</span>
44
44
  <% elsif error.platform == 'Android' %>
45
45
  <span class="badge badge-android">Android</span>
46
+ <% elsif error.platform == 'Web' %>
47
+ <span class="badge badge-web">Web</span>
46
48
  <% else %>
47
49
  <span class="badge badge-api"><%= error.platform || 'API' %></span>
48
50
  <% end %>
@@ -91,15 +91,15 @@
91
91
  <% max_count = pattern_data[:hourly_distribution].values.max || 1 %>
92
92
  <% intensity = max_count > 0 ? (count.to_f / max_count * 100).round : 0 %>
93
93
  <% is_peak = pattern_data[:peak_hours].include?(hour) %>
94
- <div class="text-center m-1" style="width: 45px;">
95
- <div class="rounded p-2 <%= is_peak ? 'border border-danger border-2' : '' %>"
94
+ <div class="text-center m-1 heatmap-cell-wrapper" style="width: 45px;">
95
+ <div class="rounded p-2 heatmap-cell <%= is_peak ? 'border border-danger border-2' : '' %>"
96
96
  style="background-color: rgba(220, 53, 69, <%= intensity / 100.0 %>); min-height: 50px; display: flex; align-items: center; justify-content: center;"
97
97
  title="<%= hour %>:00 - <%= count %> errors">
98
- <small class="fw-bold <%= intensity > 50 ? 'text-white' : 'text-dark' %>">
98
+ <small class="fw-bold heatmap-count <%= intensity > 50 ? 'text-white' : 'text-dark' %>">
99
99
  <%= count %>
100
100
  </small>
101
101
  </div>
102
- <small class="text-muted"><%= hour %></small>
102
+ <small class="heatmap-hour"><%= hour %></small>
103
103
  </div>
104
104
  <% end %>
105
105
  </div>
@@ -0,0 +1,167 @@
1
+ <%# Timeline view for related errors leading up to this error %>
2
+ <% if @related_errors.any? %>
3
+ <div class="card">
4
+ <div class="card-header bg-white">
5
+ <h5 class="mb-0">
6
+ <i class="bi bi-clock-history"></i> Timeline
7
+ <small class="text-muted ms-2">Recent related errors</small>
8
+ </h5>
9
+ </div>
10
+ <div class="card-body">
11
+ <div class="timeline">
12
+ <% @related_errors.each_with_index do |error, index| %>
13
+ <div class="timeline-item <%= index == @related_errors.length - 1 ? 'timeline-item-last' : '' %>">
14
+ <div class="timeline-marker <%= error.id == @error.id ? 'timeline-marker-current' : '' %>">
15
+ <% if error.id == @error.id %>
16
+ <i class="bi bi-exclamation-circle-fill text-danger"></i>
17
+ <% elsif error.resolved? %>
18
+ <i class="bi bi-check-circle-fill text-success"></i>
19
+ <% else %>
20
+ <i class="bi bi-circle-fill text-secondary"></i>
21
+ <% end %>
22
+ </div>
23
+
24
+ <div class="timeline-content">
25
+ <div class="d-flex justify-content-between align-items-start mb-1">
26
+ <div class="flex-grow-1">
27
+ <% if error.id == @error.id %>
28
+ <strong class="text-danger">
29
+ <i class="bi bi-arrow-right-circle"></i> Current Error
30
+ </strong>
31
+ <% else %>
32
+ <%= link_to error_path(error), class: "text-decoration-none" do %>
33
+ <strong><%= error.error_type %></strong>
34
+ <% end %>
35
+ <% end %>
36
+
37
+ <% if error.platform.present? %>
38
+ <% if error.platform == 'iOS' %>
39
+ <span class="badge badge-ios ms-2">iOS</span>
40
+ <% elsif error.platform == 'Android' %>
41
+ <span class="badge badge-android ms-2">Android</span>
42
+ <% else %>
43
+ <span class="badge badge-api ms-2"><%= error.platform %></span>
44
+ <% end %>
45
+ <% end %>
46
+ </div>
47
+
48
+ <small class="text-muted">
49
+ <%= time_ago_in_words(error.occurred_at) %> ago
50
+ </small>
51
+ </div>
52
+
53
+ <% if error.message.present? %>
54
+ <p class="mb-1 text-muted small">
55
+ <%= truncate(error.message, length: 100) %>
56
+ </p>
57
+ <% end %>
58
+
59
+ <div class="d-flex align-items-center gap-2 small">
60
+ <% if error.user_id %>
61
+ <span class="text-muted">
62
+ <i class="bi bi-person"></i>
63
+ User: <%= error.user_id %>
64
+ </span>
65
+ <% end %>
66
+
67
+ <% if error.occurrence_count > 1 %>
68
+ <span class="text-muted">
69
+ <i class="bi bi-arrow-repeat"></i>
70
+ <%= error.occurrence_count %>x
71
+ </span>
72
+ <% end %>
73
+
74
+ <% if error.resolved? %>
75
+ <span class="badge bg-success">
76
+ <i class="bi bi-check-circle"></i> Resolved
77
+ </span>
78
+ <% end %>
79
+ </div>
80
+ </div>
81
+ </div>
82
+ <% end %>
83
+ </div>
84
+ </div>
85
+ </div>
86
+
87
+ <style>
88
+ .timeline {
89
+ position: relative;
90
+ padding-left: 0;
91
+ }
92
+
93
+ .timeline-item {
94
+ position: relative;
95
+ padding-left: 40px;
96
+ padding-bottom: 30px;
97
+ }
98
+
99
+ .timeline-item-last {
100
+ padding-bottom: 0;
101
+ }
102
+
103
+ .timeline-item::before {
104
+ content: '';
105
+ position: absolute;
106
+ left: 11px;
107
+ top: 30px;
108
+ bottom: 0;
109
+ width: 2px;
110
+ background: var(--bs-border-color);
111
+ }
112
+
113
+ .timeline-item-last::before {
114
+ display: none;
115
+ }
116
+
117
+ .timeline-marker {
118
+ position: absolute;
119
+ left: 0;
120
+ top: 0;
121
+ width: 24px;
122
+ height: 24px;
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ background: white;
127
+ border: 2px solid var(--bs-border-color);
128
+ border-radius: 50%;
129
+ z-index: 1;
130
+ }
131
+
132
+ .timeline-marker-current {
133
+ border-color: var(--bs-danger);
134
+ background: var(--bs-danger-bg-subtle);
135
+ box-shadow: 0 0 0 4px var(--bs-danger-bg-subtle);
136
+ }
137
+
138
+ .timeline-marker i {
139
+ font-size: 12px;
140
+ }
141
+
142
+ .timeline-content {
143
+ background: var(--bs-body-bg);
144
+ padding: 12px;
145
+ border-radius: 8px;
146
+ border: 1px solid var(--bs-border-color);
147
+ }
148
+
149
+ .timeline-item:hover .timeline-content {
150
+ border-color: var(--bs-primary);
151
+ background: var(--bs-primary-bg-subtle);
152
+ }
153
+
154
+ /* Dark mode support */
155
+ body.dark-mode .timeline-marker,
156
+ html[data-theme="dark"] body .timeline-marker {
157
+ background: var(--card-bg);
158
+ border-color: var(--border-color);
159
+ }
160
+
161
+ body.dark-mode .timeline-marker-current,
162
+ html[data-theme="dark"] body .timeline-marker-current {
163
+ border-color: var(--bs-danger);
164
+ background: rgba(220, 53, 69, 0.2);
165
+ }
166
+ </style>
167
+ <% end %>