rails_error_dashboard 0.1.1 → 0.1.4
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 +92 -21
- data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +107 -0
- data/app/assets/stylesheets/rails_error_dashboard/_components.scss +625 -0
- data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +257 -0
- data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +203 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.css.map +7 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.scss +61 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +135 -1
- data/app/helpers/rails_error_dashboard/application_helper.rb +80 -4
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +91 -0
- data/app/helpers/rails_error_dashboard/overview_helper.rb +78 -0
- data/app/helpers/rails_error_dashboard/user_agent_helper.rb +118 -0
- data/app/models/rails_error_dashboard/error_comment.rb +27 -0
- data/app/models/rails_error_dashboard/error_log.rb +159 -0
- data/app/views/layouts/rails_error_dashboard/application.html.erb +39 -1
- data/app/views/layouts/rails_error_dashboard.html.erb +796 -299
- data/app/views/layouts/rails_error_dashboard_old_backup.html.erb +383 -0
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +2 -0
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +4 -4
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +167 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +439 -22
- data/app/views/rails_error_dashboard/errors/index.html.erb +127 -11
- data/app/views/rails_error_dashboard/errors/overview.html.erb +253 -0
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +29 -18
- data/app/views/rails_error_dashboard/errors/show.html.erb +353 -54
- data/config/routes.rb +11 -1
- data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +27 -0
- data/db/migrate/20251226020100_create_error_comments.rb +18 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +8 -2
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +21 -0
- data/lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +317 -0
- data/lib/rails_error_dashboard/commands/batch_delete_errors.rb +1 -1
- data/lib/rails_error_dashboard/commands/batch_resolve_errors.rb +2 -2
- data/lib/rails_error_dashboard/commands/log_error.rb +47 -9
- data/lib/rails_error_dashboard/commands/resolve_error.rb +1 -1
- data/lib/rails_error_dashboard/configuration.rb +8 -0
- data/lib/rails_error_dashboard/error_reporter.rb +4 -4
- data/lib/rails_error_dashboard/logger.rb +105 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +2 -2
- data/lib/rails_error_dashboard/plugin.rb +3 -3
- data/lib/rails_error_dashboard/plugin_registry.rb +2 -2
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +1 -1
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +1 -1
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +109 -1
- data/lib/rails_error_dashboard/queries/errors_list.rb +134 -7
- data/lib/rails_error_dashboard/queries/mttr_stats.rb +111 -0
- data/lib/rails_error_dashboard/queries/recurring_issues.rb +97 -0
- data/lib/rails_error_dashboard/services/backtrace_parser.rb +113 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +5 -0
- data/lib/tasks/rails_error_dashboard_tasks.rake +85 -4
- metadata +36 -2
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
// Dynamic chart colors based on theme
|
|
3
|
+
window.getChartColors = function() {
|
|
4
|
+
const isDark = document.body.classList.contains('dark-mode');
|
|
5
|
+
return {
|
|
6
|
+
textColor: isDark ? '#cdd6f4' : '#1f2937',
|
|
7
|
+
gridColor: isDark ? 'rgba(88, 91, 112, 0.2)' : 'rgba(0, 0, 0, 0.1)'
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
window.getChartLibraryOptions = function() {
|
|
12
|
+
const colors = window.getChartColors();
|
|
13
|
+
return {
|
|
14
|
+
scales: {
|
|
15
|
+
x: {
|
|
16
|
+
ticks: { color: colors.textColor },
|
|
17
|
+
title: { color: colors.textColor },
|
|
18
|
+
grid: { color: colors.gridColor }
|
|
19
|
+
},
|
|
20
|
+
y: {
|
|
21
|
+
ticks: { color: colors.textColor },
|
|
22
|
+
title: { color: colors.textColor },
|
|
23
|
+
grid: { color: colors.gridColor }
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
plugins: {
|
|
27
|
+
legend: { labels: { color: colors.textColor } }
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
</script>
|
|
32
|
+
|
|
1
33
|
<div class="py-4">
|
|
2
34
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
3
35
|
<h2 class="mb-0"><i class="bi bi-graph-up text-primary"></i> Error Analytics</h2>
|
|
@@ -48,13 +80,37 @@
|
|
|
48
80
|
<h5 class="mb-0"><i class="bi bi-graph-up"></i> Error Trend (Last <%= @days %> Days)</h5>
|
|
49
81
|
</div>
|
|
50
82
|
<div class="card-body">
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
83
|
+
<div id="errors-over-time-chart"></div>
|
|
84
|
+
<script>
|
|
85
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
86
|
+
const colors = window.getChartColors();
|
|
87
|
+
new Chartkick.LineChart("errors-over-time-chart", <%= raw @errors_over_time.to_json %>, {
|
|
88
|
+
color: "#8B5CF6",
|
|
89
|
+
curve: false,
|
|
90
|
+
points: true,
|
|
91
|
+
height: "300px",
|
|
92
|
+
xtitle: "Date",
|
|
93
|
+
ytitle: "Number of Errors",
|
|
94
|
+
library: {
|
|
95
|
+
scales: {
|
|
96
|
+
x: {
|
|
97
|
+
ticks: { color: colors.textColor },
|
|
98
|
+
title: { color: colors.textColor, display: true, text: 'Date' },
|
|
99
|
+
grid: { color: colors.gridColor }
|
|
100
|
+
},
|
|
101
|
+
y: {
|
|
102
|
+
ticks: { color: colors.textColor },
|
|
103
|
+
title: { color: colors.textColor, display: true, text: 'Number of Errors' },
|
|
104
|
+
grid: { color: colors.gridColor }
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
plugins: {
|
|
108
|
+
legend: { labels: { color: colors.textColor } }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
</script>
|
|
58
114
|
</div>
|
|
59
115
|
</div>
|
|
60
116
|
|
|
@@ -69,11 +125,23 @@
|
|
|
69
125
|
<h5 class="mb-0"><i class="bi bi-phone"></i> Errors by Platform</h5>
|
|
70
126
|
</div>
|
|
71
127
|
<div class="card-body">
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
128
|
+
<div id="errors-by-platform-chart"></div>
|
|
129
|
+
<script>
|
|
130
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
131
|
+
const colors = window.getChartColors();
|
|
132
|
+
new Chartkick.PieChart("errors-by-platform-chart", <%= raw @errors_by_platform.to_json %>, {
|
|
133
|
+
colors: ["#000000", "#3DDC84", "#3B82F6"],
|
|
134
|
+
height: "300px",
|
|
135
|
+
legend: "bottom",
|
|
136
|
+
donut: true,
|
|
137
|
+
library: {
|
|
138
|
+
plugins: {
|
|
139
|
+
legend: { labels: { color: colors.textColor } }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
</script>
|
|
77
145
|
</div>
|
|
78
146
|
</div>
|
|
79
147
|
</div>
|
|
@@ -88,11 +156,35 @@
|
|
|
88
156
|
<h5 class="mb-0"><i class="bi bi-bug"></i> Top 10 Error Types</h5>
|
|
89
157
|
</div>
|
|
90
158
|
<div class="card-body">
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
159
|
+
<div id="errors-by-type-chart"></div>
|
|
160
|
+
<script>
|
|
161
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
162
|
+
const colors = window.getChartColors();
|
|
163
|
+
new Chartkick.BarChart("errors-by-type-chart", <%= raw @errors_by_type.to_json %>, {
|
|
164
|
+
color: "#EF4444",
|
|
165
|
+
height: "400px",
|
|
166
|
+
xtitle: "Error Type",
|
|
167
|
+
ytitle: "Count",
|
|
168
|
+
library: {
|
|
169
|
+
scales: {
|
|
170
|
+
x: {
|
|
171
|
+
ticks: { color: colors.textColor },
|
|
172
|
+
title: { color: colors.textColor, display: true, text: 'Error Type' },
|
|
173
|
+
grid: { color: colors.gridColor }
|
|
174
|
+
},
|
|
175
|
+
y: {
|
|
176
|
+
ticks: { color: colors.textColor },
|
|
177
|
+
title: { color: colors.textColor, display: true, text: 'Count' },
|
|
178
|
+
grid: { color: colors.gridColor }
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
plugins: {
|
|
182
|
+
legend: { labels: { color: colors.textColor } }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
</script>
|
|
96
188
|
</div>
|
|
97
189
|
</div>
|
|
98
190
|
</div>
|
|
@@ -106,11 +198,35 @@
|
|
|
106
198
|
<h5 class="mb-0"><i class="bi bi-clock"></i> Errors by Hour of Day</h5>
|
|
107
199
|
</div>
|
|
108
200
|
<div class="card-body">
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
201
|
+
<div id="errors-by-hour-chart"></div>
|
|
202
|
+
<script>
|
|
203
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
204
|
+
const colors = window.getChartColors();
|
|
205
|
+
new Chartkick.ColumnChart("errors-by-hour-chart", <%= raw @errors_by_hour.to_json %>, {
|
|
206
|
+
color: "#8B5CF6",
|
|
207
|
+
height: "300px",
|
|
208
|
+
xtitle: "Hour",
|
|
209
|
+
ytitle: "Number of Errors",
|
|
210
|
+
library: {
|
|
211
|
+
scales: {
|
|
212
|
+
x: {
|
|
213
|
+
ticks: { color: colors.textColor },
|
|
214
|
+
title: { color: colors.textColor, display: true, text: 'Hour' },
|
|
215
|
+
grid: { color: colors.gridColor }
|
|
216
|
+
},
|
|
217
|
+
y: {
|
|
218
|
+
ticks: { color: colors.textColor },
|
|
219
|
+
title: { color: colors.textColor, display: true, text: 'Number of Errors' },
|
|
220
|
+
grid: { color: colors.gridColor }
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
plugins: {
|
|
224
|
+
legend: { labels: { color: colors.textColor } }
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
</script>
|
|
114
230
|
</div>
|
|
115
231
|
</div>
|
|
116
232
|
</div>
|
|
@@ -214,4 +330,305 @@
|
|
|
214
330
|
</div>
|
|
215
331
|
</div>
|
|
216
332
|
</div>
|
|
333
|
+
|
|
334
|
+
<!-- Recurring Issues Analysis -->
|
|
335
|
+
<div class="card mb-4">
|
|
336
|
+
<div class="card-header bg-white">
|
|
337
|
+
<h5 class="mb-0"><i class="bi bi-arrow-repeat"></i> Recurring Issues</h5>
|
|
338
|
+
</div>
|
|
339
|
+
<div class="card-body">
|
|
340
|
+
<% if @recurring_data[:high_frequency_errors].any? %>
|
|
341
|
+
<h6 class="text-muted mb-3">High Frequency Errors</h6>
|
|
342
|
+
<div class="table-responsive mb-4">
|
|
343
|
+
<table class="table table-sm">
|
|
344
|
+
<thead>
|
|
345
|
+
<tr>
|
|
346
|
+
<th>Error Type</th>
|
|
347
|
+
<th>Total Occurrences</th>
|
|
348
|
+
<th>Duration</th>
|
|
349
|
+
<th>First Seen</th>
|
|
350
|
+
<th>Last Seen</th>
|
|
351
|
+
<th>Status</th>
|
|
352
|
+
</tr>
|
|
353
|
+
</thead>
|
|
354
|
+
<tbody>
|
|
355
|
+
<% @recurring_data[:high_frequency_errors].first(10).each do |error| %>
|
|
356
|
+
<tr>
|
|
357
|
+
<td><code class="small"><%= error[:error_type] %></code></td>
|
|
358
|
+
<td><span class="badge bg-danger"><%= error[:total_occurrences] %></span></td>
|
|
359
|
+
<td><%= error[:duration_days] %> days</td>
|
|
360
|
+
<td><small class="text-muted"><%= error[:first_seen].strftime("%b %d, %Y") %></small></td>
|
|
361
|
+
<td><small class="text-muted"><%= error[:last_seen].strftime("%b %d, %Y %H:%M") %></small></td>
|
|
362
|
+
<td>
|
|
363
|
+
<% if error[:still_active] %>
|
|
364
|
+
<span class="badge bg-warning">Active</span>
|
|
365
|
+
<% else %>
|
|
366
|
+
<span class="badge bg-secondary">Inactive</span>
|
|
367
|
+
<% end %>
|
|
368
|
+
</td>
|
|
369
|
+
</tr>
|
|
370
|
+
<% end %>
|
|
371
|
+
</tbody>
|
|
372
|
+
</table>
|
|
373
|
+
</div>
|
|
374
|
+
<% else %>
|
|
375
|
+
<p class="text-muted">No high-frequency errors found in the last <%= @days %> days.</p>
|
|
376
|
+
<% end %>
|
|
377
|
+
|
|
378
|
+
<% if @recurring_data[:persistent_errors].any? %>
|
|
379
|
+
<h6 class="text-muted mb-3 mt-4">Persistent Unresolved Errors</h6>
|
|
380
|
+
<div class="alert alert-info">
|
|
381
|
+
<i class="bi bi-info-circle"></i> These errors have been unresolved for more than 7 days.
|
|
382
|
+
</div>
|
|
383
|
+
<div class="table-responsive">
|
|
384
|
+
<table class="table table-sm">
|
|
385
|
+
<thead>
|
|
386
|
+
<tr>
|
|
387
|
+
<th>Error Type</th>
|
|
388
|
+
<th>Message</th>
|
|
389
|
+
<th>Platform</th>
|
|
390
|
+
<th>Count</th>
|
|
391
|
+
<th>Age</th>
|
|
392
|
+
<th>Actions</th>
|
|
393
|
+
</tr>
|
|
394
|
+
</thead>
|
|
395
|
+
<tbody>
|
|
396
|
+
<% @recurring_data[:persistent_errors].first(10).each do |error| %>
|
|
397
|
+
<tr>
|
|
398
|
+
<td><code class="small"><%= error[:error_type] %></code></td>
|
|
399
|
+
<td class="text-muted small"><%= error[:message] %></td>
|
|
400
|
+
<td><%= error[:platform] %></td>
|
|
401
|
+
<td><span class="badge bg-secondary"><%= error[:occurrence_count] %>x</span></td>
|
|
402
|
+
<td>
|
|
403
|
+
<span class="badge bg-warning"><%= error[:age_days] %> days</span>
|
|
404
|
+
</td>
|
|
405
|
+
<td>
|
|
406
|
+
<%= link_to "View", error_path(error[:id]), class: "btn btn-sm btn-outline-primary" %>
|
|
407
|
+
</td>
|
|
408
|
+
</tr>
|
|
409
|
+
<% end %>
|
|
410
|
+
</tbody>
|
|
411
|
+
</table>
|
|
412
|
+
</div>
|
|
413
|
+
<% end %>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<!-- Release Comparison Charts -->
|
|
418
|
+
<% if @errors_by_version.present? && @errors_by_version.count >= 2 %>
|
|
419
|
+
<div class="row g-4 mb-4">
|
|
420
|
+
<!-- Errors by Version Chart -->
|
|
421
|
+
<div class="col-md-8">
|
|
422
|
+
<div class="card">
|
|
423
|
+
<div class="card-header bg-white">
|
|
424
|
+
<h5 class="mb-0"><i class="bi bi-git"></i> Errors by Release Version</h5>
|
|
425
|
+
</div>
|
|
426
|
+
<div class="card-body">
|
|
427
|
+
<div id="errors-by-version-chart"></div>
|
|
428
|
+
<script>
|
|
429
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
430
|
+
const versionData = <%= raw @errors_by_version.transform_values { |v| v[:count] }.to_json %>;
|
|
431
|
+
const colors = window.getChartColors();
|
|
432
|
+
|
|
433
|
+
new Chartkick.ColumnChart("errors-by-version-chart", versionData, {
|
|
434
|
+
colors: ["#8B5CF6", "#EF4444", "#F59E0B", "#10B981"],
|
|
435
|
+
height: "300px",
|
|
436
|
+
xtitle: "Version",
|
|
437
|
+
ytitle: "Error Count",
|
|
438
|
+
library: window.getChartLibraryOptions()
|
|
439
|
+
});
|
|
440
|
+
});
|
|
441
|
+
</script>
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<!-- Release Comparison Card -->
|
|
447
|
+
<div class="col-md-4">
|
|
448
|
+
<div class="card">
|
|
449
|
+
<div class="card-header bg-white">
|
|
450
|
+
<h5 class="mb-0">Latest vs Previous</h5>
|
|
451
|
+
</div>
|
|
452
|
+
<div class="card-body">
|
|
453
|
+
<% if @release_comparison.present? %>
|
|
454
|
+
<div class="mb-3">
|
|
455
|
+
<small class="text-muted">Latest Version</small>
|
|
456
|
+
<h4><%= @release_comparison[:latest_version] %></h4>
|
|
457
|
+
<p class="mb-0">
|
|
458
|
+
<span class="badge bg-danger"><%= @release_comparison[:latest_count] %> errors</span>
|
|
459
|
+
<% if @release_comparison[:latest_critical] > 0 %>
|
|
460
|
+
<span class="badge bg-dark ms-1"><%= @release_comparison[:latest_critical] %> critical</span>
|
|
461
|
+
<% end %>
|
|
462
|
+
</p>
|
|
463
|
+
</div>
|
|
464
|
+
<div class="mb-3">
|
|
465
|
+
<small class="text-muted">Previous Version</small>
|
|
466
|
+
<h4><%= @release_comparison[:previous_version] %></h4>
|
|
467
|
+
<p class="mb-0">
|
|
468
|
+
<span class="badge bg-secondary"><%= @release_comparison[:previous_count] %> errors</span>
|
|
469
|
+
<% if @release_comparison[:previous_critical] > 0 %>
|
|
470
|
+
<span class="badge bg-secondary ms-1"><%= @release_comparison[:previous_critical] %> critical</span>
|
|
471
|
+
<% end %>
|
|
472
|
+
</p>
|
|
473
|
+
</div>
|
|
474
|
+
<hr>
|
|
475
|
+
<div>
|
|
476
|
+
<small class="text-muted">Change</small>
|
|
477
|
+
<h4 class="<%= @release_comparison[:change_percentage] > 0 ? 'text-danger' : 'text-success' %>">
|
|
478
|
+
<%= @release_comparison[:change_percentage] > 0 ? '↑' : '↓' %>
|
|
479
|
+
<%= @release_comparison[:change_percentage].abs %>%
|
|
480
|
+
</h4>
|
|
481
|
+
</div>
|
|
482
|
+
<% else %>
|
|
483
|
+
<p class="text-muted">Not enough release data for comparison.</p>
|
|
484
|
+
<% end %>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
|
|
490
|
+
<!-- Problematic Releases Alert -->
|
|
491
|
+
<% if @problematic_releases.any? %>
|
|
492
|
+
<div class="alert alert-warning mb-4">
|
|
493
|
+
<h5 class="alert-heading"><i class="bi bi-exclamation-triangle"></i> Problematic Releases Detected</h5>
|
|
494
|
+
<p>The following releases have significantly higher error rates (>2x average):</p>
|
|
495
|
+
<ul class="mb-0">
|
|
496
|
+
<% @problematic_releases.each do |release| %>
|
|
497
|
+
<li>
|
|
498
|
+
<strong><%= release[:version] %></strong>:
|
|
499
|
+
<%= release[:error_count] %> errors
|
|
500
|
+
(<%= release[:critical_count] %> critical,
|
|
501
|
+
+<%= release[:deviation_from_avg] %>% from average)
|
|
502
|
+
</li>
|
|
503
|
+
<% end %>
|
|
504
|
+
</ul>
|
|
505
|
+
</div>
|
|
506
|
+
<% end %>
|
|
507
|
+
<% end %>
|
|
508
|
+
|
|
509
|
+
<!-- MTTR (Mean Time to Resolution) -->
|
|
510
|
+
<% if @mttr_stats[:total_resolved] > 0 %>
|
|
511
|
+
<div class="card mb-4">
|
|
512
|
+
<div class="card-header bg-white">
|
|
513
|
+
<h5 class="mb-0"><i class="bi bi-clock-history"></i> Resolution Performance (MTTR)</h5>
|
|
514
|
+
</div>
|
|
515
|
+
<div class="card-body">
|
|
516
|
+
<div class="row g-3 mb-4">
|
|
517
|
+
<!-- Overall MTTR -->
|
|
518
|
+
<div class="col-md-3">
|
|
519
|
+
<div class="card border-info">
|
|
520
|
+
<div class="card-body text-center">
|
|
521
|
+
<small class="text-muted d-block">Overall MTTR</small>
|
|
522
|
+
<h3 class="text-info mb-0"><%= @mttr_stats[:overall_mttr] %>h</h3>
|
|
523
|
+
<small class="text-muted">Average resolution time</small>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
|
|
528
|
+
<!-- Fastest -->
|
|
529
|
+
<div class="col-md-3">
|
|
530
|
+
<div class="card border-success">
|
|
531
|
+
<div class="card-body text-center">
|
|
532
|
+
<small class="text-muted d-block">Fastest</small>
|
|
533
|
+
<h3 class="text-success mb-0"><%= @mttr_stats[:fastest_resolution] || 0 %>m</h3>
|
|
534
|
+
<small class="text-muted">Best resolution time</small>
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<!-- Slowest -->
|
|
540
|
+
<div class="col-md-3">
|
|
541
|
+
<div class="card border-danger">
|
|
542
|
+
<div class="card-body text-center">
|
|
543
|
+
<small class="text-muted d-block">Slowest</small>
|
|
544
|
+
<h3 class="text-danger mb-0"><%= @mttr_stats[:slowest_resolution] || 0 %>h</h3>
|
|
545
|
+
<small class="text-muted">Longest resolution time</small>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<!-- Total Resolved -->
|
|
551
|
+
<div class="col-md-3">
|
|
552
|
+
<div class="card border-secondary">
|
|
553
|
+
<div class="card-body text-center">
|
|
554
|
+
<small class="text-muted d-block">Total Resolved</small>
|
|
555
|
+
<h3 class="text-secondary mb-0"><%= @mttr_stats[:total_resolved] %></h3>
|
|
556
|
+
<small class="text-muted">In last <%= @days %> days</small>
|
|
557
|
+
</div>
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
|
|
562
|
+
<!-- MTTR by Platform -->
|
|
563
|
+
<% if @mttr_by_platform.present? && @mttr_by_platform.any? %>
|
|
564
|
+
<div class="mt-4">
|
|
565
|
+
<h6 class="text-muted mb-3">MTTR by Platform</h6>
|
|
566
|
+
<div id="mttr-by-platform-chart"></div>
|
|
567
|
+
<script>
|
|
568
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
569
|
+
const colors = window.getChartColors();
|
|
570
|
+
new Chartkick.BarChart("mttr-by-platform-chart",
|
|
571
|
+
<%= raw @mttr_by_platform.to_json %>, {
|
|
572
|
+
suffix: " hours",
|
|
573
|
+
height: "200px",
|
|
574
|
+
colors: ["#8B5CF6"],
|
|
575
|
+
library: window.getChartLibraryOptions()
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
</script>
|
|
579
|
+
</div>
|
|
580
|
+
<% end %>
|
|
581
|
+
|
|
582
|
+
<!-- MTTR by Severity -->
|
|
583
|
+
<% if @mttr_stats[:mttr_by_severity].present? && @mttr_stats[:mttr_by_severity].any? %>
|
|
584
|
+
<div class="mt-4">
|
|
585
|
+
<h6 class="text-muted mb-3">MTTR by Severity</h6>
|
|
586
|
+
<div class="table-responsive">
|
|
587
|
+
<table class="table table-sm">
|
|
588
|
+
<thead>
|
|
589
|
+
<tr>
|
|
590
|
+
<th>Severity</th>
|
|
591
|
+
<th>Average Time to Resolution</th>
|
|
592
|
+
</tr>
|
|
593
|
+
</thead>
|
|
594
|
+
<tbody>
|
|
595
|
+
<% @mttr_stats[:mttr_by_severity].each do |severity, hours| %>
|
|
596
|
+
<tr>
|
|
597
|
+
<td>
|
|
598
|
+
<span class="badge bg-<%= severity_color(severity) %>">
|
|
599
|
+
<%= severity.to_s.capitalize %>
|
|
600
|
+
</span>
|
|
601
|
+
</td>
|
|
602
|
+
<td><strong><%= hours %> hours</strong></td>
|
|
603
|
+
</tr>
|
|
604
|
+
<% end %>
|
|
605
|
+
</tbody>
|
|
606
|
+
</table>
|
|
607
|
+
</div>
|
|
608
|
+
</div>
|
|
609
|
+
<% end %>
|
|
610
|
+
|
|
611
|
+
<!-- MTTR Trend -->
|
|
612
|
+
<% if @mttr_stats[:mttr_trend].present? && @mttr_stats[:mttr_trend].any? %>
|
|
613
|
+
<div class="mt-4">
|
|
614
|
+
<h6 class="text-muted mb-3">MTTR Trend (Weekly)</h6>
|
|
615
|
+
<div id="mttr-trend-chart"></div>
|
|
616
|
+
<script>
|
|
617
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
618
|
+
const colors = window.getChartColors();
|
|
619
|
+
new Chartkick.LineChart("mttr-trend-chart",
|
|
620
|
+
<%= raw @mttr_stats[:mttr_trend].to_json %>, {
|
|
621
|
+
suffix: " hours",
|
|
622
|
+
height: "250px",
|
|
623
|
+
colors: ["#10B981"],
|
|
624
|
+
curve: false,
|
|
625
|
+
library: window.getChartLibraryOptions()
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
</script>
|
|
629
|
+
</div>
|
|
630
|
+
<% end %>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
<% end %>
|
|
217
634
|
</div>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
<!-- Subscribe to Turbo Stream updates -->
|
|
2
|
-
|
|
1
|
+
<!-- Subscribe to Turbo Stream updates (only if ActionCable is available) -->
|
|
2
|
+
<% if defined?(ActionCable) %>
|
|
3
|
+
<%= turbo_stream_from "error_list" %>
|
|
4
|
+
<% end %>
|
|
3
5
|
|
|
4
6
|
<div class="py-4">
|
|
5
7
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
@@ -119,12 +121,22 @@
|
|
|
119
121
|
<% end %>
|
|
120
122
|
|
|
121
123
|
<!-- Filters -->
|
|
122
|
-
<div class="card mb-4">
|
|
124
|
+
<div class="card mb-4" id="filters-section">
|
|
123
125
|
<div class="card-header bg-white">
|
|
124
126
|
<h5 class="mb-0">Filters & Search</h5>
|
|
125
127
|
</div>
|
|
126
128
|
<div class="card-body">
|
|
127
|
-
|
|
129
|
+
<!-- Quick Filter Buttons -->
|
|
130
|
+
<div class="d-flex gap-2 mb-3">
|
|
131
|
+
<%= link_to "All Errors", errors_path,
|
|
132
|
+
class: "btn btn-sm #{params[:assigned_to].blank? ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
133
|
+
<%= link_to "Unassigned", errors_path(assigned_to: '__unassigned__'),
|
|
134
|
+
class: "btn btn-sm #{params[:assigned_to] == '__unassigned__' ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
135
|
+
<%= link_to "My Errors", errors_path(assigned_to: current_user_name),
|
|
136
|
+
class: "btn btn-sm #{params[:assigned_to] == current_user_name ? 'btn-primary' : 'btn-outline-primary'}" %>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>
|
|
128
140
|
<div class="col-md-4">
|
|
129
141
|
<%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
|
|
130
142
|
</div>
|
|
@@ -149,11 +161,82 @@
|
|
|
149
161
|
], params[:severity]), class: "form-select" %>
|
|
150
162
|
</div>
|
|
151
163
|
|
|
164
|
+
<!-- Time Range Filter -->
|
|
165
|
+
<div class="col-md-2">
|
|
166
|
+
<%= select_tag :timeframe, options_for_select([
|
|
167
|
+
['All Time', ''],
|
|
168
|
+
['Last Hour', 'last_hour'],
|
|
169
|
+
['Today', 'today'],
|
|
170
|
+
['Yesterday', 'yesterday'],
|
|
171
|
+
['Last 7 Days', 'last_7_days'],
|
|
172
|
+
['Last 30 Days', 'last_30_days'],
|
|
173
|
+
['Last 90 Days', 'last_90_days']
|
|
174
|
+
], params[:timeframe]), class: "form-select" %>
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<!-- Frequency Filter -->
|
|
178
|
+
<div class="col-md-2">
|
|
179
|
+
<%= select_tag :frequency, options_for_select([
|
|
180
|
+
['All Frequencies', ''],
|
|
181
|
+
['Once', 'once'],
|
|
182
|
+
['2-9 Times', 'few'],
|
|
183
|
+
['10-99 Times', 'frequent'],
|
|
184
|
+
['100+ Times', 'very_frequent'],
|
|
185
|
+
['Recurring (Active)', 'recurring']
|
|
186
|
+
], params[:frequency]), class: "form-select" %>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<!-- Phase 3: Workflow Filters -->
|
|
190
|
+
<div class="col-md-2">
|
|
191
|
+
<%= select_tag :status, options_for_select([
|
|
192
|
+
['All Statuses', ''],
|
|
193
|
+
['🆕 New', 'new'],
|
|
194
|
+
['🔵 In Progress', 'in_progress'],
|
|
195
|
+
['🟡 Investigating', 'investigating'],
|
|
196
|
+
['✅ Resolved', 'resolved'],
|
|
197
|
+
['⚫ Won\'t Fix', 'wont_fix']
|
|
198
|
+
], params[:status]), class: "form-select" %>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div class="col-md-2">
|
|
202
|
+
<%= select_tag :assigned_to, options_for_select([
|
|
203
|
+
['All Assignments', ''],
|
|
204
|
+
['Unassigned', '__unassigned__'],
|
|
205
|
+
['Assigned', '__assigned__']
|
|
206
|
+
], params[:assigned_to]), class: "form-select" %>
|
|
207
|
+
</div>
|
|
208
|
+
|
|
209
|
+
<div class="col-md-2">
|
|
210
|
+
<%= select_tag :priority_level, options_for_select([
|
|
211
|
+
['All Priorities', ''],
|
|
212
|
+
['🔴 Critical (P3)', 3],
|
|
213
|
+
['🟠 High (P2)', 2],
|
|
214
|
+
['🟡 Medium (P1)', 1],
|
|
215
|
+
['⚪ Low (P0)', 0]
|
|
216
|
+
], params[:priority_level]), class: "form-select" %>
|
|
217
|
+
</div>
|
|
218
|
+
|
|
152
219
|
<div class="col-auto">
|
|
153
220
|
<div class="form-check mt-2">
|
|
154
|
-
|
|
221
|
+
<%
|
|
222
|
+
# Determine checkbox state based on params
|
|
223
|
+
# If unresolved param is missing or nil, default to checked (true)
|
|
224
|
+
# If unresolved param is explicitly "false" or "0", uncheck it
|
|
225
|
+
is_checked = if params[:unresolved].nil?
|
|
226
|
+
true # Default to checked when first loading
|
|
227
|
+
else
|
|
228
|
+
params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
229
|
+
end
|
|
230
|
+
%>
|
|
231
|
+
<%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox" %>
|
|
155
232
|
<%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
|
|
156
|
-
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
|
|
236
|
+
<div class="col-auto">
|
|
237
|
+
<div class="form-check mt-2">
|
|
238
|
+
<%= check_box_tag :hide_snoozed, "1", params[:hide_snoozed] == "1", class: "form-check-input" %>
|
|
239
|
+
<%= label_tag :hide_snoozed, "Hide snoozed", class: "form-check-label" %>
|
|
157
240
|
</div>
|
|
158
241
|
</div>
|
|
159
242
|
|
|
@@ -207,13 +290,13 @@
|
|
|
207
290
|
<th style="width: 40px;">
|
|
208
291
|
<input type="checkbox" id="select-all" class="form-check-input">
|
|
209
292
|
</th>
|
|
210
|
-
<th
|
|
211
|
-
<th
|
|
293
|
+
<th><%= sortable_header("Severity", "severity") %></th>
|
|
294
|
+
<th><%= sortable_header("Error Type", "error_type") %></th>
|
|
212
295
|
<th>Message</th>
|
|
213
|
-
<th
|
|
214
|
-
<th
|
|
296
|
+
<th><%= sortable_header("Occurrences", "occurrence_count") %></th>
|
|
297
|
+
<th><%= sortable_header("First / Last Seen", "last_seen_at") %></th>
|
|
215
298
|
<% if @platforms.size > 1 %>
|
|
216
|
-
<th
|
|
299
|
+
<th><%= sortable_header("Platform", "platform") %></th>
|
|
217
300
|
<% end %>
|
|
218
301
|
<th>Status</th>
|
|
219
302
|
<th></th>
|
|
@@ -408,4 +491,37 @@
|
|
|
408
491
|
'? - Show this help');
|
|
409
492
|
}
|
|
410
493
|
});
|
|
494
|
+
|
|
495
|
+
// Preserve scroll position on page load
|
|
496
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
497
|
+
const scrollPos = sessionStorage.getItem('scrollPos');
|
|
498
|
+
if (scrollPos) {
|
|
499
|
+
window.scrollTo(0, parseInt(scrollPos));
|
|
500
|
+
sessionStorage.removeItem('scrollPos');
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// Handle filter form submission
|
|
505
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
506
|
+
const filterForm = document.querySelector('form[action="<%= errors_path %>"]');
|
|
507
|
+
if (filterForm) {
|
|
508
|
+
filterForm.addEventListener('submit', function(e) {
|
|
509
|
+
// Save scroll position before form submission
|
|
510
|
+
sessionStorage.setItem('scrollPos', window.pageYOffset);
|
|
511
|
+
|
|
512
|
+
// Handle unchecked checkbox - when unchecked, send "0" explicitly
|
|
513
|
+
const unresolvedCheckbox = document.getElementById('unresolved_checkbox');
|
|
514
|
+
if (unresolvedCheckbox && !unresolvedCheckbox.checked) {
|
|
515
|
+
// Create hidden input to send "0" when unchecked
|
|
516
|
+
const hiddenInput = document.createElement('input');
|
|
517
|
+
hiddenInput.type = 'hidden';
|
|
518
|
+
hiddenInput.name = 'unresolved';
|
|
519
|
+
hiddenInput.value = '0';
|
|
520
|
+
filterForm.appendChild(hiddenInput);
|
|
521
|
+
// Remove the checkbox from form submission to avoid sending empty value
|
|
522
|
+
unresolvedCheckbox.disabled = true;
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
});
|
|
411
527
|
</script>
|