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
@@ -4,7 +4,19 @@
4
4
  <%= link_to errors_path, class: "btn btn-outline-secondary btn-sm mb-2" do %>
5
5
  <i class="bi bi-arrow-left"></i> Back to Errors
6
6
  <% end %>
7
- <h2 class="mb-0">Error Details</h2>
7
+ <h2 class="mb-0">
8
+ Error Details
9
+ <% severity = @error.severity %>
10
+ <% if severity == :critical %>
11
+ <span class="badge bg-danger fs-5">CRITICAL</span>
12
+ <% elsif severity == :high %>
13
+ <span class="badge bg-warning text-dark fs-5">HIGH</span>
14
+ <% elsif severity == :medium %>
15
+ <span class="badge bg-info text-dark fs-5">MEDIUM</span>
16
+ <% else %>
17
+ <span class="badge bg-secondary fs-5">LOW</span>
18
+ <% end %>
19
+ </h2>
8
20
  </div>
9
21
  <div>
10
22
  <% if @error.resolved? %>
@@ -32,11 +44,72 @@
32
44
  <%= @error.message %>
33
45
  </div>
34
46
 
35
- <h6 class="text-muted mb-2 mt-4">Backtrace:</h6>
47
+ <h6 class="text-muted mb-2 mt-4">
48
+ Backtrace:
49
+ <% if @error.backtrace.present? %>
50
+ <% frames = parse_backtrace(@error.backtrace) %>
51
+ <% app_frames = filter_app_code(frames) %>
52
+ <% framework_frames = filter_framework_code(frames) %>
53
+ <small class="text-muted">
54
+ (<%= app_frames.count %> your code, <%= framework_frames.count %> framework/gems)
55
+ </small>
56
+ <% end %>
57
+ </h6>
36
58
  <% if @error.backtrace.present? %>
37
- <div class="bg-dark text-light p-3 rounded" style="max-height: 400px; overflow-y: auto;">
38
- <pre class="mb-0"><code><%= @error.backtrace %></code></pre>
39
- </div>
59
+ <% frames = parse_backtrace(@error.backtrace) %>
60
+ <% app_frames = filter_app_code(frames) %>
61
+ <% framework_frames = filter_framework_code(frames) %>
62
+
63
+ <% if app_frames.any? %>
64
+ <!-- Your Code Section (Expanded by default) -->
65
+ <div class="card mb-3">
66
+ <div class="card-header bg-success bg-opacity-10 border-success">
67
+ <div class="d-flex justify-content-between align-items-center">
68
+ <strong class="text-success">
69
+ <i class="bi bi-code-square"></i> Your Code
70
+ </strong>
71
+ <span class="badge bg-success"><%= app_frames.count %> frames</span>
72
+ </div>
73
+ </div>
74
+ <div class="card-body p-0">
75
+ <div class="code-block p-3" style="max-height: 300px; overflow-y: auto; overflow-x: auto;">
76
+ <pre class="mb-0"><code><% app_frames.each do |frame| %><span class="<%= frame_bg_class(frame[:category]) %> d-block px-2 py-1"><span class="<%= frame_color_class(frame[:category]) %>"><%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %></span> in <span class="text-info">`<%= frame[:method_name] %>'</span></span>
77
+ <% end %></code></pre>
78
+ </div>
79
+ </div>
80
+ </div>
81
+ <% end %>
82
+
83
+ <% if framework_frames.any? %>
84
+ <!-- Framework/Gem Code Section (Collapsed by default) -->
85
+ <div class="accordion" id="frameworkBacktraceAccordion">
86
+ <div class="accordion-item">
87
+ <h2 class="accordion-header">
88
+ <button class="accordion-button collapsed bg-light" type="button" data-bs-toggle="collapse" data-bs-target="#frameworkBacktrace" aria-expanded="false" aria-controls="frameworkBacktrace">
89
+ <i class="bi bi-gear me-2 text-warning"></i>
90
+ <strong>Framework & Gem Code</strong>
91
+ <span class="badge bg-secondary ms-2"><%= framework_frames.count %> frames</span>
92
+ <small class="text-muted ms-2">(click to expand)</small>
93
+ </button>
94
+ </h2>
95
+ <div id="frameworkBacktrace" class="accordion-collapse collapse" data-bs-parent="#frameworkBacktraceAccordion">
96
+ <div class="accordion-body p-0">
97
+ <div class="code-block p-3" style="max-height: 400px; overflow-y: auto; overflow-x: auto;">
98
+ <pre class="mb-0"><code><% framework_frames.each do |frame| %><span class="<%= frame_bg_class(frame[:category]) %> d-block px-2 py-1"><span class="<%= frame_color_class(frame[:category]) %>"><%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %></span> in <span class="text-info">`<%= frame[:method_name] %>'</span></span>
99
+ <% end %></code></pre>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ <% end %>
106
+
107
+ <% if app_frames.empty? && framework_frames.empty? %>
108
+ <!-- Fallback: Show raw backtrace if parsing failed -->
109
+ <div class="code-block p-3 rounded" style="max-height: 400px; overflow-y: auto; overflow-x: auto;">
110
+ <pre class="mb-0"><code><%= @error.backtrace %></code></pre>
111
+ </div>
112
+ <% end %>
40
113
  <% else %>
41
114
  <p class="text-muted">No backtrace available</p>
42
115
  <% end %>
@@ -66,7 +139,28 @@
66
139
  </tr>
67
140
  <tr>
68
141
  <th>User Agent:</th>
69
- <td><small><%= @error.user_agent || 'N/A' %></small></td>
142
+ <td>
143
+ <% if @error.user_agent.present? %>
144
+ <% ua_info = parse_user_agent(@error.user_agent) %>
145
+ <div class="mb-2">
146
+ <%= browser_icon(ua_info) %>
147
+ <strong><%= ua_info[:browser_name] %></strong>
148
+ <% if ua_info[:browser_version].present? %>
149
+ <span class="text-muted"><%= ua_info[:browser_version] %></span>
150
+ <% end %>
151
+ <span class="ms-2"><%= os_icon(ua_info) %> <%= ua_info[:os_name] %></span>
152
+ <span class="ms-2"><%= device_icon(ua_info) %> <%= ua_info[:device_type] %></span>
153
+ </div>
154
+ <details>
155
+ <summary class="text-muted" style="cursor: pointer;">
156
+ <small>Raw User Agent</small>
157
+ </summary>
158
+ <small class="text-muted"><%= @error.user_agent %></small>
159
+ </details>
160
+ <% else %>
161
+ <span class="text-muted">N/A</span>
162
+ <% end %>
163
+ </td>
70
164
  </tr>
71
165
  <tr>
72
166
  <th>IP Address:</th>
@@ -76,60 +170,355 @@
76
170
  </div>
77
171
  </div>
78
172
 
79
- <!-- Related Errors -->
80
- <% if @related_errors.any? %>
81
- <div class="card">
82
- <div class="card-header bg-white">
83
- <h5 class="mb-0"><i class="bi bi-collection"></i> Related Errors</h5>
173
+ <!-- Similar Errors (Fuzzy Matching) -->
174
+ <% if RailsErrorDashboard.configuration.enable_similar_errors && @error.respond_to?(:similar_errors) %>
175
+ <% similar = @error.similar_errors(threshold: 0.6, limit: 5) %>
176
+ <% if similar.any? %>
177
+ <div class="card mb-4">
178
+ <div class="card-header bg-white">
179
+ <h5 class="mb-0">
180
+ <i class="bi bi-diagram-3"></i> Similar Errors
181
+ <span class="badge bg-info text-dark">Fuzzy Matching</span>
182
+ </h5>
183
+ <small class="text-muted">Errors with similar backtraces and messages (60%+ similarity)</small>
184
+ </div>
185
+ <div class="card-body p-0">
186
+ <div class="table-responsive">
187
+ <table class="table table-hover mb-0">
188
+ <thead class="table-light">
189
+ <tr>
190
+ <th>Similarity</th>
191
+ <th>Error Type</th>
192
+ <th>Message</th>
193
+ <th>Platform</th>
194
+ <th>Occurrences</th>
195
+ <th></th>
196
+ </tr>
197
+ </thead>
198
+ <tbody>
199
+ <% similar.each do |item| %>
200
+ <% similar_error = item[:error] %>
201
+ <% similarity_pct = (item[:similarity] * 100).round %>
202
+ <tr>
203
+ <td>
204
+ <div class="progress" style="width: 60px; height: 20px;">
205
+ <div class="progress-bar bg-success" role="progressbar"
206
+ style="width: <%= similarity_pct %>%"
207
+ aria-valuenow="<%= similarity_pct %>"
208
+ aria-valuemin="0"
209
+ aria-valuemax="100">
210
+ <%= similarity_pct %>%
211
+ </div>
212
+ </div>
213
+ </td>
214
+ <td><code><%= similar_error.error_type %></code></td>
215
+ <td>
216
+ <div class="text-truncate" style="max-width: 300px;"
217
+ data-bs-toggle="tooltip"
218
+ title="<%= similar_error.message %>">
219
+ <%= similar_error.message %>
220
+ </div>
221
+ </td>
222
+ <td>
223
+ <% if similar_error.platform == 'iOS' %>
224
+ <span class="badge badge-ios">iOS</span>
225
+ <% elsif similar_error.platform == 'Android' %>
226
+ <span class="badge badge-android">Android</span>
227
+ <% else %>
228
+ <span class="badge badge-api"><%= similar_error.platform || 'API' %></span>
229
+ <% end %>
230
+ </td>
231
+ <td><span class="badge bg-primary"><%= similar_error.occurrence_count %>x</span></td>
232
+ <td>
233
+ <%= link_to "View", error_path(similar_error), class: "btn btn-sm btn-outline-primary" %>
234
+ </td>
235
+ </tr>
236
+ <% end %>
237
+ </tbody>
238
+ </table>
239
+ </div>
240
+ </div>
84
241
  </div>
85
- <div class="card-body p-0">
86
- <div class="table-responsive">
87
- <table class="table table-hover mb-0">
88
- <thead class="table-light">
89
- <tr>
90
- <th>Time</th>
91
- <th>Message</th>
92
- <th>Platform</th>
93
- <th>Status</th>
94
- <th></th>
95
- </tr>
96
- </thead>
97
- <tbody>
98
- <% @related_errors.each do |related| %>
242
+ <% end %>
243
+ <% end %>
244
+
245
+ <!-- Co-occurring Errors -->
246
+ <% if RailsErrorDashboard.configuration.enable_co_occurring_errors && @error.respond_to?(:co_occurring_errors) %>
247
+ <% co_occurring = @error.co_occurring_errors(window_minutes: 5, min_frequency: 2, limit: 5) %>
248
+ <% if co_occurring.any? %>
249
+ <div class="card mb-4">
250
+ <div class="card-header bg-white">
251
+ <h5 class="mb-0">
252
+ <i class="bi bi-clock-history"></i> Co-occurring Errors
253
+ <span class="badge bg-info text-dark">Time Patterns</span>
254
+ </h5>
255
+ <small class="text-muted">Errors that occur within 5 minutes of this error</small>
256
+ </div>
257
+ <div class="card-body p-0">
258
+ <div class="table-responsive">
259
+ <table class="table table-hover mb-0">
260
+ <thead class="table-light">
99
261
  <tr>
100
- <td><small><%= related.occurred_at.strftime("%m/%d %I:%M%p") %></small></td>
101
- <td>
102
- <div class="text-truncate" style="max-width: 400px;">
103
- <%= related.message %>
104
- </div>
105
- </td>
106
- <td>
107
- <% if related.platform == 'iOS' %>
108
- <span class="badge badge-ios">iOS</span>
109
- <% elsif related.platform == 'Android' %>
110
- <span class="badge badge-android">Android</span>
111
- <% else %>
112
- <span class="badge badge-api"><%= related.platform || 'API' %></span>
113
- <% end %>
114
- </td>
115
- <td>
116
- <% if related.resolved? %>
117
- <i class="bi bi-check-circle-fill text-success"></i>
118
- <% else %>
119
- <i class="bi bi-exclamation-circle-fill text-danger"></i>
120
- <% end %>
121
- </td>
122
- <td>
123
- <%= link_to "View", error_path(related), class: "btn btn-sm btn-outline-primary" %>
124
- </td>
262
+ <th>Error Type</th>
263
+ <th>Message</th>
264
+ <th>Platform</th>
265
+ <th>Frequency</th>
266
+ <th>Avg Delay</th>
267
+ <th></th>
125
268
  </tr>
126
- <% end %>
127
- </tbody>
128
- </table>
269
+ </thead>
270
+ <tbody>
271
+ <% co_occurring.each do |result| %>
272
+ <% error = result[:error] %>
273
+ <% frequency = result[:frequency] %>
274
+ <% avg_delay = result[:avg_delay_seconds] %>
275
+ <tr>
276
+ <td>
277
+ <span class="badge bg-danger"><%= error.error_type %></span>
278
+ </td>
279
+ <td>
280
+ <span data-bs-toggle="tooltip" title="<%= error.message %>">
281
+ <%= truncate(error.message, length: 60) %>
282
+ </span>
283
+ </td>
284
+ <td>
285
+ <% if error.platform.present? %>
286
+ <span class="badge bg-secondary"><%= error.platform %></span>
287
+ <% end %>
288
+ </td>
289
+ <td>
290
+ <span class="badge bg-info text-dark">
291
+ <%= frequency %> time<%= frequency > 1 ? 's' : '' %>
292
+ </span>
293
+ </td>
294
+ <td>
295
+ <% if avg_delay < 0 %>
296
+ <span class="text-warning"><%= number_to_human(avg_delay.abs, units: {unit: "s", thousand: "s"}, precision: 1) %> before</span>
297
+ <% elsif avg_delay > 0 %>
298
+ <span class="text-success"><%= number_to_human(avg_delay, units: {unit: "s", thousand: "s"}, precision: 1) %> after</span>
299
+ <% else %>
300
+ <span class="text-muted">simultaneous</span>
301
+ <% end %>
302
+ </td>
303
+ <td class="text-end">
304
+ <%= link_to "View", error_path(error), class: "btn btn-sm btn-outline-primary" %>
305
+ </td>
306
+ </tr>
307
+ <% end %>
308
+ </tbody>
309
+ </table>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ <% end %>
314
+ <% end %>
315
+
316
+ <!-- Timeline: Related Errors -->
317
+ <%= render "timeline" %>
318
+
319
+ <!-- Phase 3: Comment Threads -->
320
+ <% if @error.respond_to?(:comments) %>
321
+ <div class="card mb-4">
322
+ <div class="card-header bg-white">
323
+ <h5 class="mb-0">
324
+ <i class="bi bi-chat-dots"></i> Discussion
325
+ <span class="badge bg-secondary ms-2"><%= @error.comments.count %></span>
326
+ </h5>
327
+ </div>
328
+ <div class="card-body">
329
+ <!-- Existing Comments -->
330
+ <% if @error.comments.recent_first.any? %>
331
+ <div class="mb-4">
332
+ <% @error.comments.recent_first.each do |comment| %>
333
+ <div class="border-bottom pb-3 mb-3">
334
+ <div class="d-flex justify-content-between align-items-start mb-2">
335
+ <div>
336
+ <strong class="text-primary">
337
+ <i class="bi bi-person-circle"></i> <%= comment.author_name %>
338
+ </strong>
339
+ <% if comment.recent? %>
340
+ <span class="badge bg-success ms-2">New</span>
341
+ <% end %>
342
+ </div>
343
+ <small class="text-muted">
344
+ <%= comment.formatted_time %>
345
+ <span class="ms-1 text-muted">(<%= time_ago_in_words(comment.created_at) %> ago)</span>
346
+ </small>
347
+ </div>
348
+ <div class="text-break">
349
+ <%= simple_format(comment.body, class: "mb-0") %>
350
+ </div>
351
+ </div>
352
+ <% end %>
353
+ </div>
354
+ <% else %>
355
+ <p class="text-muted mb-4">
356
+ <i class="bi bi-info-circle"></i> No comments yet. Start the discussion below.
357
+ </p>
358
+ <% end %>
359
+
360
+ <!-- Add Comment Form -->
361
+ <div class="border-top pt-3">
362
+ <h6 class="mb-3">
363
+ <i class="bi bi-plus-circle"></i> Add Comment
364
+ </h6>
365
+ <%= form_with url: add_comment_error_path(@error), method: :post do |f| %>
366
+ <div class="mb-3">
367
+ <label for="author_name" class="form-label">Your Name <span class="text-danger">*</span></label>
368
+ <%= text_field_tag :author_name, @error.assigned_to, class: "form-control", placeholder: "e.g., John Doe", required: true %>
369
+ </div>
370
+ <div class="mb-3">
371
+ <label for="body" class="form-label">Comment <span class="text-danger">*</span></label>
372
+ <%= text_area_tag :body, nil, class: "form-control", rows: 4, placeholder: "Share your thoughts, findings, or updates...", required: true %>
373
+ </div>
374
+ <%= submit_tag "Post Comment", class: "btn btn-primary" %>
375
+ <% end %>
376
+ </div>
377
+ </div>
378
+ </div>
379
+ <% end %>
380
+
381
+ <!-- Error Cascades -->
382
+ <% if RailsErrorDashboard.configuration.enable_error_cascades %>
383
+ <% cascades = @error.error_cascades %>
384
+ <% if cascades[:parents].any? || cascades[:children].any? %>
385
+ <div class="card">
386
+ <div class="card-header bg-white">
387
+ <h5 class="mb-0">
388
+ <i class="bi bi-diagram-3"></i> Error Cascades
389
+ <small class="text-muted ms-2">Causal relationships between errors</small>
390
+ </h5>
391
+ </div>
392
+ <div class="card-body">
393
+ <!-- Parent Errors (What causes this error) -->
394
+ <% if cascades[:parents].any? %>
395
+ <div class="mb-4">
396
+ <h6 class="text-danger">
397
+ <i class="bi bi-arrow-down-circle"></i> Triggered By
398
+ <small class="text-muted">(errors that cause this one)</small>
399
+ </h6>
400
+ <div class="table-responsive">
401
+ <table class="table table-sm table-hover mb-0">
402
+ <thead class="table-light">
403
+ <tr>
404
+ <th>Error Type</th>
405
+ <th>Message</th>
406
+ <th>Probability</th>
407
+ <th>Frequency</th>
408
+ <th>Avg Delay</th>
409
+ <th></th>
410
+ </tr>
411
+ </thead>
412
+ <tbody>
413
+ <% cascades[:parents].each do |cascade| %>
414
+ <% parent = cascade[:error] %>
415
+ <tr>
416
+ <td>
417
+ <span class="badge bg-<%= severity_color(parent.severity) %>">
418
+ <%= parent.error_type %>
419
+ </span>
420
+ </td>
421
+ <td>
422
+ <div class="text-truncate" style="max-width: 250px;">
423
+ <%= parent.message %>
424
+ </div>
425
+ </td>
426
+ <td>
427
+ <% probability_pct = (cascade[:probability] * 100).round(1) %>
428
+ <span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
429
+ <%= probability_pct %>%
430
+ </span>
431
+ </td>
432
+ <td>
433
+ <span class="badge bg-secondary"><%= cascade[:frequency] %>x</span>
434
+ </td>
435
+ <td>
436
+ <small class="text-muted">
437
+ <%= cascade[:avg_delay_seconds].round(1) %>s
438
+ </small>
439
+ </td>
440
+ <td>
441
+ <%= link_to "View", error_path(parent), class: "btn btn-sm btn-outline-primary" %>
442
+ </td>
443
+ </tr>
444
+ <% end %>
445
+ </tbody>
446
+ </table>
447
+ </div>
448
+ </div>
449
+ <% end %>
450
+
451
+ <!-- Child Errors (What this error causes) -->
452
+ <% if cascades[:children].any? %>
453
+ <div>
454
+ <h6 class="text-warning">
455
+ <i class="bi bi-arrow-up-circle"></i> Triggers
456
+ <small class="text-muted">(errors caused by this one)</small>
457
+ </h6>
458
+ <div class="table-responsive">
459
+ <table class="table table-sm table-hover mb-0">
460
+ <thead class="table-light">
461
+ <tr>
462
+ <th>Error Type</th>
463
+ <th>Message</th>
464
+ <th>Probability</th>
465
+ <th>Frequency</th>
466
+ <th>Avg Delay</th>
467
+ <th></th>
468
+ </tr>
469
+ </thead>
470
+ <tbody>
471
+ <% cascades[:children].each do |cascade| %>
472
+ <% child = cascade[:error] %>
473
+ <tr>
474
+ <td>
475
+ <span class="badge bg-<%= severity_color(child.severity) %>">
476
+ <%= child.error_type %>
477
+ </span>
478
+ </td>
479
+ <td>
480
+ <div class="text-truncate" style="max-width: 250px;">
481
+ <%= child.message %>
482
+ </div>
483
+ </td>
484
+ <td>
485
+ <% probability_pct = (cascade[:probability] * 100).round(1) %>
486
+ <span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
487
+ <%= probability_pct %>%
488
+ </span>
489
+ </td>
490
+ <td>
491
+ <span class="badge bg-secondary"><%= cascade[:frequency] %>x</span>
492
+ </td>
493
+ <td>
494
+ <small class="text-muted">
495
+ +<%= cascade[:avg_delay_seconds].round(1) %>s
496
+ </small>
497
+ </td>
498
+ <td>
499
+ <%= link_to "View", error_path(child), class: "btn btn-sm btn-outline-primary" %>
500
+ </td>
501
+ </tr>
502
+ <% end %>
503
+ </tbody>
504
+ </table>
505
+ </div>
506
+ </div>
507
+ <% end %>
508
+
509
+ <div class="mt-3">
510
+ <small class="text-muted">
511
+ <i class="bi bi-info-circle"></i>
512
+ Cascade patterns show causal relationships. High probability (≥70%) and frequency (≥3) indicate strong cascades.
513
+ </small>
129
514
  </div>
130
515
  </div>
131
516
  </div>
517
+ <% end %>
132
518
  <% end %>
519
+
520
+ <!-- Occurrence Patterns and Bursts -->
521
+ <%= render "pattern_insights" %>
133
522
  </div>
134
523
 
135
524
  <!-- Sidebar -->
@@ -141,16 +530,37 @@
141
530
  </div>
142
531
  <div class="card-body">
143
532
  <div class="mb-3">
144
- <small class="text-muted d-block mb-1">Occurred At</small>
145
- <strong><%= @error.occurred_at.strftime("%B %d, %Y") %></strong><br>
146
- <small><%= @error.occurred_at.strftime("%I:%M:%S %p %Z") %></small>
533
+ <small class="text-muted d-block mb-1">Occurrence Count</small>
534
+ <span class="badge bg-primary fs-5"><%= @error.occurrence_count %>x</span>
535
+ <% if @error.occurrence_count > 1 %>
536
+ <br><small class="text-muted">This error has occurred <%= @error.occurrence_count %> times</small>
537
+ <% end %>
538
+ </div>
539
+
540
+ <div class="mb-3">
541
+ <small class="text-muted d-block mb-1">First Seen</small>
542
+ <strong><%= @error.first_seen_at&.strftime("%B %d, %Y") || 'N/A' %></strong><br>
543
+ <small><%= @error.first_seen_at&.strftime("%I:%M:%S %p %Z") || 'N/A' %></small>
147
544
  </div>
148
545
 
149
546
  <div class="mb-3">
150
- <small class="text-muted d-block mb-1">Environment</small>
151
- <span class="badge <%= @error.environment == 'production' ? 'bg-danger' : 'bg-info' %>">
152
- <%= @error.environment.titleize %>
153
- </span>
547
+ <small class="text-muted d-block mb-1">Last Seen</small>
548
+ <strong><%= @error.last_seen_at&.strftime("%B %d, %Y") || 'N/A' %></strong><br>
549
+ <small><%= @error.last_seen_at&.strftime("%I:%M:%S %p %Z") || 'N/A' %></small>
550
+ </div>
551
+
552
+ <div class="mb-3">
553
+ <small class="text-muted d-block mb-1">Severity Level</small>
554
+ <% severity = @error.severity %>
555
+ <% if severity == :critical %>
556
+ <span class="badge bg-danger fs-6">CRITICAL</span>
557
+ <% elsif severity == :high %>
558
+ <span class="badge bg-warning text-dark fs-6">HIGH</span>
559
+ <% elsif severity == :medium %>
560
+ <span class="badge bg-info text-dark fs-6">MEDIUM</span>
561
+ <% else %>
562
+ <span class="badge bg-secondary fs-6">LOW</span>
563
+ <% end %>
154
564
  </div>
155
565
 
156
566
  <div class="mb-3">
@@ -166,17 +576,24 @@
166
576
 
167
577
  <div class="mb-3">
168
578
  <small class="text-muted d-block mb-1">User</small>
169
- <% if @error.user %>
579
+ <% if @error.respond_to?(:user) && @error.user %>
170
580
  <strong><%= @error.user.email %></strong><br>
171
581
  <small class="text-muted">ID: <%= @error.user_id %></small>
582
+ <% elsif @error.user_id %>
583
+ <span class="text-muted">User ID: <%= @error.user_id %></span>
172
584
  <% else %>
173
585
  <span class="text-muted">Guest / Unauthenticated</span>
174
586
  <% end %>
175
587
  </div>
176
588
 
589
+ <!-- Phase 3: Workflow Status -->
177
590
  <div class="mb-3">
178
- <small class="text-muted d-block mb-1">Status</small>
179
- <% if @error.resolved? %>
591
+ <small class="text-muted d-block mb-1">Workflow Status</small>
592
+ <% if @error.respond_to?(:status) %>
593
+ <span class="badge bg-<%= @error.status_badge_color %> fs-6">
594
+ <%= @error.status.titleize %>
595
+ </span>
596
+ <% elsif @error.resolved? %>
180
597
  <span class="badge bg-success">
181
598
  <i class="bi bi-check-circle"></i> Resolved
182
599
  </span>
@@ -193,6 +610,67 @@
193
610
  <% end %>
194
611
  </div>
195
612
 
613
+ <!-- Phase 3: Assignment -->
614
+ <% if @error.respond_to?(:assigned_to) %>
615
+ <div class="mb-3">
616
+ <small class="text-muted d-block mb-1">Assigned To</small>
617
+ <% if @error.assigned? %>
618
+ <div class="d-flex align-items-center justify-content-between">
619
+ <div>
620
+ <i class="bi bi-person-fill text-primary"></i>
621
+ <strong><%= @error.assigned_to %></strong>
622
+ <% if @error.assigned_at.present? %>
623
+ <br>
624
+ <small class="text-muted">
625
+ <%= time_ago_in_words(@error.assigned_at) %> ago
626
+ </small>
627
+ <% end %>
628
+ </div>
629
+ <%= button_to unassign_error_path(@error), method: :patch, class: "btn btn-sm btn-outline-secondary",
630
+ data: { turbo_confirm: "Remove assignment?" } do %>
631
+ <i class="bi bi-x"></i>
632
+ <% end %>
633
+ </div>
634
+ <% else %>
635
+ <button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#assignModal">
636
+ <i class="bi bi-person-plus"></i> Assign
637
+ </button>
638
+ <% end %>
639
+ </div>
640
+
641
+ <!-- Phase 3: Priority -->
642
+ <div class="mb-3">
643
+ <small class="text-muted d-block mb-1">Priority</small>
644
+ <% if @error.respond_to?(:priority_level) %>
645
+ <span class="badge bg-<%= @error.priority_color %> fs-6">
646
+ <%= @error.priority_label %>
647
+ </span>
648
+ <button type="button" class="btn btn-sm btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#priorityModal">
649
+ <i class="bi bi-pencil"></i>
650
+ </button>
651
+ <% end %>
652
+ </div>
653
+
654
+ <!-- Phase 3: Snooze -->
655
+ <div class="mb-3">
656
+ <small class="text-muted d-block mb-1">Snooze</small>
657
+ <% if @error.respond_to?(:snoozed?) && @error.snoozed? %>
658
+ <div class="alert alert-warning py-2 mb-2">
659
+ <i class="bi bi-alarm"></i>
660
+ <strong>Snoozed</strong><br>
661
+ <small>Until <%= @error.snoozed_until&.strftime("%b %d at %I:%M %p") %></small>
662
+ </div>
663
+ <%= button_to unsnooze_error_path(@error), method: :patch, class: "btn btn-sm btn-outline-warning" do %>
664
+ <i class="bi bi-alarm-fill"></i> Unsnooze
665
+ <% end %>
666
+ <% else %>
667
+ <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#snoozeModal">
668
+ <i class="bi bi-alarm"></i> Snooze
669
+ </button>
670
+ <% end %>
671
+ </div>
672
+ <% end %>
673
+
196
674
  <% if @error.resolved? && @error.resolved_by_name.present? %>
197
675
  <div class="mb-3">
198
676
  <small class="text-muted d-block mb-1">Resolved By</small>
@@ -216,6 +694,44 @@
216
694
  </div>
217
695
  <% end %>
218
696
 
697
+ <!-- Environment Information -->
698
+ <div class="mb-3">
699
+ <small class="text-muted d-block mb-2">
700
+ <i class="bi bi-gear"></i> Environment
701
+ </small>
702
+
703
+ <% if @error.app_version.present? %>
704
+ <div class="mb-1">
705
+ <small class="text-muted">App Version:</small>
706
+ <code class="ms-1"><%= @error.app_version %></code>
707
+ </div>
708
+ <% end %>
709
+
710
+ <% if @error.git_sha.present? %>
711
+ <div class="mb-1">
712
+ <small class="text-muted">Git SHA:</small>
713
+ <code class="ms-1"><%= @error.git_sha[0..7] %></code>
714
+ </div>
715
+ <% end %>
716
+
717
+ <div class="mb-1">
718
+ <small class="text-muted">Rails:</small>
719
+ <code class="ms-1"><%= Rails.version %></code>
720
+ </div>
721
+
722
+ <div class="mb-1">
723
+ <small class="text-muted">Ruby:</small>
724
+ <code class="ms-1"><%= RUBY_VERSION %></code>
725
+ </div>
726
+
727
+ <div class="mb-1">
728
+ <small class="text-muted">Environment:</small>
729
+ <span class="badge bg-<%= Rails.env.production? ? 'danger' : Rails.env.development? ? 'success' : 'warning' %> ms-1">
730
+ <%= Rails.env.titleize %>
731
+ </span>
732
+ </div>
733
+ </div>
734
+
219
735
  <div>
220
736
  <small class="text-muted d-block mb-1">Error ID</small>
221
737
  <code><%= @error.id %></code>
@@ -223,6 +739,103 @@
223
739
  </div>
224
740
  </div>
225
741
 
742
+ <!-- Baseline Statistics -->
743
+ <% if RailsErrorDashboard.configuration.enable_baseline_alerts %>
744
+ <% baselines = @error.baselines %>
745
+ <% if baselines[:hourly].present? || baselines[:daily].present? || baselines[:weekly].present? %>
746
+ <div class="card mb-4">
747
+ <div class="card-header bg-white">
748
+ <h5 class="mb-0">
749
+ <i class="bi bi-graph-up"></i> Baseline Statistics
750
+ <small class="text-muted ms-2">Historical patterns</small>
751
+ </h5>
752
+ </div>
753
+ <div class="card-body">
754
+ <% anomaly = @error.baseline_anomaly %>
755
+ <% if anomaly[:anomaly] %>
756
+ <div class="alert alert-<%= anomaly[:level] == :critical ? 'danger' : anomaly[:level] == :high ? 'warning' : 'info' %> mb-3">
757
+ <i class="bi bi-exclamation-triangle-fill"></i>
758
+ <strong>Anomaly Detected!</strong><br>
759
+ <small>
760
+ This error is <%= anomaly[:std_devs_above]&.round(1) %>σ above baseline
761
+ (<%= anomaly[:level].to_s.upcase %>)
762
+ </small>
763
+ </div>
764
+ <% end %>
765
+
766
+ <% [:hourly, :daily, :weekly].each do |type| %>
767
+ <% baseline = baselines[type] %>
768
+ <% next unless baseline %>
769
+
770
+ <div class="mb-3">
771
+ <small class="text-muted d-block mb-1 text-capitalize">
772
+ <i class="bi bi-<%= type == :hourly ? 'clock' : type == :daily ? 'calendar-day' : 'calendar-week' %>"></i>
773
+ <%= type.to_s.capitalize %> Baseline
774
+ </small>
775
+
776
+ <div class="row g-2">
777
+ <div class="col-6">
778
+ <div class="text-center p-2 bg-light rounded">
779
+ <small class="text-muted d-block">Mean</small>
780
+ <strong><%= baseline.mean&.round(1) || 'N/A' %></strong>
781
+ </div>
782
+ </div>
783
+ <div class="col-6">
784
+ <div class="text-center p-2 bg-light rounded">
785
+ <small class="text-muted d-block">Std Dev</small>
786
+ <strong><%= baseline.std_dev&.round(1) || 'N/A' %></strong>
787
+ </div>
788
+ </div>
789
+ </div>
790
+
791
+ <div class="row g-2 mt-2">
792
+ <div class="col-6">
793
+ <div class="text-center p-2 bg-light rounded">
794
+ <small class="text-muted d-block">95th %</small>
795
+ <strong><%= baseline.percentile_95&.round(1) || 'N/A' %></strong>
796
+ </div>
797
+ </div>
798
+ <div class="col-6">
799
+ <div class="text-center p-2 bg-light rounded">
800
+ <small class="text-muted d-block">99th %</small>
801
+ <strong><%= baseline.percentile_99&.round(1) || 'N/A' %></strong>
802
+ </div>
803
+ </div>
804
+ </div>
805
+
806
+ <% if baseline.mean && baseline.std_dev %>
807
+ <div class="mt-2">
808
+ <small class="text-muted">
809
+ Threshold (2σ): <strong><%= baseline.threshold(sensitivity: 2)&.round(1) %></strong>
810
+ </small>
811
+ </div>
812
+ <% end %>
813
+
814
+ <div class="mt-2">
815
+ <small class="text-muted">
816
+ Sample: <%= baseline.sample_size %> periods
817
+ <span class="text-muted">|</span>
818
+ Updated: <%= baseline.updated_at&.strftime("%m/%d %I:%M%p") %>
819
+ </small>
820
+ </div>
821
+ </div>
822
+
823
+ <% unless baseline == baselines.values.compact.last %>
824
+ <hr>
825
+ <% end %>
826
+ <% end %>
827
+
828
+ <div class="mt-3">
829
+ <small class="text-muted">
830
+ <i class="bi bi-info-circle"></i>
831
+ Baselines establish "normal" error rates. Anomalies are detected when current rates exceed baseline + 2σ.
832
+ </small>
833
+ </div>
834
+ </div>
835
+ </div>
836
+ <% end %>
837
+ <% end %>
838
+
226
839
  <!-- Quick Actions -->
227
840
  <div class="card">
228
841
  <div class="card-header bg-white">
@@ -234,7 +847,7 @@
234
847
  <i class="bi bi-filter"></i> View Similar Errors
235
848
  <% end %>
236
849
 
237
- <% if @error.user %>
850
+ <% if @error.user_id %>
238
851
  <%= link_to errors_path(user_id: @error.user_id), class: "btn btn-outline-primary btn-sm" do %>
239
852
  <i class="bi bi-person"></i> View User's Errors
240
853
  <% end %>
@@ -258,7 +871,7 @@
258
871
  <div class="modal fade" id="resolveModal" tabindex="-1" aria-labelledby="resolveModalLabel" aria-hidden="true">
259
872
  <div class="modal-dialog">
260
873
  <div class="modal-content">
261
- <%= form_with url: resolve_error_path(@error), method: :patch, class: "modal-content" do |f| %>
874
+ <%= form_with url: resolve_error_path(@error), method: :post, class: "modal-content" do |f| %>
262
875
  <div class="modal-header">
263
876
  <h5 class="modal-title" id="resolveModalLabel">
264
877
  <i class="bi bi-check-circle"></i> Mark Error as Resolved
@@ -292,3 +905,106 @@
292
905
  </div>
293
906
  </div>
294
907
  </div>
908
+
909
+ <!-- Phase 3: Assignment Modal -->
910
+ <div class="modal fade" id="assignModal" tabindex="-1" aria-labelledby="assignModalLabel" aria-hidden="true">
911
+ <div class="modal-dialog">
912
+ <div class="modal-content">
913
+ <%= form_with url: assign_error_path(@error), method: :patch do |f| %>
914
+ <div class="modal-header">
915
+ <h5 class="modal-title" id="assignModalLabel">
916
+ <i class="bi bi-person-plus"></i> Assign Error
917
+ </h5>
918
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
919
+ </div>
920
+ <div class="modal-body">
921
+ <div class="mb-3">
922
+ <label for="assigned_to" class="form-label">Assign To <span class="text-danger">*</span></label>
923
+ <%= text_field_tag :assigned_to, nil, class: "form-control", placeholder: "e.g., John Doe", required: true, autofocus: true %>
924
+ <small class="text-muted">Who should investigate this error?</small>
925
+ </div>
926
+ </div>
927
+ <div class="modal-footer">
928
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
929
+ <%= submit_tag "Assign", class: "btn btn-primary" %>
930
+ </div>
931
+ <% end %>
932
+ </div>
933
+ </div>
934
+ </div>
935
+
936
+ <!-- Phase 3: Priority Modal -->
937
+ <div class="modal fade" id="priorityModal" tabindex="-1" aria-labelledby="priorityModalLabel" aria-hidden="true">
938
+ <div class="modal-dialog">
939
+ <div class="modal-content">
940
+ <%= form_with url: update_priority_error_path(@error), method: :patch do |f| %>
941
+ <div class="modal-header">
942
+ <h5 class="modal-title" id="priorityModalLabel">
943
+ <i class="bi bi-flag"></i> Update Priority
944
+ </h5>
945
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
946
+ </div>
947
+ <div class="modal-body">
948
+ <div class="mb-3">
949
+ <label for="priority_level" class="form-label">Priority Level <span class="text-danger">*</span></label>
950
+ <%= select_tag :priority_level,
951
+ options_for_select([
952
+ ['Critical (P3)', 3],
953
+ ['High (P2)', 2],
954
+ ['Medium (P1)', 1],
955
+ ['Low (P0)', 0]
956
+ ], @error.priority_level),
957
+ class: "form-select", required: true %>
958
+ <small class="text-muted">Current: <%= @error.priority_label %></small>
959
+ </div>
960
+ </div>
961
+ <div class="modal-footer">
962
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
963
+ <%= submit_tag "Update Priority", class: "btn btn-warning" %>
964
+ </div>
965
+ <% end %>
966
+ </div>
967
+ </div>
968
+ </div>
969
+
970
+ <!-- Phase 3: Snooze Modal -->
971
+ <div class="modal fade" id="snoozeModal" tabindex="-1" aria-labelledby="snoozeModalLabel" aria-hidden="true">
972
+ <div class="modal-dialog">
973
+ <div class="modal-content">
974
+ <%= form_with url: snooze_error_path(@error), method: :patch do |f| %>
975
+ <div class="modal-header">
976
+ <h5 class="modal-title" id="snoozeModalLabel">
977
+ <i class="bi bi-alarm"></i> Snooze Error
978
+ </h5>
979
+ <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
980
+ </div>
981
+ <div class="modal-body">
982
+ <div class="mb-3">
983
+ <label for="hours" class="form-label">Snooze Duration <span class="text-danger">*</span></label>
984
+ <%= select_tag :hours,
985
+ options_for_select([
986
+ ['1 hour', 1],
987
+ ['4 hours', 4],
988
+ ['8 hours', 8],
989
+ ['24 hours (1 day)', 24],
990
+ ['48 hours (2 days)', 48],
991
+ ['168 hours (1 week)', 168]
992
+ ], 24),
993
+ class: "form-select", required: true %>
994
+ <small class="text-muted">Hide this error from active views</small>
995
+ </div>
996
+
997
+ <div class="mb-3">
998
+ <label for="reason" class="form-label">Reason (Optional)</label>
999
+ <%= text_area_tag :reason, nil, class: "form-control", rows: 3, placeholder: "Why are you snoozing this error?" %>
1000
+ <small class="text-muted">Reason will be added as a comment</small>
1001
+ </div>
1002
+ </div>
1003
+ <div class="modal-footer">
1004
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
1005
+ <%= submit_tag "Snooze", class: "btn btn-warning" %>
1006
+ </div>
1007
+ <% end %>
1008
+ </div>
1009
+ </div>
1010
+ </div>