rails_error_dashboard 0.2.3 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +82 -14
- data/app/controllers/rails_error_dashboard/errors_controller.rb +88 -1
- data/app/helpers/rails_error_dashboard/application_helper.rb +25 -0
- data/app/jobs/rails_error_dashboard/retention_cleanup_job.rb +18 -6
- data/app/views/layouts/rails_error_dashboard.html.erb +145 -1
- data/app/views/rails_error_dashboard/errors/_breadcrumbs_group.html.erb +236 -0
- data/app/views/rails_error_dashboard/errors/_co_occurring_errors.html.erb +70 -0
- data/app/views/rails_error_dashboard/errors/_discussion.html.erb +107 -0
- data/app/views/rails_error_dashboard/errors/_error_cascades.html.erb +138 -0
- data/app/views/rails_error_dashboard/errors/_error_info.html.erb +190 -0
- data/app/views/rails_error_dashboard/errors/_modals.html.erb +139 -0
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/_request_context.html.erb +97 -0
- data/app/views/rails_error_dashboard/errors/_show_scripts.html.erb +156 -0
- data/app/views/rails_error_dashboard/errors/_sidebar_metadata.html.erb +352 -0
- data/app/views/rails_error_dashboard/errors/_similar_errors.html.erb +75 -0
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +1 -1
- data/app/views/rails_error_dashboard/errors/cache_health_summary.html.erb +143 -0
- data/app/views/rails_error_dashboard/errors/deprecations.html.erb +129 -0
- data/app/views/rails_error_dashboard/errors/n_plus_one_summary.html.erb +134 -0
- data/app/views/rails_error_dashboard/errors/settings.html.erb +17 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +20 -1132
- data/config/routes.rb +3 -0
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +11 -5
- data/db/migrate/20260303000001_add_breadcrumbs_to_error_logs.rb +9 -0
- data/db/migrate/20260304000001_add_system_health_to_error_logs.rb +12 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +82 -6
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +67 -5
- data/lib/rails_error_dashboard/commands/log_error.rb +33 -0
- data/lib/rails_error_dashboard/configuration.rb +45 -3
- data/lib/rails_error_dashboard/engine.rb +14 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +8 -0
- data/lib/rails_error_dashboard/queries/cache_health_summary.rb +72 -0
- data/lib/rails_error_dashboard/queries/deprecation_warnings.rb +80 -0
- data/lib/rails_error_dashboard/queries/n_plus_one_summary.rb +83 -0
- data/lib/rails_error_dashboard/services/breadcrumb_collector.rb +182 -0
- data/lib/rails_error_dashboard/services/cache_analyzer.rb +76 -0
- data/lib/rails_error_dashboard/services/curl_generator.rb +80 -0
- data/lib/rails_error_dashboard/services/n_plus_one_detector.rb +74 -0
- data/lib/rails_error_dashboard/services/system_health_snapshot.rb +145 -0
- data/lib/rails_error_dashboard/subscribers/breadcrumb_subscriber.rb +210 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +20 -0
- data/lib/tasks/error_dashboard.rake +68 -2
- metadata +37 -11
|
@@ -1,61 +1,6 @@
|
|
|
1
1
|
<% content_for :page_title, "Error ##{@error.id}" %>
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
function downloadErrorJSON(event) {
|
|
5
|
-
const errorData = {
|
|
6
|
-
id: <%= raw @error.id.to_json %>,
|
|
7
|
-
error_type: <%= raw @error.error_type.to_json %>,
|
|
8
|
-
message: <%= raw @error.message.to_json %>,
|
|
9
|
-
backtrace: <%= raw @error.backtrace.to_json %>,
|
|
10
|
-
occurred_at: <%= raw @error.occurred_at.to_json %>,
|
|
11
|
-
first_seen_at: <%= raw @error.first_seen_at.to_json %>,
|
|
12
|
-
last_seen_at: <%= raw @error.last_seen_at.to_json %>,
|
|
13
|
-
occurrence_count: <%= raw @error.occurrence_count.to_json %>,
|
|
14
|
-
resolved: <%= raw @error.resolved?.to_json %>,
|
|
15
|
-
resolved_at: <%= raw @error.resolved_at.to_json %>,
|
|
16
|
-
resolved_by_name: <%= raw @error.resolved_by_name.to_json %>,
|
|
17
|
-
platform: <%= raw @error.platform.to_json %>,
|
|
18
|
-
user_id: <%= raw @error.user_id.to_json %>,
|
|
19
|
-
severity: <%= raw @error.severity.to_json %>,
|
|
20
|
-
priority_level: <%= raw (@error.priority_level || 0).to_json %>,
|
|
21
|
-
<% if @error.respond_to?(:exception_cause) && @error.exception_cause.present? %>
|
|
22
|
-
exception_cause: <%= raw @error.exception_cause %>,
|
|
23
|
-
<% end %>
|
|
24
|
-
<% if @error.respond_to?(:app_version) %>
|
|
25
|
-
app_version: <%= raw @error.app_version.to_json %>,
|
|
26
|
-
<% end %>
|
|
27
|
-
<% if @error.respond_to?(:git_commit) %>
|
|
28
|
-
git_commit: <%= raw @error.git_commit.to_json %>,
|
|
29
|
-
<% end %>
|
|
30
|
-
created_at: <%= raw @error.created_at.to_json %>,
|
|
31
|
-
updated_at: <%= raw @error.updated_at.to_json %>
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const jsonString = JSON.stringify(errorData, null, 2);
|
|
35
|
-
const blob = new Blob([jsonString], { type: 'application/json' });
|
|
36
|
-
const url = URL.createObjectURL(blob);
|
|
37
|
-
const link = document.createElement('a');
|
|
38
|
-
link.href = url;
|
|
39
|
-
link.download = `error_${errorData.id}_${errorData.error_type.replace(/[^a-zA-Z0-9]/g, '_')}.json`;
|
|
40
|
-
document.body.appendChild(link);
|
|
41
|
-
link.click();
|
|
42
|
-
document.body.removeChild(link);
|
|
43
|
-
URL.revokeObjectURL(url);
|
|
44
|
-
|
|
45
|
-
// Visual feedback
|
|
46
|
-
const button = event.currentTarget;
|
|
47
|
-
const originalHTML = button.innerHTML;
|
|
48
|
-
button.innerHTML = '<i class="bi bi-check"></i> Downloaded!';
|
|
49
|
-
button.classList.remove('btn-outline-secondary');
|
|
50
|
-
button.classList.add('btn-success');
|
|
51
|
-
|
|
52
|
-
setTimeout(() => {
|
|
53
|
-
button.innerHTML = originalHTML;
|
|
54
|
-
button.classList.remove('btn-success');
|
|
55
|
-
button.classList.add('btn-outline-secondary');
|
|
56
|
-
}, 2000);
|
|
57
|
-
}
|
|
58
|
-
</script>
|
|
3
|
+
<%= render "show_scripts", error: @error %>
|
|
59
4
|
|
|
60
5
|
<div class="py-4" data-controller="loading">
|
|
61
6
|
<!-- Breadcrumbs -->
|
|
@@ -99,680 +44,35 @@
|
|
|
99
44
|
</div>
|
|
100
45
|
</div>
|
|
101
46
|
|
|
47
|
+
<!-- Section Navigation -->
|
|
48
|
+
<div id="section-nav-wrapper">
|
|
49
|
+
<div id="section-nav" class="d-flex align-items-center gap-2 py-2 mb-0">
|
|
50
|
+
<span class="section-nav-label text-muted me-1"><i class="bi bi-compass"></i></span>
|
|
51
|
+
<div class="section-nav-scroll d-flex gap-1 overflow-auto flex-nowrap">
|
|
52
|
+
<%# Pills are generated by JavaScript based on which sections exist in the DOM %>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
102
57
|
<div class="row g-4">
|
|
103
58
|
<!-- Error Information -->
|
|
104
59
|
<div class="col-md-8">
|
|
105
|
-
|
|
106
|
-
<div class="card mb-4">
|
|
107
|
-
<div class="card-header bg-danger text-white">
|
|
108
|
-
<div class="d-flex justify-content-between align-items-center">
|
|
109
|
-
<h5 class="mb-0">
|
|
110
|
-
<i class="bi bi-bug-fill"></i> <%= @error.error_type %>
|
|
111
|
-
<% if @error.recent? %>
|
|
112
|
-
<span class="badge bg-success ms-2" data-bs-toggle="tooltip" title="Error occurred within the last hour">NEW</span>
|
|
113
|
-
<% end %>
|
|
114
|
-
<% if @error.reopened? %>
|
|
115
|
-
<span class="badge bg-warning text-dark ms-2" data-bs-toggle="tooltip" title="Previously resolved, recurred <%= @error.reopened_at&.strftime('%b %d, %Y %H:%M') %>">
|
|
116
|
-
<i class="bi bi-arrow-counterclockwise"></i> Reopened
|
|
117
|
-
</span>
|
|
118
|
-
<% end %>
|
|
119
|
-
</h5>
|
|
120
|
-
<button class="btn btn-sm btn-outline-light" onclick="copyToClipboard('<%= j @error.error_type %>', this)" title="Copy error type">
|
|
121
|
-
<i class="bi bi-clipboard"></i>
|
|
122
|
-
</button>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
<div class="card-body">
|
|
126
|
-
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
127
|
-
<h6 class="text-muted mb-0">Error Message:</h6>
|
|
128
|
-
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<%= j @error.message %>', this)" title="Copy error message">
|
|
129
|
-
<i class="bi bi-clipboard"></i> Copy
|
|
130
|
-
</button>
|
|
131
|
-
</div>
|
|
132
|
-
<div class="alert alert-danger">
|
|
133
|
-
<%= @error.message %>
|
|
134
|
-
</div>
|
|
60
|
+
<%= render "error_info", error: @error %>
|
|
135
61
|
|
|
136
|
-
|
|
137
|
-
<% begin %>
|
|
138
|
-
<% cause_chain = JSON.parse(@error.exception_cause) %>
|
|
139
|
-
<% if cause_chain.is_a?(Array) && cause_chain.any? %>
|
|
140
|
-
<div class="mt-4 mb-3">
|
|
141
|
-
<h6 class="text-muted mb-2">
|
|
142
|
-
<i class="bi bi-link-45deg"></i> Exception Cause Chain
|
|
143
|
-
<span class="badge bg-secondary ms-1"><%= cause_chain.length %></span>
|
|
144
|
-
</h6>
|
|
145
|
-
<div class="cause-chain">
|
|
146
|
-
<% cause_chain.each_with_index do |cause, index| %>
|
|
147
|
-
<% cause_bt = cause["backtrace"] %>
|
|
148
|
-
<% cause_bt = cause_bt.is_a?(String) ? cause_bt.split("\n") : Array(cause_bt) %>
|
|
149
|
-
<div class="card mb-2 border-warning">
|
|
150
|
-
<div class="card-header bg-warning bg-opacity-10 py-2">
|
|
151
|
-
<div class="d-flex justify-content-between align-items-center">
|
|
152
|
-
<div>
|
|
153
|
-
<small class="text-muted">Caused by<%= " (##{index + 1})" if cause_chain.length > 1 %>:</small>
|
|
154
|
-
<strong class="ms-1"><code><%= cause["class_name"] %></code></strong>
|
|
155
|
-
</div>
|
|
156
|
-
<% if cause_bt.any? %>
|
|
157
|
-
<button class="btn btn-sm btn-outline-secondary py-0" type="button"
|
|
158
|
-
data-bs-toggle="collapse"
|
|
159
|
-
data-bs-target="#cause-backtrace-<%= index %>"
|
|
160
|
-
aria-expanded="false">
|
|
161
|
-
<small><i class="bi bi-code-slash"></i> Backtrace</small>
|
|
162
|
-
</button>
|
|
163
|
-
<% end %>
|
|
164
|
-
</div>
|
|
165
|
-
</div>
|
|
166
|
-
<div class="card-body py-2">
|
|
167
|
-
<small><%= cause["message"] %></small>
|
|
168
|
-
</div>
|
|
169
|
-
<% if cause_bt.any? %>
|
|
170
|
-
<div class="collapse" id="cause-backtrace-<%= index %>">
|
|
171
|
-
<div class="card-body p-0 border-top">
|
|
172
|
-
<div class="code-block p-2" style="max-height: 200px; overflow-y: auto; overflow-x: auto; font-size: 0.8rem;">
|
|
173
|
-
<pre class="mb-0"><code><% cause_bt.each do |line| %><%= line %>
|
|
174
|
-
<% end %></code></pre>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
</div>
|
|
178
|
-
<% end %>
|
|
179
|
-
</div>
|
|
180
|
-
<% end %>
|
|
181
|
-
</div>
|
|
182
|
-
</div>
|
|
183
|
-
<% end %>
|
|
184
|
-
<% rescue JSON::ParserError %>
|
|
185
|
-
<%# Silently skip invalid JSON — safety first %>
|
|
186
|
-
<% end %>
|
|
187
|
-
<% end %>
|
|
62
|
+
<%= render "request_context", error: @error %>
|
|
188
63
|
|
|
189
|
-
|
|
190
|
-
<h6 class="text-muted mb-0">
|
|
191
|
-
Backtrace:
|
|
192
|
-
<% if @error.backtrace.present? %>
|
|
193
|
-
<% frames = parse_backtrace(@error.backtrace) %>
|
|
194
|
-
<% app_frames = filter_app_code(frames) %>
|
|
195
|
-
<% framework_frames = filter_framework_code(frames) %>
|
|
196
|
-
<small class="text-muted">
|
|
197
|
-
(<%= app_frames.count %> your code, <%= framework_frames.count %> framework/gems)
|
|
198
|
-
</small>
|
|
199
|
-
<% end %>
|
|
200
|
-
</h6>
|
|
201
|
-
<% if @error.backtrace.present? %>
|
|
202
|
-
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard(`<%= j @error.backtrace %>`, this)" title="Copy full backtrace">
|
|
203
|
-
<i class="bi bi-clipboard"></i> Copy Full Backtrace
|
|
204
|
-
</button>
|
|
205
|
-
<% end %>
|
|
206
|
-
</div>
|
|
207
|
-
<% if @error.backtrace.present? %>
|
|
208
|
-
<% frames = parse_backtrace(@error.backtrace) %>
|
|
209
|
-
<% app_frames = filter_app_code(frames) %>
|
|
210
|
-
<% framework_frames = filter_framework_code(frames) %>
|
|
64
|
+
<%= render "breadcrumbs_group", error: @error %>
|
|
211
65
|
|
|
212
|
-
|
|
213
|
-
<!-- Your Code Section (Expanded by default) -->
|
|
214
|
-
<div class="card mb-3">
|
|
215
|
-
<div class="card-header bg-success bg-opacity-10 border-success">
|
|
216
|
-
<div class="d-flex justify-content-between align-items-center">
|
|
217
|
-
<strong class="text-success">
|
|
218
|
-
<i class="bi bi-code-square"></i> Your Code
|
|
219
|
-
</strong>
|
|
220
|
-
<span class="badge bg-success"><%= app_frames.count %> frames</span>
|
|
221
|
-
</div>
|
|
222
|
-
</div>
|
|
223
|
-
<div class="card-body p-0">
|
|
224
|
-
<div class="code-block p-3" style="max-height: none; overflow-y: visible; overflow-x: auto;">
|
|
225
|
-
<% app_frames.each_with_index do |frame, index| %>
|
|
226
|
-
<div class="backtrace-frame-wrapper mb-2">
|
|
227
|
-
<!-- Frame header (always visible) -->
|
|
228
|
-
<div class="<%= frame_bg_class(frame[:category]) %> d-flex justify-content-between align-items-center px-2 py-1">
|
|
229
|
-
<div>
|
|
230
|
-
<span class="backtrace-frame-number"><%= index + 1 %></span>
|
|
231
|
-
<span class="<%= frame_color_class(frame[:category]) %>">
|
|
232
|
-
<%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %>
|
|
233
|
-
</span> in <span class="backtrace-method-name">`<%= frame[:method_name] %>'</span>
|
|
234
|
-
</div>
|
|
235
|
-
<% if RailsErrorDashboard.configuration.enable_source_code_integration && frame[:category] == :app %>
|
|
236
|
-
<button class="btn btn-sm btn-outline-secondary" type="button"
|
|
237
|
-
data-bs-toggle="collapse"
|
|
238
|
-
data-bs-target="#source-code-<%= index %>"
|
|
239
|
-
aria-expanded="false"
|
|
240
|
-
aria-controls="source-code-<%= index %>">
|
|
241
|
-
<i class="bi bi-code-slash"></i> <span class="d-none d-md-inline">View Source</span>
|
|
242
|
-
</button>
|
|
243
|
-
<% end %>
|
|
244
|
-
</div>
|
|
66
|
+
<%= render "similar_errors", error: @error %>
|
|
245
67
|
|
|
246
|
-
|
|
247
|
-
<% if RailsErrorDashboard.configuration.enable_source_code_integration && frame[:category] == :app %>
|
|
248
|
-
<div class="collapse" id="source-code-<%= index %>">
|
|
249
|
-
<%= render "source_code", frame: frame, error: @error, index: index %>
|
|
250
|
-
</div>
|
|
251
|
-
<% end %>
|
|
252
|
-
</div>
|
|
253
|
-
<% end %>
|
|
254
|
-
</div>
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
257
|
-
<% end %>
|
|
258
|
-
|
|
259
|
-
<% if framework_frames.any? %>
|
|
260
|
-
<!-- Framework/Gem Code Section (Collapsed by default) -->
|
|
261
|
-
<div class="accordion" id="frameworkBacktraceAccordion">
|
|
262
|
-
<div class="accordion-item">
|
|
263
|
-
<h2 class="accordion-header">
|
|
264
|
-
<button class="accordion-button collapsed bg-light" type="button" data-bs-toggle="collapse" data-bs-target="#frameworkBacktrace" aria-expanded="false" aria-controls="frameworkBacktrace">
|
|
265
|
-
<i class="bi bi-gear me-2 text-warning"></i>
|
|
266
|
-
<strong>Framework & Gem Code</strong>
|
|
267
|
-
<span class="badge bg-secondary ms-2"><%= framework_frames.count %> frames</span>
|
|
268
|
-
<small class="text-muted ms-2">(click to expand)</small>
|
|
269
|
-
</button>
|
|
270
|
-
</h2>
|
|
271
|
-
<div id="frameworkBacktrace" class="accordion-collapse collapse" data-bs-parent="#frameworkBacktraceAccordion">
|
|
272
|
-
<div class="accordion-body p-0">
|
|
273
|
-
<div class="code-block p-3" style="max-height: 400px; overflow-y: auto; overflow-x: auto;">
|
|
274
|
-
<pre class="mb-0"><code><% framework_frames.each_with_index do |frame, index| %><span class="<%= frame_bg_class(frame[:category]) %> d-block px-2 py-1"><span class="backtrace-frame-number"><%= index + 1 %></span><span class="<%= frame_color_class(frame[:category]) %>"><%= frame_icon(frame[:category]) %> <%= frame[:short_path] %>:<%= frame[:line_number] %></span> in <span class="backtrace-method-name">`<%= frame[:method_name] %>'</span></span>
|
|
275
|
-
<% end %></code></pre>
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
280
|
-
</div>
|
|
281
|
-
<% end %>
|
|
282
|
-
|
|
283
|
-
<% if app_frames.empty? && framework_frames.empty? %>
|
|
284
|
-
<!-- Fallback: Show raw backtrace if parsing failed -->
|
|
285
|
-
<div class="code-block p-3 rounded" style="max-height: 400px; overflow-y: auto; overflow-x: auto;">
|
|
286
|
-
<pre class="mb-0"><code><%= @error.backtrace %></code></pre>
|
|
287
|
-
</div>
|
|
288
|
-
<% end %>
|
|
289
|
-
<% else %>
|
|
290
|
-
<p class="text-muted">No backtrace available</p>
|
|
291
|
-
<% end %>
|
|
292
|
-
</div>
|
|
293
|
-
</div>
|
|
294
|
-
<% end %>
|
|
295
|
-
|
|
296
|
-
<!-- Request Context -->
|
|
297
|
-
<% cache [@error, 'request_context_v2'] do %>
|
|
298
|
-
<div class="card mb-4">
|
|
299
|
-
<div class="card-header bg-white">
|
|
300
|
-
<h5 class="mb-0"><i class="bi bi-globe"></i> Request Context</h5>
|
|
301
|
-
</div>
|
|
302
|
-
<div class="card-body">
|
|
303
|
-
<table class="table table-sm">
|
|
304
|
-
<tr>
|
|
305
|
-
<th width="200">Request URL:</th>
|
|
306
|
-
<td>
|
|
307
|
-
<% if @error.respond_to?(:http_method) && @error.http_method.present? %>
|
|
308
|
-
<span class="badge bg-primary me-1"><%= @error.http_method %></span>
|
|
309
|
-
<% end %>
|
|
310
|
-
<code><%= @error.request_url || 'N/A' %></code>
|
|
311
|
-
</td>
|
|
312
|
-
</tr>
|
|
313
|
-
<% if @error.respond_to?(:hostname) && @error.hostname.present? %>
|
|
314
|
-
<tr>
|
|
315
|
-
<th>Hostname:</th>
|
|
316
|
-
<td><code><%= @error.hostname %></code></td>
|
|
317
|
-
</tr>
|
|
318
|
-
<% end %>
|
|
319
|
-
<% if @error.respond_to?(:content_type) && @error.content_type.present? %>
|
|
320
|
-
<tr>
|
|
321
|
-
<th>Content Type:</th>
|
|
322
|
-
<td><code><%= @error.content_type %></code></td>
|
|
323
|
-
</tr>
|
|
324
|
-
<% end %>
|
|
325
|
-
<% if @error.respond_to?(:request_duration_ms) && @error.request_duration_ms.present? %>
|
|
326
|
-
<tr>
|
|
327
|
-
<th>Request Duration:</th>
|
|
328
|
-
<td>
|
|
329
|
-
<% duration = @error.request_duration_ms %>
|
|
330
|
-
<span class="badge bg-<%= duration > 5000 ? 'danger' : duration > 1000 ? 'warning' : 'success' %>">
|
|
331
|
-
<%= duration > 1000 ? "#{(duration / 1000.0).round(1)}s" : "#{duration}ms" %>
|
|
332
|
-
</span>
|
|
333
|
-
</td>
|
|
334
|
-
</tr>
|
|
335
|
-
<% end %>
|
|
336
|
-
<tr>
|
|
337
|
-
<th>Request Params:</th>
|
|
338
|
-
<td>
|
|
339
|
-
<% if @error.request_params.present? %>
|
|
340
|
-
<pre class="mb-0"><code><%= JSON.pretty_generate(JSON.parse(@error.request_params)) rescue @error.request_params %></code></pre>
|
|
341
|
-
<% else %>
|
|
342
|
-
<span class="text-muted">N/A</span>
|
|
343
|
-
<% end %>
|
|
344
|
-
</td>
|
|
345
|
-
</tr>
|
|
346
|
-
<tr>
|
|
347
|
-
<th>User Agent:</th>
|
|
348
|
-
<td>
|
|
349
|
-
<% if @error.user_agent.present? %>
|
|
350
|
-
<% ua_info = parse_user_agent(@error.user_agent) %>
|
|
351
|
-
<div class="mb-2">
|
|
352
|
-
<%= browser_icon(ua_info) %>
|
|
353
|
-
<strong><%= ua_info[:browser_name] %></strong>
|
|
354
|
-
<% if ua_info[:browser_version].present? %>
|
|
355
|
-
<span class="text-muted"><%= ua_info[:browser_version] %></span>
|
|
356
|
-
<% end %>
|
|
357
|
-
<span class="ms-2"><%= os_icon(ua_info) %> <%= ua_info[:os_name] %></span>
|
|
358
|
-
<span class="ms-2"><%= device_icon(ua_info) %> <%= ua_info[:device_type] %></span>
|
|
359
|
-
</div>
|
|
360
|
-
<details>
|
|
361
|
-
<summary class="text-muted" style="cursor: pointer;">
|
|
362
|
-
<small>Raw User Agent</small>
|
|
363
|
-
</summary>
|
|
364
|
-
<small class="text-muted"><%= @error.user_agent %></small>
|
|
365
|
-
</details>
|
|
366
|
-
<% else %>
|
|
367
|
-
<span class="text-muted">N/A</span>
|
|
368
|
-
<% end %>
|
|
369
|
-
</td>
|
|
370
|
-
</tr>
|
|
371
|
-
<tr>
|
|
372
|
-
<th>IP Address:</th>
|
|
373
|
-
<td><code><%= @error.ip_address || 'N/A' %></code></td>
|
|
374
|
-
</tr>
|
|
375
|
-
</table>
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
<% end %>
|
|
379
|
-
|
|
380
|
-
<!-- Similar Errors (Fuzzy Matching) -->
|
|
381
|
-
<% if RailsErrorDashboard.configuration.enable_similar_errors && @error.respond_to?(:similar_errors) %>
|
|
382
|
-
<% similar = @error.similar_errors(threshold: 0.6, limit: 5) %>
|
|
383
|
-
<% if similar.any? %>
|
|
384
|
-
<% cache [@error, 'similar_errors_v1', similar.map { |s| s[:error]&.updated_at }.compact.max] do %>
|
|
385
|
-
<div class="card mb-4">
|
|
386
|
-
<div class="card-header bg-white">
|
|
387
|
-
<h5 class="mb-0">
|
|
388
|
-
<i class="bi bi-diagram-3"></i> Similar Errors
|
|
389
|
-
<span class="badge bg-info text-dark">Fuzzy Matching</span>
|
|
390
|
-
</h5>
|
|
391
|
-
<small class="text-muted">Errors with similar backtraces and messages (60%+ similarity)</small>
|
|
392
|
-
</div>
|
|
393
|
-
<div class="card-body p-0">
|
|
394
|
-
<div class="table-responsive">
|
|
395
|
-
<table class="table table-hover mb-0">
|
|
396
|
-
<thead class="table-light">
|
|
397
|
-
<tr>
|
|
398
|
-
<th>Similarity</th>
|
|
399
|
-
<th>Error Type</th>
|
|
400
|
-
<th>Message</th>
|
|
401
|
-
<th>Platform</th>
|
|
402
|
-
<th>Occurrences</th>
|
|
403
|
-
<th></th>
|
|
404
|
-
</tr>
|
|
405
|
-
</thead>
|
|
406
|
-
<tbody>
|
|
407
|
-
<% similar.each do |item| %>
|
|
408
|
-
<% similar_error = item[:error] %>
|
|
409
|
-
<% similarity_pct = (item[:similarity] * 100).round %>
|
|
410
|
-
<tr>
|
|
411
|
-
<td>
|
|
412
|
-
<div class="progress" style="width: 60px; height: 20px;">
|
|
413
|
-
<div class="progress-bar bg-success" role="progressbar"
|
|
414
|
-
style="width: <%= similarity_pct %>%"
|
|
415
|
-
aria-valuenow="<%= similarity_pct %>"
|
|
416
|
-
aria-valuemin="0"
|
|
417
|
-
aria-valuemax="100">
|
|
418
|
-
<%= similarity_pct %>%
|
|
419
|
-
</div>
|
|
420
|
-
</div>
|
|
421
|
-
</td>
|
|
422
|
-
<td><code><%= similar_error.error_type %></code></td>
|
|
423
|
-
<td>
|
|
424
|
-
<div class="text-truncate" style="max-width: 300px;"
|
|
425
|
-
data-bs-toggle="tooltip"
|
|
426
|
-
title="<%= similar_error.message %>">
|
|
427
|
-
<%= similar_error.message %>
|
|
428
|
-
</div>
|
|
429
|
-
</td>
|
|
430
|
-
<td>
|
|
431
|
-
<% if similar_error.platform == 'iOS' %>
|
|
432
|
-
<span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
|
|
433
|
-
<% elsif similar_error.platform == 'Android' %>
|
|
434
|
-
<span class="badge badge-android"><i class="bi bi-android2"></i> Android</span>
|
|
435
|
-
<% elsif similar_error.platform == 'Web' %>
|
|
436
|
-
<span class="badge badge-web"><i class="bi bi-globe"></i> Web</span>
|
|
437
|
-
<% else %>
|
|
438
|
-
<span class="badge badge-api"><i class="bi bi-server"></i> <%= similar_error.platform || 'API' %></span>
|
|
439
|
-
<% end %>
|
|
440
|
-
</td>
|
|
441
|
-
<td><span class="badge bg-primary"><%= similar_error.occurrence_count %>x</span></td>
|
|
442
|
-
<td>
|
|
443
|
-
<%= link_to "View", error_path(similar_error), class: "btn btn-sm btn-outline-primary" %>
|
|
444
|
-
</td>
|
|
445
|
-
</tr>
|
|
446
|
-
<% end %>
|
|
447
|
-
</tbody>
|
|
448
|
-
</table>
|
|
449
|
-
</div>
|
|
450
|
-
</div>
|
|
451
|
-
</div>
|
|
452
|
-
<% end %>
|
|
453
|
-
<% end %>
|
|
454
|
-
<% end %>
|
|
455
|
-
|
|
456
|
-
<!-- Co-occurring Errors -->
|
|
457
|
-
<% if RailsErrorDashboard.configuration.enable_co_occurring_errors && @error.respond_to?(:co_occurring_errors) %>
|
|
458
|
-
<% co_occurring = @error.co_occurring_errors(window_minutes: 5, min_frequency: 2, limit: 5) %>
|
|
459
|
-
<% if co_occurring.any? %>
|
|
460
|
-
<div class="card mb-4">
|
|
461
|
-
<div class="card-header bg-white">
|
|
462
|
-
<h5 class="mb-0">
|
|
463
|
-
<i class="bi bi-clock-history"></i> Co-occurring Errors
|
|
464
|
-
<span class="badge bg-info text-dark">Time Patterns</span>
|
|
465
|
-
</h5>
|
|
466
|
-
<small class="text-muted">Errors that occur within 5 minutes of this error</small>
|
|
467
|
-
</div>
|
|
468
|
-
<div class="card-body p-0">
|
|
469
|
-
<div class="table-responsive">
|
|
470
|
-
<table class="table table-hover mb-0">
|
|
471
|
-
<thead class="table-light">
|
|
472
|
-
<tr>
|
|
473
|
-
<th>Error Type</th>
|
|
474
|
-
<th>Message</th>
|
|
475
|
-
<th>Platform</th>
|
|
476
|
-
<th>Frequency</th>
|
|
477
|
-
<th>Avg Delay</th>
|
|
478
|
-
<th></th>
|
|
479
|
-
</tr>
|
|
480
|
-
</thead>
|
|
481
|
-
<tbody>
|
|
482
|
-
<% co_occurring.each do |result| %>
|
|
483
|
-
<% error = result[:error] %>
|
|
484
|
-
<% frequency = result[:frequency] %>
|
|
485
|
-
<% avg_delay = result[:avg_delay_seconds] %>
|
|
486
|
-
<tr>
|
|
487
|
-
<td>
|
|
488
|
-
<span class="badge bg-danger"><%= error.error_type %></span>
|
|
489
|
-
</td>
|
|
490
|
-
<td>
|
|
491
|
-
<span data-bs-toggle="tooltip" title="<%= error.message %>">
|
|
492
|
-
<%= truncate(error.message, length: 60) %>
|
|
493
|
-
</span>
|
|
494
|
-
</td>
|
|
495
|
-
<td>
|
|
496
|
-
<% if error.platform.present? %>
|
|
497
|
-
<span class="badge bg-secondary"><%= error.platform %></span>
|
|
498
|
-
<% end %>
|
|
499
|
-
</td>
|
|
500
|
-
<td>
|
|
501
|
-
<span class="badge bg-info text-dark">
|
|
502
|
-
<%= frequency %> time<%= frequency > 1 ? 's' : '' %>
|
|
503
|
-
</span>
|
|
504
|
-
</td>
|
|
505
|
-
<td>
|
|
506
|
-
<% if avg_delay < 0 %>
|
|
507
|
-
<span class="text-warning"><%= number_to_human(avg_delay.abs, units: {unit: "s", thousand: "s"}, precision: 1) %> before</span>
|
|
508
|
-
<% elsif avg_delay > 0 %>
|
|
509
|
-
<span class="text-success"><%= number_to_human(avg_delay, units: {unit: "s", thousand: "s"}, precision: 1) %> after</span>
|
|
510
|
-
<% else %>
|
|
511
|
-
<span class="text-muted">simultaneous</span>
|
|
512
|
-
<% end %>
|
|
513
|
-
</td>
|
|
514
|
-
<td class="text-end">
|
|
515
|
-
<%= link_to "View", error_path(error), class: "btn btn-sm btn-outline-primary" %>
|
|
516
|
-
</td>
|
|
517
|
-
</tr>
|
|
518
|
-
<% end %>
|
|
519
|
-
</tbody>
|
|
520
|
-
</table>
|
|
521
|
-
</div>
|
|
522
|
-
</div>
|
|
523
|
-
</div>
|
|
524
|
-
<% end %>
|
|
525
|
-
<% end %>
|
|
68
|
+
<%= render "co_occurring_errors", error: @error %>
|
|
526
69
|
|
|
527
70
|
<!-- Timeline: Related Errors -->
|
|
528
71
|
<%= render "timeline" %>
|
|
529
72
|
|
|
530
|
-
|
|
531
|
-
<% if @error.respond_to?(:comments) %>
|
|
532
|
-
<div class="card mb-4">
|
|
533
|
-
<div class="card-header bg-white">
|
|
534
|
-
<h5 class="mb-0">
|
|
535
|
-
<i class="bi bi-chat-dots"></i> Discussion
|
|
536
|
-
<span class="badge bg-secondary ms-2"><%= @error.comments.count %></span>
|
|
537
|
-
</h5>
|
|
538
|
-
</div>
|
|
539
|
-
<div class="card-body">
|
|
540
|
-
<!-- Existing Comments -->
|
|
541
|
-
<% if @error.comments.recent_first.any? %>
|
|
542
|
-
<div class="mb-4">
|
|
543
|
-
<% @error.comments.recent_first.each_with_index do |comment, index| %>
|
|
544
|
-
<div class="<%= index < @error.comments.count - 1 ? 'border-bottom' : '' %> pb-3 mb-3">
|
|
545
|
-
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
546
|
-
<div>
|
|
547
|
-
<strong class="text-primary">
|
|
548
|
-
<i class="bi bi-person-circle"></i> <%= comment.author_name %>
|
|
549
|
-
</strong>
|
|
550
|
-
<% if comment.recent? %>
|
|
551
|
-
<span class="badge bg-success ms-2">New</span>
|
|
552
|
-
<% end %>
|
|
553
|
-
</div>
|
|
554
|
-
<small class="text-muted">
|
|
555
|
-
<%= local_time(comment.created_at, format: :datetime) %>
|
|
556
|
-
<span class="ms-1 text-muted">(<%= local_time_ago(comment.created_at) %>)</span>
|
|
557
|
-
</small>
|
|
558
|
-
</div>
|
|
559
|
-
<div class="text-break">
|
|
560
|
-
<%= auto_link_urls(comment.body, error: @error).html_safe %>
|
|
561
|
-
</div>
|
|
562
|
-
</div>
|
|
563
|
-
<% end %>
|
|
564
|
-
</div>
|
|
565
|
-
<% else %>
|
|
566
|
-
<p class="text-muted mb-4">
|
|
567
|
-
<i class="bi bi-info-circle"></i> No comments yet. Start the discussion below.
|
|
568
|
-
</p>
|
|
569
|
-
<% end %>
|
|
570
|
-
|
|
571
|
-
<!-- Add Comment Form -->
|
|
572
|
-
<div class="<%= @error.comments.any? ? '' : 'border-top' %> pt-3">
|
|
573
|
-
<h6 class="mb-3">
|
|
574
|
-
<i class="bi bi-plus-circle"></i> Add Comment
|
|
575
|
-
</h6>
|
|
576
|
-
<%= form_with url: add_comment_error_path(@error), method: :post do |f| %>
|
|
577
|
-
<div class="mb-3">
|
|
578
|
-
<label for="author_name" class="form-label">Your Name <span class="text-danger">*</span></label>
|
|
579
|
-
<%= text_field_tag :author_name, @error.assigned_to, class: "form-control", placeholder: "e.g., John Doe", required: true %>
|
|
580
|
-
</div>
|
|
581
|
-
<div class="mb-3">
|
|
582
|
-
<label for="body" class="form-label">Comment <span class="text-danger">*</span></label>
|
|
583
|
-
|
|
584
|
-
<!-- Quick Templates -->
|
|
585
|
-
<div class="mb-2">
|
|
586
|
-
<small class="text-muted d-block mb-1">
|
|
587
|
-
<i class="bi bi-lightning-fill"></i> Quick templates:
|
|
588
|
-
</small>
|
|
589
|
-
<div class="d-flex flex-wrap gap-1">
|
|
590
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('investigating')">
|
|
591
|
-
<i class="bi bi-search"></i> Investigating
|
|
592
|
-
</button>
|
|
593
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('found_fix')">
|
|
594
|
-
<i class="bi bi-wrench"></i> Found Fix
|
|
595
|
-
</button>
|
|
596
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('need_info')">
|
|
597
|
-
<i class="bi bi-question-circle"></i> Need Info
|
|
598
|
-
</button>
|
|
599
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('duplicate')">
|
|
600
|
-
<i class="bi bi-files"></i> Duplicate
|
|
601
|
-
</button>
|
|
602
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="insertTemplate('cannot_reproduce')">
|
|
603
|
-
<i class="bi bi-x-circle"></i> Cannot Reproduce
|
|
604
|
-
</button>
|
|
605
|
-
</div>
|
|
606
|
-
</div>
|
|
607
|
-
|
|
608
|
-
<%= text_area_tag :body, nil, class: "form-control", rows: 4, placeholder: "Share your thoughts, findings, or updates...", required: true, id: "comment_body" %>
|
|
609
|
-
</div>
|
|
610
|
-
<%= submit_tag "Post Comment", class: "btn btn-primary" %>
|
|
611
|
-
<% end %>
|
|
612
|
-
</div>
|
|
613
|
-
|
|
614
|
-
<script>
|
|
615
|
-
function insertTemplate(templateType) {
|
|
616
|
-
const textarea = document.getElementById('comment_body');
|
|
617
|
-
const templates = {
|
|
618
|
-
investigating: "🔍 Investigating this issue now. Will update with findings.",
|
|
619
|
-
found_fix: "✅ Found the fix!\n\nRoot cause: \nSolution: \nPR: ",
|
|
620
|
-
need_info: "ℹ️ Need more information:\n\n- \n- \n\nPlease provide details to help debug this issue.",
|
|
621
|
-
duplicate: "📋 This appears to be a duplicate of error #\n\nClosing as duplicate.",
|
|
622
|
-
cannot_reproduce: "❌ Cannot reproduce this issue.\n\nAttempted:\n- \n- \n\nNeed more details or steps to reproduce."
|
|
623
|
-
};
|
|
624
|
-
|
|
625
|
-
const template = templates[templateType];
|
|
626
|
-
if (template) {
|
|
627
|
-
textarea.value = template;
|
|
628
|
-
textarea.focus();
|
|
629
|
-
// Move cursor to end
|
|
630
|
-
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
</script>
|
|
634
|
-
</div>
|
|
635
|
-
</div>
|
|
636
|
-
<% end %>
|
|
637
|
-
|
|
638
|
-
<!-- Error Cascades -->
|
|
639
|
-
<% if RailsErrorDashboard.configuration.enable_error_cascades %>
|
|
640
|
-
<% cascades = @error.error_cascades %>
|
|
641
|
-
<% if cascades[:parents].any? || cascades[:children].any? %>
|
|
642
|
-
<div class="card">
|
|
643
|
-
<div class="card-header bg-white">
|
|
644
|
-
<h5 class="mb-0">
|
|
645
|
-
<i class="bi bi-diagram-3"></i> Error Cascades
|
|
646
|
-
<small class="text-muted ms-2">Causal relationships between errors</small>
|
|
647
|
-
</h5>
|
|
648
|
-
</div>
|
|
649
|
-
<div class="card-body">
|
|
650
|
-
<!-- Parent Errors (What causes this error) -->
|
|
651
|
-
<% if cascades[:parents].any? %>
|
|
652
|
-
<div class="mb-4">
|
|
653
|
-
<h6 class="text-danger">
|
|
654
|
-
<i class="bi bi-arrow-down-circle"></i> Triggered By
|
|
655
|
-
<small class="text-muted">(errors that cause this one)</small>
|
|
656
|
-
</h6>
|
|
657
|
-
<div class="table-responsive">
|
|
658
|
-
<table class="table table-sm table-hover mb-0">
|
|
659
|
-
<thead class="table-light">
|
|
660
|
-
<tr>
|
|
661
|
-
<th>Error Type</th>
|
|
662
|
-
<th>Message</th>
|
|
663
|
-
<th>Probability</th>
|
|
664
|
-
<th>Frequency</th>
|
|
665
|
-
<th>Avg Delay</th>
|
|
666
|
-
<th></th>
|
|
667
|
-
</tr>
|
|
668
|
-
</thead>
|
|
669
|
-
<tbody>
|
|
670
|
-
<% cascades[:parents].each do |cascade| %>
|
|
671
|
-
<% parent = cascade[:error] %>
|
|
672
|
-
<tr>
|
|
673
|
-
<td>
|
|
674
|
-
<span class="badge bg-<%= severity_color(parent.severity) %>">
|
|
675
|
-
<%= parent.error_type %>
|
|
676
|
-
</span>
|
|
677
|
-
</td>
|
|
678
|
-
<td>
|
|
679
|
-
<div class="text-truncate" style="max-width: 250px;">
|
|
680
|
-
<%= parent.message %>
|
|
681
|
-
</div>
|
|
682
|
-
</td>
|
|
683
|
-
<td>
|
|
684
|
-
<% probability_pct = ((cascade[:probability] || 0) * 100).round(1) %>
|
|
685
|
-
<span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
|
|
686
|
-
<%= probability_pct %>%
|
|
687
|
-
</span>
|
|
688
|
-
</td>
|
|
689
|
-
<td>
|
|
690
|
-
<span class="badge bg-secondary"><%= cascade[:frequency] %>x</span>
|
|
691
|
-
</td>
|
|
692
|
-
<td>
|
|
693
|
-
<small class="text-muted">
|
|
694
|
-
<%= (cascade[:avg_delay_seconds] || 0).round(1) %>s
|
|
695
|
-
</small>
|
|
696
|
-
</td>
|
|
697
|
-
<td>
|
|
698
|
-
<%= link_to "View", error_path(parent), class: "btn btn-sm btn-outline-primary" %>
|
|
699
|
-
</td>
|
|
700
|
-
</tr>
|
|
701
|
-
<% end %>
|
|
702
|
-
</tbody>
|
|
703
|
-
</table>
|
|
704
|
-
</div>
|
|
705
|
-
</div>
|
|
706
|
-
<% end %>
|
|
73
|
+
<%= render "discussion", error: @error %>
|
|
707
74
|
|
|
708
|
-
|
|
709
|
-
<% if cascades[:children].any? %>
|
|
710
|
-
<div>
|
|
711
|
-
<h6 class="text-warning">
|
|
712
|
-
<i class="bi bi-arrow-up-circle"></i> Triggers
|
|
713
|
-
<small class="text-muted">(errors caused by this one)</small>
|
|
714
|
-
</h6>
|
|
715
|
-
<div class="table-responsive">
|
|
716
|
-
<table class="table table-sm table-hover mb-0">
|
|
717
|
-
<thead class="table-light">
|
|
718
|
-
<tr>
|
|
719
|
-
<th>Error Type</th>
|
|
720
|
-
<th>Message</th>
|
|
721
|
-
<th>Probability</th>
|
|
722
|
-
<th>Frequency</th>
|
|
723
|
-
<th>Avg Delay</th>
|
|
724
|
-
<th></th>
|
|
725
|
-
</tr>
|
|
726
|
-
</thead>
|
|
727
|
-
<tbody>
|
|
728
|
-
<% cascades[:children].each do |cascade| %>
|
|
729
|
-
<% child = cascade[:error] %>
|
|
730
|
-
<tr>
|
|
731
|
-
<td>
|
|
732
|
-
<span class="badge bg-<%= severity_color(child.severity) %>">
|
|
733
|
-
<%= child.error_type %>
|
|
734
|
-
</span>
|
|
735
|
-
</td>
|
|
736
|
-
<td>
|
|
737
|
-
<div class="text-truncate" style="max-width: 250px;">
|
|
738
|
-
<%= child.message %>
|
|
739
|
-
</div>
|
|
740
|
-
</td>
|
|
741
|
-
<td>
|
|
742
|
-
<% probability_pct = ((cascade[:probability] || 0) * 100).round(1) %>
|
|
743
|
-
<span class="badge <%= probability_pct >= 70 ? 'bg-danger' : 'bg-warning' %>">
|
|
744
|
-
<%= probability_pct %>%
|
|
745
|
-
</span>
|
|
746
|
-
</td>
|
|
747
|
-
<td>
|
|
748
|
-
<span class="badge bg-secondary"><%= cascade[:frequency] %>x</span>
|
|
749
|
-
</td>
|
|
750
|
-
<td>
|
|
751
|
-
<small class="text-muted">
|
|
752
|
-
+<%= (cascade[:avg_delay_seconds] || 0).round(1) %>s
|
|
753
|
-
</small>
|
|
754
|
-
</td>
|
|
755
|
-
<td>
|
|
756
|
-
<%= link_to "View", error_path(child), class: "btn btn-sm btn-outline-primary" %>
|
|
757
|
-
</td>
|
|
758
|
-
</tr>
|
|
759
|
-
<% end %>
|
|
760
|
-
</tbody>
|
|
761
|
-
</table>
|
|
762
|
-
</div>
|
|
763
|
-
</div>
|
|
764
|
-
<% end %>
|
|
765
|
-
|
|
766
|
-
<div class="mt-3">
|
|
767
|
-
<small class="text-muted">
|
|
768
|
-
<i class="bi bi-info-circle"></i>
|
|
769
|
-
Cascade patterns show causal relationships. High probability (≥70%) and frequency (≥3) indicate strong cascades.
|
|
770
|
-
</small>
|
|
771
|
-
</div>
|
|
772
|
-
</div>
|
|
773
|
-
</div>
|
|
774
|
-
<% end %>
|
|
775
|
-
<% end %>
|
|
75
|
+
<%= render "error_cascades", error: @error %>
|
|
776
76
|
|
|
777
77
|
<!-- Occurrence Patterns and Bursts -->
|
|
778
78
|
<%= render "pattern_insights" %>
|
|
@@ -780,281 +80,7 @@
|
|
|
780
80
|
|
|
781
81
|
<!-- Sidebar -->
|
|
782
82
|
<div class="col-md-4">
|
|
783
|
-
|
|
784
|
-
<div class="card mb-4">
|
|
785
|
-
<div class="card-header bg-white">
|
|
786
|
-
<h5 class="mb-0"><i class="bi bi-info-circle"></i> Metadata</h5>
|
|
787
|
-
</div>
|
|
788
|
-
<div class="card-body">
|
|
789
|
-
<div class="mb-3">
|
|
790
|
-
<small class="text-muted d-block mb-1">Occurrence Count</small>
|
|
791
|
-
<span class="badge bg-primary fs-5"><%= @error.occurrence_count %>x</span>
|
|
792
|
-
<% if @error.occurrence_count > 1 %>
|
|
793
|
-
<br><small class="text-muted">This error has occurred <%= @error.occurrence_count %> times</small>
|
|
794
|
-
<% end %>
|
|
795
|
-
</div>
|
|
796
|
-
|
|
797
|
-
<div class="mb-3">
|
|
798
|
-
<small class="text-muted d-block mb-1">First Seen</small>
|
|
799
|
-
<% if @related_errors.any? %>
|
|
800
|
-
<%= link_to "#timeline", class: "text-decoration-none", data: { bs_toggle: "tooltip" }, title: "Jump to timeline" do %>
|
|
801
|
-
<strong><%= local_time(@error.first_seen_at, format: :date_only) %></strong><br>
|
|
802
|
-
<small><%= local_time(@error.first_seen_at, format: :time_only) %></small>
|
|
803
|
-
<i class="bi bi-arrow-down-circle ms-1"></i>
|
|
804
|
-
<% end %>
|
|
805
|
-
<% else %>
|
|
806
|
-
<strong><%= local_time(@error.first_seen_at, format: :date_only) %></strong><br>
|
|
807
|
-
<small><%= local_time(@error.first_seen_at, format: :time_only) %></small>
|
|
808
|
-
<% end %>
|
|
809
|
-
</div>
|
|
810
|
-
|
|
811
|
-
<div class="mb-3">
|
|
812
|
-
<small class="text-muted d-block mb-1">Last Seen</small>
|
|
813
|
-
<strong><%= local_time(@error.last_seen_at, format: :date_only) %></strong><br>
|
|
814
|
-
<small><%= local_time(@error.last_seen_at, format: :time_only) %></small>
|
|
815
|
-
</div>
|
|
816
|
-
|
|
817
|
-
<div class="mb-3">
|
|
818
|
-
<small class="text-muted d-block mb-1">Severity Level</small>
|
|
819
|
-
<% severity = @error.severity %>
|
|
820
|
-
<% if severity == :critical %>
|
|
821
|
-
<span class="badge bg-danger fs-6">CRITICAL</span>
|
|
822
|
-
<% elsif severity == :high %>
|
|
823
|
-
<span class="badge bg-warning text-dark fs-6">HIGH</span>
|
|
824
|
-
<% elsif severity == :medium %>
|
|
825
|
-
<span class="badge bg-info text-dark fs-6">MEDIUM</span>
|
|
826
|
-
<% else %>
|
|
827
|
-
<span class="badge bg-secondary fs-6">LOW</span>
|
|
828
|
-
<% end %>
|
|
829
|
-
</div>
|
|
830
|
-
|
|
831
|
-
<div class="mb-3">
|
|
832
|
-
<small class="text-muted d-block mb-1">Platform</small>
|
|
833
|
-
<% if @error.platform == 'iOS' %>
|
|
834
|
-
<span class="badge badge-ios"><i class="bi bi-apple"></i> iOS</span>
|
|
835
|
-
<% elsif @error.platform == 'Android' %>
|
|
836
|
-
<span class="badge badge-android"><i class="bi bi-android2"></i> Android</span>
|
|
837
|
-
<% elsif @error.platform == 'Web' %>
|
|
838
|
-
<span class="badge badge-web"><i class="bi bi-globe"></i> Web</span>
|
|
839
|
-
<% else %>
|
|
840
|
-
<span class="badge badge-api"><i class="bi bi-server"></i> <%= @error.platform || 'API' %></span>
|
|
841
|
-
<% end %>
|
|
842
|
-
</div>
|
|
843
|
-
|
|
844
|
-
<div class="mb-3">
|
|
845
|
-
<small class="text-muted d-block mb-1">User</small>
|
|
846
|
-
<% if @error.respond_to?(:user) && @error.user %>
|
|
847
|
-
<strong><%= @error.user.email %></strong><br>
|
|
848
|
-
<small class="text-muted">ID: <%= @error.user_id %></small>
|
|
849
|
-
<% elsif @error.user_id %>
|
|
850
|
-
<span class="text-muted">User ID: <%= @error.user_id %></span>
|
|
851
|
-
<% else %>
|
|
852
|
-
<span class="text-muted">Guest / Unauthenticated</span>
|
|
853
|
-
<% end %>
|
|
854
|
-
</div>
|
|
855
|
-
|
|
856
|
-
<!-- Phase 3: Workflow Status -->
|
|
857
|
-
<div class="mb-3">
|
|
858
|
-
<small class="text-muted d-block mb-1">Workflow Status</small>
|
|
859
|
-
<% if @error.respond_to?(:status) %>
|
|
860
|
-
<% badge_color = @error.status_badge_color %>
|
|
861
|
-
<% text_dark = %w[info warning light].include?(badge_color) ? 'text-dark' : '' %>
|
|
862
|
-
<span class="badge bg-<%= badge_color %> <%= text_dark %> fs-6">
|
|
863
|
-
<%= @error.status.titleize %>
|
|
864
|
-
</span>
|
|
865
|
-
<% elsif @error.resolved? %>
|
|
866
|
-
<span class="badge bg-success">
|
|
867
|
-
<i class="bi bi-check-circle"></i> Resolved
|
|
868
|
-
</span>
|
|
869
|
-
<% if @error.resolved_at.present? %>
|
|
870
|
-
<br>
|
|
871
|
-
<small class="text-muted mt-1 d-block">
|
|
872
|
-
<%= local_time(@error.resolved_at, format: :full) %>
|
|
873
|
-
</small>
|
|
874
|
-
<% end %>
|
|
875
|
-
<% else %>
|
|
876
|
-
<span class="badge bg-danger">
|
|
877
|
-
<i class="bi bi-exclamation-circle"></i> Unresolved
|
|
878
|
-
</span>
|
|
879
|
-
<% end %>
|
|
880
|
-
</div>
|
|
881
|
-
|
|
882
|
-
<!-- Reopened indicator -->
|
|
883
|
-
<% if @error.reopened? %>
|
|
884
|
-
<div class="mb-3">
|
|
885
|
-
<small class="text-muted d-block mb-1">Reopened</small>
|
|
886
|
-
<span class="badge bg-warning text-dark">
|
|
887
|
-
<i class="bi bi-arrow-counterclockwise"></i> Reopened
|
|
888
|
-
</span>
|
|
889
|
-
<br>
|
|
890
|
-
<small class="text-muted mt-1 d-block">
|
|
891
|
-
<%= local_time(@error.reopened_at, format: :full) %>
|
|
892
|
-
</small>
|
|
893
|
-
</div>
|
|
894
|
-
<% end %>
|
|
895
|
-
|
|
896
|
-
<!-- Phase 3: Assignment -->
|
|
897
|
-
<% if @error.respond_to?(:assigned_to) %>
|
|
898
|
-
<div class="mb-3">
|
|
899
|
-
<small class="text-muted d-block mb-1">Assigned To</small>
|
|
900
|
-
<% if @error.assigned? %>
|
|
901
|
-
<div class="d-flex align-items-center justify-content-between">
|
|
902
|
-
<div>
|
|
903
|
-
<i class="bi bi-person-fill text-primary"></i>
|
|
904
|
-
<strong><%= @error.assigned_to %></strong>
|
|
905
|
-
<% if @error.assigned_at.present? %>
|
|
906
|
-
<br>
|
|
907
|
-
<small class="text-muted">
|
|
908
|
-
<%= local_time_ago(@error.assigned_at) %>
|
|
909
|
-
</small>
|
|
910
|
-
<% end %>
|
|
911
|
-
</div>
|
|
912
|
-
<%= button_to unassign_error_path(@error), method: :post, class: "btn btn-sm btn-outline-secondary",
|
|
913
|
-
data: { turbo_confirm: "Remove assignment?" } do %>
|
|
914
|
-
<i class="bi bi-x"></i>
|
|
915
|
-
<% end %>
|
|
916
|
-
</div>
|
|
917
|
-
<% else %>
|
|
918
|
-
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#assignModal">
|
|
919
|
-
<i class="bi bi-person-plus"></i> Assign
|
|
920
|
-
</button>
|
|
921
|
-
<% end %>
|
|
922
|
-
</div>
|
|
923
|
-
|
|
924
|
-
<!-- Phase 3: Priority -->
|
|
925
|
-
<div class="mb-3">
|
|
926
|
-
<small class="text-muted d-block mb-1">Priority</small>
|
|
927
|
-
<% if @error.respond_to?(:priority_level) %>
|
|
928
|
-
<span class="badge bg-<%= @error.priority_color %> fs-6">
|
|
929
|
-
<%= @error.priority_label %>
|
|
930
|
-
</span>
|
|
931
|
-
<button type="button" class="btn btn-sm btn-outline-secondary ms-2" data-bs-toggle="modal" data-bs-target="#priorityModal">
|
|
932
|
-
<i class="bi bi-pencil"></i>
|
|
933
|
-
</button>
|
|
934
|
-
<% end %>
|
|
935
|
-
</div>
|
|
936
|
-
|
|
937
|
-
<!-- Phase 3: Snooze -->
|
|
938
|
-
<div class="mb-3">
|
|
939
|
-
<small class="text-muted d-block mb-1">Snooze</small>
|
|
940
|
-
<% if @error.respond_to?(:snoozed?) && @error.snoozed? %>
|
|
941
|
-
<div class="alert alert-warning py-2 mb-2">
|
|
942
|
-
<i class="bi bi-alarm"></i>
|
|
943
|
-
<strong>Snoozed</strong><br>
|
|
944
|
-
<small>Until <%= local_time(@error.snoozed_until, format: :datetime) %></small>
|
|
945
|
-
</div>
|
|
946
|
-
<%= button_to unsnooze_error_path(@error), method: :post, class: "btn btn-sm btn-outline-warning" do %>
|
|
947
|
-
<i class="bi bi-alarm-fill"></i> Unsnooze
|
|
948
|
-
<% end %>
|
|
949
|
-
<% else %>
|
|
950
|
-
<button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#snoozeModal">
|
|
951
|
-
<i class="bi bi-alarm"></i> Snooze
|
|
952
|
-
</button>
|
|
953
|
-
<% end %>
|
|
954
|
-
</div>
|
|
955
|
-
<% end %>
|
|
956
|
-
|
|
957
|
-
<% if @error.resolved? && @error.resolved_by_name.present? %>
|
|
958
|
-
<div class="mb-3">
|
|
959
|
-
<small class="text-muted d-block mb-1">Resolved By</small>
|
|
960
|
-
<strong><%= @error.resolved_by_name %></strong>
|
|
961
|
-
</div>
|
|
962
|
-
<% end %>
|
|
963
|
-
|
|
964
|
-
<% if @error.resolved? && @error.resolution_reference.present? %>
|
|
965
|
-
<div class="mb-3">
|
|
966
|
-
<small class="text-muted d-block mb-1">Reference</small>
|
|
967
|
-
<code><%= @error.resolution_reference %></code>
|
|
968
|
-
</div>
|
|
969
|
-
<% end %>
|
|
970
|
-
|
|
971
|
-
<% if @error.resolved? && @error.resolution_comment.present? %>
|
|
972
|
-
<div class="mb-3">
|
|
973
|
-
<small class="text-muted d-block mb-1">Resolution Notes</small>
|
|
974
|
-
<div class="alert alert-success mb-0">
|
|
975
|
-
<%= auto_link_urls(@error.resolution_comment, error: @error).html_safe %>
|
|
976
|
-
</div>
|
|
977
|
-
</div>
|
|
978
|
-
<% end %>
|
|
979
|
-
|
|
980
|
-
<!-- Environment Information -->
|
|
981
|
-
<div class="mb-3">
|
|
982
|
-
<small class="text-muted d-block mb-2">
|
|
983
|
-
<i class="bi bi-gear"></i> Environment
|
|
984
|
-
</small>
|
|
985
|
-
|
|
986
|
-
<% if @error.app_version.present? %>
|
|
987
|
-
<div class="mb-1">
|
|
988
|
-
<small class="text-muted">App Version:</small>
|
|
989
|
-
<code class="ms-1"><%= @error.app_version %></code>
|
|
990
|
-
</div>
|
|
991
|
-
<% end %>
|
|
992
|
-
|
|
993
|
-
<% if @error.git_sha.present? %>
|
|
994
|
-
<div class="mb-1">
|
|
995
|
-
<small class="text-muted">Git SHA:</small>
|
|
996
|
-
<span class="ms-1"><%= git_commit_link(@error.git_sha) %></span>
|
|
997
|
-
</div>
|
|
998
|
-
<% end %>
|
|
999
|
-
|
|
1000
|
-
<% env_info = @error.respond_to?(:environment_info) && @error.environment_info.present? ?
|
|
1001
|
-
JSON.parse(@error.environment_info, symbolize_names: true) : nil rescue nil %>
|
|
1002
|
-
|
|
1003
|
-
<div class="mb-1">
|
|
1004
|
-
<small class="text-muted">Rails:</small>
|
|
1005
|
-
<code class="ms-1"><%= env_info&.dig(:rails_version) || Rails.version %></code>
|
|
1006
|
-
</div>
|
|
1007
|
-
|
|
1008
|
-
<div class="mb-1">
|
|
1009
|
-
<small class="text-muted">Ruby:</small>
|
|
1010
|
-
<code class="ms-1"><%= env_info&.dig(:ruby_version) || RUBY_VERSION %></code>
|
|
1011
|
-
</div>
|
|
1012
|
-
|
|
1013
|
-
<div class="mb-1">
|
|
1014
|
-
<small class="text-muted">Environment:</small>
|
|
1015
|
-
<% env_name = env_info&.dig(:rails_env) || Rails.env.to_s %>
|
|
1016
|
-
<span class="badge bg-<%= env_name == 'production' ? 'danger' : env_name == 'development' ? 'success' : 'warning' %> ms-1">
|
|
1017
|
-
<%= env_name.titleize %>
|
|
1018
|
-
</span>
|
|
1019
|
-
</div>
|
|
1020
|
-
|
|
1021
|
-
<% if env_info&.dig(:server).present? && env_info[:server] != "unknown" %>
|
|
1022
|
-
<div class="mb-1">
|
|
1023
|
-
<small class="text-muted">Server:</small>
|
|
1024
|
-
<code class="ms-1"><%= env_info[:server].capitalize %></code>
|
|
1025
|
-
</div>
|
|
1026
|
-
<% end %>
|
|
1027
|
-
|
|
1028
|
-
<% if env_info&.dig(:database_adapter).present? && env_info[:database_adapter] != "unknown" %>
|
|
1029
|
-
<div class="mb-1">
|
|
1030
|
-
<small class="text-muted">Database:</small>
|
|
1031
|
-
<code class="ms-1"><%= env_info[:database_adapter] %></code>
|
|
1032
|
-
</div>
|
|
1033
|
-
<% end %>
|
|
1034
|
-
|
|
1035
|
-
<% if env_info&.dig(:gem_versions)&.any? %>
|
|
1036
|
-
<div class="mb-1">
|
|
1037
|
-
<small class="text-muted d-block">Key Gems:</small>
|
|
1038
|
-
<div class="ps-2">
|
|
1039
|
-
<% env_info[:gem_versions].each do |name, version| %>
|
|
1040
|
-
<small><code><%= name %> <%= version %></code></small><br>
|
|
1041
|
-
<% end %>
|
|
1042
|
-
</div>
|
|
1043
|
-
</div>
|
|
1044
|
-
<% end %>
|
|
1045
|
-
</div>
|
|
1046
|
-
|
|
1047
|
-
<div>
|
|
1048
|
-
<small class="text-muted d-block mb-1">Error ID</small>
|
|
1049
|
-
<div class="d-flex align-items-center gap-2">
|
|
1050
|
-
<code><%= @error.id %></code>
|
|
1051
|
-
<button class="btn btn-sm btn-outline-secondary" onclick="copyToClipboard('<%= @error.id %>', this)" title="Copy error ID">
|
|
1052
|
-
<i class="bi bi-clipboard"></i>
|
|
1053
|
-
</button>
|
|
1054
|
-
</div>
|
|
1055
|
-
</div>
|
|
1056
|
-
</div>
|
|
1057
|
-
</div>
|
|
83
|
+
<%= render "sidebar_metadata", error: @error, related_errors: @related_errors %>
|
|
1058
84
|
|
|
1059
85
|
<!-- Baseline Statistics -->
|
|
1060
86
|
<% if RailsErrorDashboard.configuration.enable_baseline_alerts %>
|
|
@@ -1184,142 +210,4 @@
|
|
|
1184
210
|
</div>
|
|
1185
211
|
</div>
|
|
1186
212
|
|
|
1187
|
-
|
|
1188
|
-
<div class="modal fade" id="resolveModal" tabindex="-1" aria-labelledby="resolveModalLabel" aria-hidden="true">
|
|
1189
|
-
<div class="modal-dialog">
|
|
1190
|
-
<div class="modal-content">
|
|
1191
|
-
<%= form_with url: resolve_error_path(@error), method: :post, class: "modal-content" do |f| %>
|
|
1192
|
-
<div class="modal-header">
|
|
1193
|
-
<h5 class="modal-title" id="resolveModalLabel">
|
|
1194
|
-
<i class="bi bi-check-circle"></i> Mark Error as Resolved
|
|
1195
|
-
</h5>
|
|
1196
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
1197
|
-
</div>
|
|
1198
|
-
<div class="modal-body">
|
|
1199
|
-
<div class="mb-3">
|
|
1200
|
-
<label for="resolved_by_name" class="form-label">Your Name <span class="text-danger">*</span></label>
|
|
1201
|
-
<%= text_field_tag :resolved_by_name, nil, class: "form-control", placeholder: "e.g., John Doe", required: true %>
|
|
1202
|
-
<small class="text-muted">Who is resolving this error?</small>
|
|
1203
|
-
</div>
|
|
1204
|
-
|
|
1205
|
-
<div class="mb-3">
|
|
1206
|
-
<label for="resolution_reference" class="form-label">Reference (Optional)</label>
|
|
1207
|
-
<%= text_field_tag :resolution_reference, nil, class: "form-control", placeholder: "e.g., JIRA-123, PR #456, GitHub Issue #789" %>
|
|
1208
|
-
<small class="text-muted">Link to ticket, PR, or issue</small>
|
|
1209
|
-
</div>
|
|
1210
|
-
|
|
1211
|
-
<div class="mb-3">
|
|
1212
|
-
<label for="resolution_comment" class="form-label">Resolution Notes (Optional)</label>
|
|
1213
|
-
<%= text_area_tag :resolution_comment, nil, class: "form-control", rows: 4, placeholder: "Describe what was done to fix this error..." %>
|
|
1214
|
-
<small class="text-muted">What was done to resolve this error?</small>
|
|
1215
|
-
</div>
|
|
1216
|
-
</div>
|
|
1217
|
-
<div class="modal-footer">
|
|
1218
|
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
1219
|
-
<%= submit_tag "Mark as Resolved", class: "btn btn-success", data: { action: "click->loading#click" } %>
|
|
1220
|
-
</div>
|
|
1221
|
-
<% end %>
|
|
1222
|
-
</div>
|
|
1223
|
-
</div>
|
|
1224
|
-
</div>
|
|
1225
|
-
|
|
1226
|
-
<!-- Phase 3: Assignment Modal -->
|
|
1227
|
-
<div class="modal fade" id="assignModal" tabindex="-1" aria-labelledby="assignModalLabel" aria-hidden="true">
|
|
1228
|
-
<div class="modal-dialog">
|
|
1229
|
-
<div class="modal-content">
|
|
1230
|
-
<%= form_with url: assign_error_path(@error), method: :post do |f| %>
|
|
1231
|
-
<div class="modal-header">
|
|
1232
|
-
<h5 class="modal-title" id="assignModalLabel">
|
|
1233
|
-
<i class="bi bi-person-plus"></i> Assign Error
|
|
1234
|
-
</h5>
|
|
1235
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
1236
|
-
</div>
|
|
1237
|
-
<div class="modal-body">
|
|
1238
|
-
<div class="mb-3">
|
|
1239
|
-
<label for="assigned_to" class="form-label">Assign To <span class="text-danger">*</span></label>
|
|
1240
|
-
<%= text_field_tag :assigned_to, nil, class: "form-control", placeholder: "e.g., John Doe", required: true, autofocus: true %>
|
|
1241
|
-
<small class="text-muted">Who should investigate this error?</small>
|
|
1242
|
-
</div>
|
|
1243
|
-
</div>
|
|
1244
|
-
<div class="modal-footer">
|
|
1245
|
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
1246
|
-
<%= submit_tag "Assign", class: "btn btn-primary", data: { action: "click->loading#click" } %>
|
|
1247
|
-
</div>
|
|
1248
|
-
<% end %>
|
|
1249
|
-
</div>
|
|
1250
|
-
</div>
|
|
1251
|
-
</div>
|
|
1252
|
-
|
|
1253
|
-
<!-- Phase 3: Priority Modal -->
|
|
1254
|
-
<div class="modal fade" id="priorityModal" tabindex="-1" aria-labelledby="priorityModalLabel" aria-hidden="true">
|
|
1255
|
-
<div class="modal-dialog">
|
|
1256
|
-
<div class="modal-content">
|
|
1257
|
-
<%= form_with url: update_priority_error_path(@error), method: :post do |f| %>
|
|
1258
|
-
<div class="modal-header">
|
|
1259
|
-
<h5 class="modal-title" id="priorityModalLabel">
|
|
1260
|
-
<i class="bi bi-flag"></i> Update Priority
|
|
1261
|
-
</h5>
|
|
1262
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
1263
|
-
</div>
|
|
1264
|
-
<div class="modal-body">
|
|
1265
|
-
<div class="mb-3">
|
|
1266
|
-
<label for="priority_level" class="form-label">Priority Level <span class="text-danger">*</span></label>
|
|
1267
|
-
<%= select_tag :priority_level,
|
|
1268
|
-
options_for_select(
|
|
1269
|
-
RailsErrorDashboard::ErrorLog.priority_options,
|
|
1270
|
-
@error.priority_level
|
|
1271
|
-
),
|
|
1272
|
-
class: "form-select", required: true %>
|
|
1273
|
-
<small class="text-muted">Current: <%= @error.priority_label %></small>
|
|
1274
|
-
</div>
|
|
1275
|
-
</div>
|
|
1276
|
-
<div class="modal-footer">
|
|
1277
|
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
1278
|
-
<%= submit_tag "Update Priority", class: "btn btn-warning", data: { action: "click->loading#click" } %>
|
|
1279
|
-
</div>
|
|
1280
|
-
<% end %>
|
|
1281
|
-
</div>
|
|
1282
|
-
</div>
|
|
1283
|
-
</div>
|
|
1284
|
-
|
|
1285
|
-
<!-- Phase 3: Snooze Modal -->
|
|
1286
|
-
<div class="modal fade" id="snoozeModal" tabindex="-1" aria-labelledby="snoozeModalLabel" aria-hidden="true">
|
|
1287
|
-
<div class="modal-dialog">
|
|
1288
|
-
<div class="modal-content">
|
|
1289
|
-
<%= form_with url: snooze_error_path(@error), method: :post do |f| %>
|
|
1290
|
-
<div class="modal-header">
|
|
1291
|
-
<h5 class="modal-title" id="snoozeModalLabel">
|
|
1292
|
-
<i class="bi bi-alarm"></i> Snooze Error
|
|
1293
|
-
</h5>
|
|
1294
|
-
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
1295
|
-
</div>
|
|
1296
|
-
<div class="modal-body">
|
|
1297
|
-
<div class="mb-3">
|
|
1298
|
-
<label for="hours" class="form-label">Snooze Duration <span class="text-danger">*</span></label>
|
|
1299
|
-
<%= select_tag :hours,
|
|
1300
|
-
options_for_select([
|
|
1301
|
-
['1 hour', 1],
|
|
1302
|
-
['4 hours', 4],
|
|
1303
|
-
['8 hours', 8],
|
|
1304
|
-
['24 hours (1 day)', 24],
|
|
1305
|
-
['48 hours (2 days)', 48],
|
|
1306
|
-
['168 hours (1 week)', 168]
|
|
1307
|
-
], 24),
|
|
1308
|
-
class: "form-select", required: true %>
|
|
1309
|
-
<small class="text-muted">Hide this error from active views</small>
|
|
1310
|
-
</div>
|
|
1311
|
-
|
|
1312
|
-
<div class="mb-3">
|
|
1313
|
-
<label for="reason" class="form-label">Reason (Optional)</label>
|
|
1314
|
-
<%= text_area_tag :reason, nil, class: "form-control", rows: 3, placeholder: "Why are you snoozing this error?" %>
|
|
1315
|
-
<small class="text-muted">Reason will be added as a comment</small>
|
|
1316
|
-
</div>
|
|
1317
|
-
</div>
|
|
1318
|
-
<div class="modal-footer">
|
|
1319
|
-
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
1320
|
-
<%= submit_tag "Snooze", class: "btn btn-warning", data: { action: "click->loading#click" } %>
|
|
1321
|
-
</div>
|
|
1322
|
-
<% end %>
|
|
1323
|
-
</div>
|
|
1324
|
-
</div>
|
|
1325
|
-
</div>
|
|
213
|
+
<%= render "modals", error: @error %>
|