rails_error_dashboard 0.1.3 → 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 +26 -0
- data/app/assets/stylesheets/rails_error_dashboard/application.css +15 -926
- data/app/controllers/rails_error_dashboard/errors_controller.rb +42 -1
- data/app/helpers/rails_error_dashboard/application_helper.rb +38 -0
- data/app/models/rails_error_dashboard/error_log.rb +14 -0
- data/app/views/layouts/rails_error_dashboard/application.html.erb +39 -1
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +301 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +44 -7
- data/config/routes.rb +4 -1
- data/lib/generators/rails_error_dashboard/uninstall/uninstall_generator.rb +317 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +77 -5
- 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/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +3 -0
- data/lib/tasks/rails_error_dashboard_tasks.rake +85 -0
- metadata +20 -2
|
@@ -146,6 +146,22 @@ module RailsErrorDashboard
|
|
|
146
146
|
@resolution_rate = analytics[:resolution_rate]
|
|
147
147
|
@mobile_errors = analytics[:mobile_errors]
|
|
148
148
|
@api_errors = analytics[:api_errors]
|
|
149
|
+
|
|
150
|
+
# Get recurring issues data
|
|
151
|
+
recurring = Queries::RecurringIssues.call(days)
|
|
152
|
+
@recurring_data = recurring
|
|
153
|
+
|
|
154
|
+
# Get release correlation data
|
|
155
|
+
correlation = Queries::ErrorCorrelation.new(days: days)
|
|
156
|
+
@errors_by_version = correlation.errors_by_version
|
|
157
|
+
@problematic_releases = correlation.problematic_releases
|
|
158
|
+
@release_comparison = calculate_release_comparison
|
|
159
|
+
|
|
160
|
+
# Get MTTR data
|
|
161
|
+
mttr_data = Queries::MttrStats.call(days)
|
|
162
|
+
@mttr_stats = mttr_data
|
|
163
|
+
@overall_mttr = mttr_data[:overall_mttr]
|
|
164
|
+
@mttr_by_platform = mttr_data[:mttr_by_platform]
|
|
149
165
|
end
|
|
150
166
|
|
|
151
167
|
def platform_comparison
|
|
@@ -221,6 +237,26 @@ module RailsErrorDashboard
|
|
|
221
237
|
|
|
222
238
|
private
|
|
223
239
|
|
|
240
|
+
def calculate_release_comparison
|
|
241
|
+
return {} if @errors_by_version.empty? || @errors_by_version.count < 2
|
|
242
|
+
|
|
243
|
+
versions_sorted = @errors_by_version.sort_by { |_, data| data[:last_seen] || Time.at(0) }.reverse
|
|
244
|
+
latest = versions_sorted.first
|
|
245
|
+
previous = versions_sorted.second
|
|
246
|
+
|
|
247
|
+
return {} if latest.nil? || previous.nil?
|
|
248
|
+
|
|
249
|
+
{
|
|
250
|
+
latest_version: latest[0],
|
|
251
|
+
latest_count: latest[1][:count],
|
|
252
|
+
latest_critical: latest[1][:critical_count],
|
|
253
|
+
previous_version: previous[0],
|
|
254
|
+
previous_count: previous[1][:count],
|
|
255
|
+
previous_critical: previous[1][:critical_count],
|
|
256
|
+
change_percentage: previous[1][:count] > 0 ? ((latest[1][:count] - previous[1][:count]).to_f / previous[1][:count] * 100).round(1) : 0.0
|
|
257
|
+
}
|
|
258
|
+
end
|
|
259
|
+
|
|
224
260
|
def filter_params
|
|
225
261
|
{
|
|
226
262
|
error_type: params[:error_type],
|
|
@@ -228,11 +264,16 @@ module RailsErrorDashboard
|
|
|
228
264
|
platform: params[:platform],
|
|
229
265
|
search: params[:search],
|
|
230
266
|
severity: params[:severity],
|
|
267
|
+
timeframe: params[:timeframe],
|
|
268
|
+
frequency: params[:frequency],
|
|
231
269
|
# Phase 3: Workflow filter params
|
|
232
270
|
status: params[:status],
|
|
233
271
|
assigned_to: params[:assigned_to],
|
|
234
272
|
priority_level: params[:priority_level],
|
|
235
|
-
hide_snoozed: params[:hide_snoozed]
|
|
273
|
+
hide_snoozed: params[:hide_snoozed],
|
|
274
|
+
# Sorting params
|
|
275
|
+
sort_by: params[:sort_by],
|
|
276
|
+
sort_direction: params[:sort_direction]
|
|
236
277
|
}
|
|
237
278
|
end
|
|
238
279
|
|
|
@@ -55,5 +55,43 @@ module RailsErrorDashboard
|
|
|
55
55
|
"var(--text-color)"
|
|
56
56
|
end
|
|
57
57
|
end
|
|
58
|
+
|
|
59
|
+
# Returns the current user name for filtering "My Errors"
|
|
60
|
+
# Uses configured dashboard username or system username
|
|
61
|
+
# @return [String] Current user identifier
|
|
62
|
+
def current_user_name
|
|
63
|
+
RailsErrorDashboard.configuration.dashboard_username || ENV["USER"] || "unknown"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Generates a sortable column header link
|
|
67
|
+
# @param label [String] The column label to display
|
|
68
|
+
# @param column [String] The column name to sort by
|
|
69
|
+
# @return [String] HTML safe link with sort indicator
|
|
70
|
+
def sortable_header(label, column)
|
|
71
|
+
current_sort = params[:sort_by]
|
|
72
|
+
current_direction = params[:sort_direction] || "desc"
|
|
73
|
+
|
|
74
|
+
# Determine new direction: if clicking same column, toggle; otherwise default to desc
|
|
75
|
+
new_direction = if current_sort == column
|
|
76
|
+
current_direction == "asc" ? "desc" : "asc"
|
|
77
|
+
else
|
|
78
|
+
"desc"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Choose icon based on current state
|
|
82
|
+
icon = if current_sort == column
|
|
83
|
+
current_direction == "asc" ? "▲" : "▼"
|
|
84
|
+
else
|
|
85
|
+
"⇅" # Unsorted indicator
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Preserve existing filter params while adding sort params
|
|
89
|
+
link_params = params.permit!.to_h.merge(sort_by: column, sort_direction: new_direction)
|
|
90
|
+
|
|
91
|
+
link_to errors_path(link_params), class: "text-decoration-none" do
|
|
92
|
+
content_tag(:span, "#{label} ", class: current_sort == column ? "fw-bold" : "") +
|
|
93
|
+
content_tag(:span, icon, class: "text-muted small")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
58
96
|
end
|
|
59
97
|
end
|
|
@@ -133,6 +133,11 @@ module RailsErrorDashboard
|
|
|
133
133
|
SystemStackError
|
|
134
134
|
SignalException
|
|
135
135
|
ActiveRecord::StatementInvalid
|
|
136
|
+
LoadError
|
|
137
|
+
SyntaxError
|
|
138
|
+
ActiveRecord::ConnectionNotEstablished
|
|
139
|
+
Redis::ConnectionError
|
|
140
|
+
OpenSSL::SSL::SSLError
|
|
136
141
|
].freeze
|
|
137
142
|
|
|
138
143
|
HIGH_SEVERITY_ERROR_TYPES = %w[
|
|
@@ -141,6 +146,11 @@ module RailsErrorDashboard
|
|
|
141
146
|
TypeError
|
|
142
147
|
NoMethodError
|
|
143
148
|
NameError
|
|
149
|
+
ZeroDivisionError
|
|
150
|
+
FloatDomainError
|
|
151
|
+
IndexError
|
|
152
|
+
KeyError
|
|
153
|
+
RangeError
|
|
144
154
|
].freeze
|
|
145
155
|
|
|
146
156
|
MEDIUM_SEVERITY_ERROR_TYPES = %w[
|
|
@@ -148,6 +158,10 @@ module RailsErrorDashboard
|
|
|
148
158
|
Timeout::Error
|
|
149
159
|
Net::ReadTimeout
|
|
150
160
|
Net::OpenTimeout
|
|
161
|
+
ActiveRecord::RecordNotUnique
|
|
162
|
+
JSON::ParserError
|
|
163
|
+
CSV::MalformedCSVError
|
|
164
|
+
Errno::ECONNREFUSED
|
|
151
165
|
].freeze
|
|
152
166
|
|
|
153
167
|
# Find existing error by hash or create new one
|
|
@@ -1,16 +1,54 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
|
-
<title>Rails
|
|
4
|
+
<title>Rails Error Dashboard</title>
|
|
5
5
|
<%= csrf_meta_tags %>
|
|
6
6
|
<%= csp_meta_tag %>
|
|
7
7
|
|
|
8
8
|
<%= yield :head %>
|
|
9
9
|
|
|
10
10
|
<%= stylesheet_link_tag "rails_error_dashboard/application", media: "all" %>
|
|
11
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
12
|
+
<script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script>
|
|
11
13
|
</head>
|
|
12
14
|
<body>
|
|
13
15
|
|
|
16
|
+
<!-- Navigation -->
|
|
17
|
+
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
|
18
|
+
<div class="container-fluid">
|
|
19
|
+
<%= link_to "Error Dashboard", root_path, class: "navbar-brand" %>
|
|
20
|
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
|
|
21
|
+
<span class="navbar-toggler-icon"></span>
|
|
22
|
+
</button>
|
|
23
|
+
<div class="collapse navbar-collapse" id="navbarNav">
|
|
24
|
+
<ul class="navbar-nav me-auto">
|
|
25
|
+
<li class="nav-item">
|
|
26
|
+
<%= link_to overview_path, class: "nav-link #{controller_name == 'errors' && action_name == 'overview' ? 'active' : ''}" do %>
|
|
27
|
+
<i class="bi bi-speedometer2"></i> Dashboard
|
|
28
|
+
<% end %>
|
|
29
|
+
</li>
|
|
30
|
+
<li class="nav-item">
|
|
31
|
+
<%= link_to errors_path, class: "nav-link #{controller_name == 'errors' && action_name == 'index' ? 'active' : ''}" do %>
|
|
32
|
+
<i class="bi bi-list-ul"></i> Errors
|
|
33
|
+
<% end %>
|
|
34
|
+
</li>
|
|
35
|
+
<li class="nav-item">
|
|
36
|
+
<%= link_to analytics_errors_path, class: "nav-link #{controller_name == 'errors' && action_name == 'analytics' ? 'active' : ''}" do %>
|
|
37
|
+
<i class="bi bi-graph-up"></i> Analytics
|
|
38
|
+
<% end %>
|
|
39
|
+
</li>
|
|
40
|
+
</ul>
|
|
41
|
+
<ul class="navbar-nav">
|
|
42
|
+
<li class="nav-item">
|
|
43
|
+
<a class="nav-link" href="#" id="theme-toggle">
|
|
44
|
+
<i class="bi bi-moon-stars"></i>
|
|
45
|
+
</a>
|
|
46
|
+
</li>
|
|
47
|
+
</ul>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</nav>
|
|
51
|
+
|
|
14
52
|
<%= yield %>
|
|
15
53
|
|
|
16
54
|
</body>
|
|
@@ -330,4 +330,305 @@
|
|
|
330
330
|
</div>
|
|
331
331
|
</div>
|
|
332
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 %>
|
|
333
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">
|
|
@@ -124,6 +126,16 @@
|
|
|
124
126
|
<h5 class="mb-0">Filters & Search</h5>
|
|
125
127
|
</div>
|
|
126
128
|
<div class="card-body">
|
|
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
|
+
|
|
127
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" %>
|
|
@@ -149,6 +161,31 @@
|
|
|
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
|
+
|
|
152
189
|
<!-- Phase 3: Workflow Filters -->
|
|
153
190
|
<div class="col-md-2">
|
|
154
191
|
<%= select_tag :status, options_for_select([
|
|
@@ -253,13 +290,13 @@
|
|
|
253
290
|
<th style="width: 40px;">
|
|
254
291
|
<input type="checkbox" id="select-all" class="form-check-input">
|
|
255
292
|
</th>
|
|
256
|
-
<th
|
|
257
|
-
<th
|
|
293
|
+
<th><%= sortable_header("Severity", "severity") %></th>
|
|
294
|
+
<th><%= sortable_header("Error Type", "error_type") %></th>
|
|
258
295
|
<th>Message</th>
|
|
259
|
-
<th
|
|
260
|
-
<th
|
|
296
|
+
<th><%= sortable_header("Occurrences", "occurrence_count") %></th>
|
|
297
|
+
<th><%= sortable_header("First / Last Seen", "last_seen_at") %></th>
|
|
261
298
|
<% if @platforms.size > 1 %>
|
|
262
|
-
<th
|
|
299
|
+
<th><%= sortable_header("Platform", "platform") %></th>
|
|
263
300
|
<% end %>
|
|
264
301
|
<th>Status</th>
|
|
265
302
|
<th></th>
|
data/config/routes.rb
CHANGED