rails_error_dashboard 0.1.0 → 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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +305 -703
  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/application_controller.rb +18 -0
  11. data/app/controllers/rails_error_dashboard/errors_controller.rb +140 -4
  12. data/app/helpers/rails_error_dashboard/application_helper.rb +55 -0
  13. data/app/helpers/rails_error_dashboard/backtrace_helper.rb +91 -0
  14. data/app/helpers/rails_error_dashboard/overview_helper.rb +78 -0
  15. data/app/helpers/rails_error_dashboard/user_agent_helper.rb +118 -0
  16. data/app/jobs/rails_error_dashboard/application_job.rb +19 -0
  17. data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +48 -0
  18. data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +263 -0
  19. data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -8
  20. data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +2 -1
  21. data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +5 -5
  22. data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +10 -6
  23. data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +5 -6
  24. data/app/mailers/rails_error_dashboard/application_mailer.rb +1 -1
  25. data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +1 -1
  26. data/app/models/rails_error_dashboard/cascade_pattern.rb +74 -0
  27. data/app/models/rails_error_dashboard/error_baseline.rb +100 -0
  28. data/app/models/rails_error_dashboard/error_comment.rb +27 -0
  29. data/app/models/rails_error_dashboard/error_log.rb +471 -3
  30. data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
  31. data/app/views/layouts/rails_error_dashboard.html.erb +816 -178
  32. data/app/views/layouts/rails_error_dashboard_old_backup.html.erb +383 -0
  33. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +3 -10
  34. data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -2
  35. data/app/views/rails_error_dashboard/errors/_error_row.html.erb +78 -0
  36. data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +209 -0
  37. data/app/views/rails_error_dashboard/errors/_stats.html.erb +34 -0
  38. data/app/views/rails_error_dashboard/errors/_timeline.html.erb +167 -0
  39. data/app/views/rails_error_dashboard/errors/analytics.html.erb +152 -56
  40. data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
  41. data/app/views/rails_error_dashboard/errors/index.html.erb +294 -138
  42. data/app/views/rails_error_dashboard/errors/overview.html.erb +253 -0
  43. data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +399 -0
  44. data/app/views/rails_error_dashboard/errors/show.html.erb +781 -65
  45. data/config/routes.rb +9 -0
  46. data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +66 -0
  47. data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +26 -0
  48. data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +12 -0
  49. data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +9 -0
  50. data/db/migrate/20251225100236_create_error_occurrences.rb +31 -0
  51. data/db/migrate/20251225101920_create_cascade_patterns.rb +33 -0
  52. data/db/migrate/20251225102500_create_error_baselines.rb +38 -0
  53. data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +27 -0
  54. data/db/migrate/20251226020100_create_error_comments.rb +18 -0
  55. data/lib/generators/rails_error_dashboard/install/install_generator.rb +276 -1
  56. data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +272 -37
  57. data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +36 -0
  58. data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +55 -0
  59. data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -1
  60. data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -2
  61. data/lib/rails_error_dashboard/commands/log_error.rb +272 -7
  62. data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
  63. data/lib/rails_error_dashboard/configuration.rb +90 -5
  64. data/lib/rails_error_dashboard/error_reporter.rb +15 -7
  65. data/lib/rails_error_dashboard/logger.rb +105 -0
  66. data/lib/rails_error_dashboard/middleware/error_catcher.rb +17 -10
  67. data/lib/rails_error_dashboard/plugin.rb +6 -3
  68. data/lib/rails_error_dashboard/plugin_registry.rb +2 -2
  69. data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +0 -1
  70. data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +3 -4
  71. data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +1 -3
  72. data/lib/rails_error_dashboard/queries/analytics_stats.rb +44 -6
  73. data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
  74. data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +86 -0
  75. data/lib/rails_error_dashboard/queries/dashboard_stats.rb +242 -2
  76. data/lib/rails_error_dashboard/queries/error_cascades.rb +74 -0
  77. data/lib/rails_error_dashboard/queries/error_correlation.rb +375 -0
  78. data/lib/rails_error_dashboard/queries/errors_list.rb +106 -10
  79. data/lib/rails_error_dashboard/queries/filter_options.rb +0 -1
  80. data/lib/rails_error_dashboard/queries/platform_comparison.rb +254 -0
  81. data/lib/rails_error_dashboard/queries/similar_errors.rb +93 -0
  82. data/lib/rails_error_dashboard/services/backtrace_parser.rb +113 -0
  83. data/lib/rails_error_dashboard/services/baseline_alert_throttler.rb +88 -0
  84. data/lib/rails_error_dashboard/services/baseline_calculator.rb +269 -0
  85. data/lib/rails_error_dashboard/services/cascade_detector.rb +95 -0
  86. data/lib/rails_error_dashboard/services/pattern_detector.rb +268 -0
  87. data/lib/rails_error_dashboard/services/similarity_calculator.rb +144 -0
  88. data/lib/rails_error_dashboard/value_objects/error_context.rb +27 -1
  89. data/lib/rails_error_dashboard/version.rb +1 -1
  90. data/lib/rails_error_dashboard.rb +57 -7
  91. metadata +69 -10
  92. data/app/models/rails_error_dashboard/application_record.rb +0 -5
  93. data/lib/rails_error_dashboard/queries/developer_insights.rb +0 -277
  94. data/lib/rails_error_dashboard/queries/errors_list_v2.rb +0 -149
  95. data/lib/tasks/rails_error_dashboard_tasks.rake +0 -4
@@ -1,123 +1,208 @@
1
+ <!-- Subscribe to Turbo Stream updates -->
2
+ <%= turbo_stream_from "error_list" %>
3
+
1
4
  <div class="py-4">
2
5
  <div class="d-flex justify-content-between align-items-center mb-4">
3
6
  <h2 class="mb-0"><i class="bi bi-bug-fill text-primary"></i> Error Overview</h2>
4
7
  <div class="text-muted">
5
- <small>Last updated: <%= Time.current.strftime("%B %d, %Y %I:%M %p") %></small>
8
+ <small>
9
+ Last updated: <%= Time.current.strftime("%B %d, %Y %I:%M %p") %>
10
+ <span class="badge bg-success ms-2" id="live-indicator">
11
+ <i class="bi bi-broadcast"></i> Live
12
+ </span>
13
+ </small>
6
14
  </div>
7
15
  </div>
8
16
 
9
17
  <!-- Stats Cards -->
10
- <div class="row g-4 mb-4">
11
- <div class="col-md-3">
12
- <div class="card stat-card">
13
- <div class="card-body">
14
- <div class="stat-label mb-2">Today</div>
15
- <div class="stat-value text-info"><%= @stats[:total_today] %></div>
16
- </div>
17
- </div>
18
- </div>
19
- <div class="col-md-3">
20
- <div class="card stat-card">
21
- <div class="card-body">
22
- <div class="stat-label mb-2">This Week</div>
23
- <div class="stat-value text-primary"><%= @stats[:total_week] %></div>
24
- </div>
25
- </div>
26
- </div>
27
- <div class="col-md-3">
28
- <div class="card stat-card">
29
- <div class="card-body">
30
- <div class="stat-label mb-2">Unresolved</div>
31
- <div class="stat-value text-danger"><%= @stats[:unresolved] %></div>
32
- </div>
33
- </div>
34
- </div>
35
- <div class="col-md-3">
36
- <div class="card stat-card">
37
- <div class="card-body">
38
- <div class="stat-label mb-2">Resolved</div>
39
- <div class="stat-value text-success"><%= @stats[:resolved] %></div>
18
+ <div id="dashboard_stats" class="mb-4">
19
+ <%= render "stats", stats: @stats %>
20
+ </div>
21
+
22
+ <!-- Top Error Types (Only show if there are errors) -->
23
+ <% if @stats[:top_errors].any? %>
24
+ <div class="row g-4 mb-4">
25
+ <div class="col-md-12">
26
+ <div class="card">
27
+ <div class="card-header bg-white">
28
+ <h5 class="mb-0">Top Error Types</h5>
29
+ </div>
30
+ <div class="card-body">
31
+ <div class="row">
32
+ <% @stats[:top_errors].first(5).each do |error_type, count| %>
33
+ <div class="col-md-2">
34
+ <div class="text-center p-3 border rounded">
35
+ <div class="fw-bold text-danger" style="font-size: 1.5rem;"><%= count %></div>
36
+ <small class="text-muted text-truncate d-block" title="<%= error_type %>"><%= error_type.split('::').last %></small>
37
+ </div>
38
+ </div>
39
+ <% end %>
40
+ </div>
41
+ </div>
40
42
  </div>
41
43
  </div>
42
44
  </div>
43
- </div>
45
+ <% end %>
44
46
 
45
- <!-- Platform & Environment Breakdown -->
46
- <div class="row g-4 mb-4">
47
- <div class="col-md-6">
48
- <div class="card">
49
- <div class="card-header bg-white">
50
- <h5 class="mb-0">Platform Breakdown</h5>
51
- </div>
52
- <div class="card-body">
53
- <% if @stats[:by_platform].any? %>
54
- <% @stats[:by_platform].each do |platform, count| %>
55
- <div class="d-flex justify-content-between align-items-center mb-2">
56
- <span>
57
- <% if platform == 'iOS' %>
58
- <span class="badge badge-ios">iOS</span>
59
- <% elsif platform == 'Android' %>
60
- <span class="badge badge-android">Android</span>
61
- <% else %>
62
- <span class="badge badge-api">API</span>
63
- <% end %>
64
- </span>
65
- <span class="fw-bold"><%= count %></span>
66
- </div>
47
+ <!-- Spike Detection Alert -->
48
+ <% if @stats[:spike_detected] %>
49
+ <div class="alert alert-warning mb-4" role="alert">
50
+ <div class="d-flex align-items-center">
51
+ <i class="bi bi-exclamation-triangle-fill fs-3 me-3"></i>
52
+ <div>
53
+ <h5 class="alert-heading mb-1">
54
+ <% case @stats[:spike_info][:severity] %>
55
+ <% when :critical %>
56
+ 🚨 Critical Error Spike Detected!
57
+ <% when :high %>
58
+ ⚠️ High Error Spike Detected
59
+ <% when :elevated %>
60
+ 📈 Elevated Error Activity
67
61
  <% end %>
68
- <% else %>
69
- <p class="text-muted mb-0">No errors recorded yet</p>
70
- <% end %>
62
+ </h5>
63
+ <p class="mb-0">
64
+ Today: <strong><%= @stats[:spike_info][:today_count] %> errors</strong>
65
+ (7-day avg: <%= @stats[:spike_info][:avg_count] %>) —
66
+ <strong><%= @stats[:spike_info][:multiplier] %>x normal levels</strong>
67
+ </p>
71
68
  </div>
72
69
  </div>
73
70
  </div>
71
+ <% end %>
74
72
 
75
- <div class="col-md-6">
76
- <div class="card">
77
- <div class="card-header bg-white">
78
- <h5 class="mb-0">Top Error Types</h5>
79
- </div>
80
- <div class="card-body">
81
- <% if @stats[:top_errors].any? %>
82
- <% @stats[:top_errors].first(5).each do |error_type, count| %>
83
- <div class="d-flex justify-content-between align-items-center mb-2">
84
- <small class="text-truncate" style="max-width: 70%;"><%= error_type %></small>
85
- <span class="badge bg-secondary"><%= count %></span>
86
- </div>
73
+ <!-- 7-Day Error Trend -->
74
+ <% if @stats[:errors_trend_7d]&.any? %>
75
+ <div class="row g-4 mb-4">
76
+ <div class="col-md-8">
77
+ <div class="card">
78
+ <div class="card-header bg-white d-flex justify-content-between align-items-center">
79
+ <h5 class="mb-0"><i class="bi bi-graph-up"></i> 7-Day Error Trend</h5>
80
+ <%= link_to analytics_errors_path, class: "btn btn-sm btn-outline-primary" do %>
81
+ <i class="bi bi-bar-chart"></i> Full Analytics
87
82
  <% end %>
88
- <% else %>
89
- <p class="text-muted mb-0">No errors recorded yet</p>
90
- <% end %>
83
+ </div>
84
+ <div class="card-body">
85
+ <%= line_chart @stats[:errors_trend_7d],
86
+ color: "#8B5CF6",
87
+ curve: false,
88
+ points: true,
89
+ height: "250px",
90
+ library: {
91
+ plugins: {
92
+ legend: { display: false }
93
+ },
94
+ scales: {
95
+ y: {
96
+ beginAtZero: true,
97
+ ticks: { precision: 0 }
98
+ }
99
+ }
100
+ } %>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ <div class="col-md-4">
105
+ <div class="card">
106
+ <div class="card-header bg-white">
107
+ <h5 class="mb-0"><i class="bi bi-pie-chart"></i> By Severity (7d)</h5>
108
+ </div>
109
+ <div class="card-body">
110
+ <%= pie_chart @stats[:errors_by_severity_7d],
111
+ colors: ["#EF4444", "#F59E0B", "#3B82F6", "#6B7280"],
112
+ height: "250px",
113
+ legend: "bottom",
114
+ donut: true %>
115
+ </div>
91
116
  </div>
92
117
  </div>
93
118
  </div>
94
- </div>
119
+ <% end %>
95
120
 
96
121
  <!-- Filters -->
97
- <div class="card mb-4">
122
+ <div class="card mb-4" id="filters-section">
98
123
  <div class="card-header bg-white">
99
124
  <h5 class="mb-0">Filters & Search</h5>
100
125
  </div>
101
126
  <div class="card-body">
102
- <%= form_with url: errors_path, method: :get, class: "row g-3" do %>
103
- <div class="col-md-3">
127
+ <%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>
128
+ <div class="col-md-4">
104
129
  <%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
105
130
  </div>
131
+
132
+ <% if @platforms.size > 1 %>
133
+ <div class="col-md-2">
134
+ <%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select" %>
135
+ </div>
136
+ <% end %>
137
+
138
+ <div class="col-md-2">
139
+ <%= select_tag :error_type, options_for_select([['All Types', '']] + @error_types.map { |t| [t, t] }, params[:error_type]), class: "form-select" %>
140
+ </div>
141
+
106
142
  <div class="col-md-2">
107
- <%= select_tag :environment, options_for_select([['All Environments', '']] + @environments.map { |e| [e.titleize, e] }, params[:environment]), class: "form-select" %>
143
+ <%= select_tag :severity, options_for_select([
144
+ ['All Severities', ''],
145
+ ['🔴 Critical', 'critical'],
146
+ ['🟠 High', 'high'],
147
+ ['🟡 Medium', 'medium'],
148
+ ['⚪ Low', 'low']
149
+ ], params[:severity]), class: "form-select" %>
108
150
  </div>
151
+
152
+ <!-- Phase 3: Workflow Filters -->
109
153
  <div class="col-md-2">
110
- <%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select" %>
154
+ <%= select_tag :status, options_for_select([
155
+ ['All Statuses', ''],
156
+ ['🆕 New', 'new'],
157
+ ['🔵 In Progress', 'in_progress'],
158
+ ['🟡 Investigating', 'investigating'],
159
+ ['✅ Resolved', 'resolved'],
160
+ ['⚫ Won\'t Fix', 'wont_fix']
161
+ ], params[:status]), class: "form-select" %>
111
162
  </div>
112
- <div class="col-md-3">
113
- <%= select_tag :error_type, options_for_select([['All Types', '']] + @error_types.map { |t| [t, t] }, params[:error_type]), class: "form-select" %>
163
+
164
+ <div class="col-md-2">
165
+ <%= select_tag :assigned_to, options_for_select([
166
+ ['All Assignments', ''],
167
+ ['Unassigned', '__unassigned__'],
168
+ ['Assigned', '__assigned__']
169
+ ], params[:assigned_to]), class: "form-select" %>
114
170
  </div>
171
+
115
172
  <div class="col-md-2">
116
- <div class="form-check">
117
- <%= check_box_tag :unresolved, true, params[:unresolved] == 'true', class: "form-check-input" %>
173
+ <%= select_tag :priority_level, options_for_select([
174
+ ['All Priorities', ''],
175
+ ['🔴 Critical (P3)', 3],
176
+ ['🟠 High (P2)', 2],
177
+ ['🟡 Medium (P1)', 1],
178
+ ['⚪ Low (P0)', 0]
179
+ ], params[:priority_level]), class: "form-select" %>
180
+ </div>
181
+
182
+ <div class="col-auto">
183
+ <div class="form-check mt-2">
184
+ <%
185
+ # Determine checkbox state based on params
186
+ # If unresolved param is missing or nil, default to checked (true)
187
+ # If unresolved param is explicitly "false" or "0", uncheck it
188
+ is_checked = if params[:unresolved].nil?
189
+ true # Default to checked when first loading
190
+ else
191
+ params[:unresolved] != "false" && params[:unresolved] != "0"
192
+ end
193
+ %>
194
+ <%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox" %>
118
195
  <%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
119
196
  </div>
120
197
  </div>
198
+
199
+ <div class="col-auto">
200
+ <div class="form-check mt-2">
201
+ <%= check_box_tag :hide_snoozed, "1", params[:hide_snoozed] == "1", class: "form-check-input" %>
202
+ <%= label_tag :hide_snoozed, "Hide snoozed", class: "form-check-label" %>
203
+ </div>
204
+ </div>
205
+
121
206
  <div class="col-12">
122
207
  <%= submit_tag "Apply Filters", class: "btn btn-primary" %>
123
208
  <%= link_to "Clear", errors_path, class: "btn btn-outline-secondary" %>
@@ -168,67 +253,21 @@
168
253
  <th style="width: 40px;">
169
254
  <input type="checkbox" id="select-all" class="form-check-input">
170
255
  </th>
171
- <th>Time</th>
256
+ <th>Severity</th>
172
257
  <th>Error Type</th>
173
258
  <th>Message</th>
174
- <th>Platform</th>
175
- <th>Environment</th>
176
- <th>User</th>
259
+ <th>Occurrences</th>
260
+ <th>First / Last Seen</th>
261
+ <% if @platforms.size > 1 %>
262
+ <th>Platform</th>
263
+ <% end %>
177
264
  <th>Status</th>
178
265
  <th></th>
179
266
  </tr>
180
267
  </thead>
181
- <tbody>
268
+ <tbody id="error_list">
182
269
  <% @errors.each do |error| %>
183
- <tr>
184
- <td onclick="event.stopPropagation();">
185
- <input type="checkbox" class="error-checkbox form-check-input" value="<%= error.id %>" data-error-id="<%= error.id %>">
186
- </td>
187
- <td onclick="window.location='<%= error_path(error) %>';">
188
- <small><%= error.occurred_at.strftime("%m/%d %I:%M%p") %></small>
189
- </td>
190
- <td onclick="window.location='<%= error_path(error) %>';">
191
- <code class="text-danger"><%= error.error_type.split('::').last %></code>
192
- </td>
193
- <td onclick="window.location='<%= error_path(error) %>';">
194
- <div class="text-truncate" style="max-width: 300px;" title="<%= error.message %>">
195
- <%= error.message %>
196
- </div>
197
- </td>
198
- <td onclick="window.location='<%= error_path(error) %>';">
199
- <% if error.platform == 'iOS' %>
200
- <span class="badge badge-ios">iOS</span>
201
- <% elsif error.platform == 'Android' %>
202
- <span class="badge badge-android">Android</span>
203
- <% else %>
204
- <span class="badge badge-api"><%= error.platform || 'API' %></span>
205
- <% end %>
206
- </td>
207
- <td onclick="window.location='<%= error_path(error) %>';">
208
- <span class="badge <%= error.environment == 'production' ? 'bg-danger' : 'bg-info' %>">
209
- <%= error.environment %>
210
- </span>
211
- </td>
212
- <td onclick="window.location='<%= error_path(error) %>';">
213
- <% if error.user %>
214
- <small><%= error.user.email %></small>
215
- <% else %>
216
- <small class="text-muted">Guest</small>
217
- <% end %>
218
- </td>
219
- <td onclick="window.location='<%= error_path(error) %>';">
220
- <% if error.resolved? %>
221
- <i class="bi bi-check-circle-fill text-success" title="Resolved"></i>
222
- <% else %>
223
- <i class="bi bi-exclamation-circle-fill text-danger" title="Unresolved"></i>
224
- <% end %>
225
- </td>
226
- <td onclick="event.stopPropagation();">
227
- <%= link_to error_path(error), class: "btn btn-sm btn-outline-primary" do %>
228
- <i class="bi bi-eye"></i>
229
- <% end %>
230
- </td>
231
- </tr>
270
+ <%= render "error_row", error: error, show_platform: @platforms.size > 1 %>
232
271
  <% end %>
233
272
  </tbody>
234
273
  </table>
@@ -240,8 +279,20 @@
240
279
  </div>
241
280
  <% else %>
242
281
  <div class="text-center py-5">
243
- <i class="bi bi-inbox display-1 text-muted"></i>
244
- <p class="text-muted mt-3">No errors found</p>
282
+ <i class="bi bi-check-circle display-1 text-success mb-3"></i>
283
+ <h4 class="text-muted">All Clear!</h4>
284
+ <p class="text-muted">
285
+ <% if params[:search].present? || params[:error_type].present? || params[:platform].present? || params[:severity].present? %>
286
+ No errors match your current filters. Try adjusting your search criteria.
287
+ <% else %>
288
+ No errors have been logged yet. Your application is running smoothly!
289
+ <% end %>
290
+ </p>
291
+ <% unless params.values.compact.any? %>
292
+ <small class="text-muted d-block mt-3">
293
+ <i class="bi bi-lightbulb"></i> Errors will appear here automatically when they occur.
294
+ </small>
295
+ <% end %>
245
296
  </div>
246
297
  <% end %>
247
298
  </div>
@@ -331,4 +382,109 @@
331
382
  });
332
383
  }
333
384
  });
385
+
386
+ // Turbo Stream animation for new errors
387
+ document.addEventListener('turbo:before-stream-render', (event) => {
388
+ const { target, action } = event.detail.newStream;
389
+
390
+ // Highlight new errors when prepended
391
+ if (action === 'prepend' && target === 'error_list') {
392
+ setTimeout(() => {
393
+ const firstRow = document.querySelector('#error_list tr:first-child');
394
+ if (firstRow) {
395
+ firstRow.classList.add('new-error');
396
+
397
+ // Remove class after animation completes
398
+ setTimeout(() => {
399
+ firstRow.classList.remove('new-error');
400
+ }, 3000);
401
+ }
402
+ }, 10);
403
+ }
404
+
405
+ // Pulse stats cards when updated
406
+ if (action === 'replace' && target === 'dashboard_stats') {
407
+ setTimeout(() => {
408
+ const statCards = document.querySelectorAll('.stat-card');
409
+ statCards.forEach(card => {
410
+ card.classList.add('updated');
411
+ setTimeout(() => card.classList.remove('updated'), 500);
412
+ });
413
+ }, 10);
414
+ }
415
+ });
416
+
417
+ // Initialize Bootstrap tooltips
418
+ document.addEventListener('DOMContentLoaded', function() {
419
+ const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
420
+ tooltipTriggerList.map(function (tooltipTriggerEl) {
421
+ return new bootstrap.Tooltip(tooltipTriggerEl);
422
+ });
423
+ });
424
+
425
+ // Keyboard shortcuts
426
+ document.addEventListener('keydown', function(e) {
427
+ // Ignore if typing in input/textarea
428
+ if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
429
+
430
+ // 'r' - Refresh page
431
+ if (e.key === 'r') {
432
+ location.reload();
433
+ }
434
+
435
+ // '/' - Focus search
436
+ if (e.key === '/') {
437
+ e.preventDefault();
438
+ const searchInput = document.querySelector('input[name="search"]');
439
+ if (searchInput) searchInput.focus();
440
+ }
441
+
442
+ // 'a' - Go to analytics
443
+ if (e.key === 'a') {
444
+ window.location.href = '<%= analytics_errors_path %>';
445
+ }
446
+
447
+ // '?' - Show keyboard shortcuts help
448
+ if (e.key === '?') {
449
+ e.preventDefault();
450
+ alert('Keyboard Shortcuts:\n\n' +
451
+ 'r - Refresh page\n' +
452
+ '/ - Focus search\n' +
453
+ 'a - Analytics page\n' +
454
+ '? - Show this help');
455
+ }
456
+ });
457
+
458
+ // Preserve scroll position on page load
459
+ document.addEventListener('DOMContentLoaded', function() {
460
+ const scrollPos = sessionStorage.getItem('scrollPos');
461
+ if (scrollPos) {
462
+ window.scrollTo(0, parseInt(scrollPos));
463
+ sessionStorage.removeItem('scrollPos');
464
+ }
465
+ });
466
+
467
+ // Handle filter form submission
468
+ document.addEventListener('DOMContentLoaded', function() {
469
+ const filterForm = document.querySelector('form[action="<%= errors_path %>"]');
470
+ if (filterForm) {
471
+ filterForm.addEventListener('submit', function(e) {
472
+ // Save scroll position before form submission
473
+ sessionStorage.setItem('scrollPos', window.pageYOffset);
474
+
475
+ // Handle unchecked checkbox - when unchecked, send "0" explicitly
476
+ const unresolvedCheckbox = document.getElementById('unresolved_checkbox');
477
+ if (unresolvedCheckbox && !unresolvedCheckbox.checked) {
478
+ // Create hidden input to send "0" when unchecked
479
+ const hiddenInput = document.createElement('input');
480
+ hiddenInput.type = 'hidden';
481
+ hiddenInput.name = 'unresolved';
482
+ hiddenInput.value = '0';
483
+ filterForm.appendChild(hiddenInput);
484
+ // Remove the checkbox from form submission to avoid sending empty value
485
+ unresolvedCheckbox.disabled = true;
486
+ }
487
+ });
488
+ }
489
+ });
334
490
  </script>