rails_error_dashboard 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +66 -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 +926 -15
- 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 +94 -1
- data/app/helpers/rails_error_dashboard/application_helper.rb +42 -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 +145 -0
- 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 +138 -22
- data/app/views/rails_error_dashboard/errors/index.html.erb +83 -4
- 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 +7 -0
- 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/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 +61 -6
- 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 +2 -0
- metadata +18 -2
- data/lib/tasks/rails_error_dashboard_tasks.rake +0 -4
|
@@ -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>
|
|
@@ -119,12 +119,12 @@
|
|
|
119
119
|
<% end %>
|
|
120
120
|
|
|
121
121
|
<!-- Filters -->
|
|
122
|
-
<div class="card mb-4">
|
|
122
|
+
<div class="card mb-4" id="filters-section">
|
|
123
123
|
<div class="card-header bg-white">
|
|
124
124
|
<h5 class="mb-0">Filters & Search</h5>
|
|
125
125
|
</div>
|
|
126
126
|
<div class="card-body">
|
|
127
|
-
<%= form_with url: errors_path, method: :get, class: "row g-3" do %>
|
|
127
|
+
<%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>
|
|
128
128
|
<div class="col-md-4">
|
|
129
129
|
<%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
|
|
130
130
|
</div>
|
|
@@ -149,11 +149,57 @@
|
|
|
149
149
|
], params[:severity]), class: "form-select" %>
|
|
150
150
|
</div>
|
|
151
151
|
|
|
152
|
+
<!-- Phase 3: Workflow Filters -->
|
|
153
|
+
<div class="col-md-2">
|
|
154
|
+
<%= select_tag :status, options_for_select([
|
|
155
|
+
['All Statuses', ''],
|
|
156
|
+
['🆕 New', 'new'],
|
|
157
|
+
['🔵 In Progress', 'in_progress'],
|
|
158
|
+
['🟡 Investigating', 'investigating'],
|
|
159
|
+
['✅ Resolved', 'resolved'],
|
|
160
|
+
['⚫ Won\'t Fix', 'wont_fix']
|
|
161
|
+
], params[:status]), class: "form-select" %>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="col-md-2">
|
|
165
|
+
<%= select_tag :assigned_to, options_for_select([
|
|
166
|
+
['All Assignments', ''],
|
|
167
|
+
['Unassigned', '__unassigned__'],
|
|
168
|
+
['Assigned', '__assigned__']
|
|
169
|
+
], params[:assigned_to]), class: "form-select" %>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div class="col-md-2">
|
|
173
|
+
<%= select_tag :priority_level, options_for_select([
|
|
174
|
+
['All Priorities', ''],
|
|
175
|
+
['🔴 Critical (P3)', 3],
|
|
176
|
+
['🟠 High (P2)', 2],
|
|
177
|
+
['🟡 Medium (P1)', 1],
|
|
178
|
+
['⚪ Low (P0)', 0]
|
|
179
|
+
], params[:priority_level]), class: "form-select" %>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
152
182
|
<div class="col-auto">
|
|
153
183
|
<div class="form-check mt-2">
|
|
154
|
-
|
|
184
|
+
<%
|
|
185
|
+
# Determine checkbox state based on params
|
|
186
|
+
# If unresolved param is missing or nil, default to checked (true)
|
|
187
|
+
# If unresolved param is explicitly "false" or "0", uncheck it
|
|
188
|
+
is_checked = if params[:unresolved].nil?
|
|
189
|
+
true # Default to checked when first loading
|
|
190
|
+
else
|
|
191
|
+
params[:unresolved] != "false" && params[:unresolved] != "0"
|
|
192
|
+
end
|
|
193
|
+
%>
|
|
194
|
+
<%= check_box_tag :unresolved, "1", is_checked, class: "form-check-input", id: "unresolved_checkbox" %>
|
|
155
195
|
<%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
|
|
156
|
-
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<div class="col-auto">
|
|
200
|
+
<div class="form-check mt-2">
|
|
201
|
+
<%= check_box_tag :hide_snoozed, "1", params[:hide_snoozed] == "1", class: "form-check-input" %>
|
|
202
|
+
<%= label_tag :hide_snoozed, "Hide snoozed", class: "form-check-label" %>
|
|
157
203
|
</div>
|
|
158
204
|
</div>
|
|
159
205
|
|
|
@@ -408,4 +454,37 @@
|
|
|
408
454
|
'? - Show this help');
|
|
409
455
|
}
|
|
410
456
|
});
|
|
457
|
+
|
|
458
|
+
// Preserve scroll position on page load
|
|
459
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
460
|
+
const scrollPos = sessionStorage.getItem('scrollPos');
|
|
461
|
+
if (scrollPos) {
|
|
462
|
+
window.scrollTo(0, parseInt(scrollPos));
|
|
463
|
+
sessionStorage.removeItem('scrollPos');
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// Handle filter form submission
|
|
468
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
469
|
+
const filterForm = document.querySelector('form[action="<%= errors_path %>"]');
|
|
470
|
+
if (filterForm) {
|
|
471
|
+
filterForm.addEventListener('submit', function(e) {
|
|
472
|
+
// Save scroll position before form submission
|
|
473
|
+
sessionStorage.setItem('scrollPos', window.pageYOffset);
|
|
474
|
+
|
|
475
|
+
// Handle unchecked checkbox - when unchecked, send "0" explicitly
|
|
476
|
+
const unresolvedCheckbox = document.getElementById('unresolved_checkbox');
|
|
477
|
+
if (unresolvedCheckbox && !unresolvedCheckbox.checked) {
|
|
478
|
+
// Create hidden input to send "0" when unchecked
|
|
479
|
+
const hiddenInput = document.createElement('input');
|
|
480
|
+
hiddenInput.type = 'hidden';
|
|
481
|
+
hiddenInput.name = 'unresolved';
|
|
482
|
+
hiddenInput.value = '0';
|
|
483
|
+
filterForm.appendChild(hiddenInput);
|
|
484
|
+
// Remove the checkbox from form submission to avoid sending empty value
|
|
485
|
+
unresolvedCheckbox.disabled = true;
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
});
|
|
411
490
|
</script>
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
<div class="container-fluid py-4">
|
|
2
|
+
<!-- Page Header -->
|
|
3
|
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
4
|
+
<h1 class="h3 mb-0">
|
|
5
|
+
<i class="bi bi-speedometer2 me-2"></i>
|
|
6
|
+
Dashboard Overview
|
|
7
|
+
</h1>
|
|
8
|
+
<div class="text-muted">
|
|
9
|
+
<small>
|
|
10
|
+
Last updated: <%= Time.current.strftime("%B %d, %Y %I:%M %p") %>
|
|
11
|
+
</small>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
<!-- Key Metrics Cards (Mobile-first: stack on mobile, 3 columns on desktop) -->
|
|
16
|
+
<div class="row g-3 mb-4">
|
|
17
|
+
<!-- Error Rate Card -->
|
|
18
|
+
<div class="col-12 col-md-6 col-lg-4">
|
|
19
|
+
<div class="card stat-card h-100 <%= error_rate_border_class(@stats[:error_rate]) %>">
|
|
20
|
+
<div class="card-body text-center">
|
|
21
|
+
<div class="stat-label mb-2">ERROR RATE</div>
|
|
22
|
+
<div class="stat-value <%= error_rate_text_class(@stats[:error_rate]) %>">
|
|
23
|
+
<%= @stats[:error_rate] %>%
|
|
24
|
+
</div>
|
|
25
|
+
<small class="text-muted">Errors per hour today</small>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<!-- Affected Users Card -->
|
|
31
|
+
<div class="col-12 col-md-6 col-lg-4">
|
|
32
|
+
<div class="card stat-card h-100">
|
|
33
|
+
<div class="card-body text-center">
|
|
34
|
+
<div class="stat-label mb-2">AFFECTED USERS</div>
|
|
35
|
+
<div class="stat-value text-primary">
|
|
36
|
+
<%= @stats[:affected_users_today] %>
|
|
37
|
+
</div>
|
|
38
|
+
<small class="<%= trend_color_class(@stats[:affected_users_change]) %>">
|
|
39
|
+
<%= trend_arrow(@stats[:affected_users_change]) %>
|
|
40
|
+
<%= @stats[:affected_users_change].abs %>
|
|
41
|
+
from yesterday
|
|
42
|
+
</small>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<!-- Trend Card -->
|
|
48
|
+
<div class="col-12 col-lg-4">
|
|
49
|
+
<div class="card stat-card h-100">
|
|
50
|
+
<div class="card-body text-center">
|
|
51
|
+
<div class="stat-label mb-2">ERROR TREND</div>
|
|
52
|
+
<div class="stat-value <%= trend_color_class(@stats[:trend_percentage]) %>">
|
|
53
|
+
<%= trend_arrow(@stats[:trend_percentage]) %>
|
|
54
|
+
<%= @stats[:trend_percentage].abs %>%
|
|
55
|
+
</div>
|
|
56
|
+
<small class="text-muted">
|
|
57
|
+
<%= trend_text(@stats[:trend_direction]) %>
|
|
58
|
+
</small>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Top 5 Errors by Impact (Mobile: stack, Desktop: 2 columns) -->
|
|
65
|
+
<% if @stats[:top_errors_by_impact].any? %>
|
|
66
|
+
<div class="card mb-4">
|
|
67
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
68
|
+
<h5 class="mb-0">
|
|
69
|
+
<i class="bi bi-exclamation-triangle me-2"></i>
|
|
70
|
+
Top 5 Errors by Impact
|
|
71
|
+
</h5>
|
|
72
|
+
<%= link_to "View All Errors →", errors_path, class: "btn btn-sm btn-outline-primary" %>
|
|
73
|
+
</div>
|
|
74
|
+
<div class="card-body">
|
|
75
|
+
<div class="row g-3">
|
|
76
|
+
<% @stats[:top_errors_by_impact].each do |error| %>
|
|
77
|
+
<div class="col-12 col-lg-6">
|
|
78
|
+
<%= link_to error_path(error[:id]), class: "text-decoration-none" do %>
|
|
79
|
+
<div class="card h-100 border-start border-<%= severity_color(error[:severity]) %> border-3">
|
|
80
|
+
<div class="card-body">
|
|
81
|
+
<div class="d-flex justify-content-between align-items-start mb-2">
|
|
82
|
+
<div class="flex-grow-1">
|
|
83
|
+
<h6 class="mb-1">
|
|
84
|
+
<span class="badge bg-<%= severity_color(error[:severity]) %> me-2">
|
|
85
|
+
<%= severity_icon(error[:severity]) %> <%= error[:severity].to_s.titleize %>
|
|
86
|
+
</span>
|
|
87
|
+
<code class="small"><%= error[:error_type] %></code>
|
|
88
|
+
</h6>
|
|
89
|
+
<p class="small text-muted mb-2"><%= error[:message] %></p>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<div class="d-flex justify-content-between align-items-center small">
|
|
93
|
+
<span class="text-muted">
|
|
94
|
+
<i class="bi bi-people-fill me-1"></i>
|
|
95
|
+
<%= error[:affected_users] %> user<%= error[:affected_users] != 1 ? 's' : '' %>
|
|
96
|
+
</span>
|
|
97
|
+
<span class="text-muted">
|
|
98
|
+
<i class="bi bi-arrow-repeat me-1"></i>
|
|
99
|
+
<%= error[:occurrence_count] %> occurrence<%= error[:occurrence_count] != 1 ? 's' : '' %>
|
|
100
|
+
</span>
|
|
101
|
+
<span class="fw-bold text-danger">
|
|
102
|
+
Impact: <%= error[:impact_score] %>
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
<% end %>
|
|
108
|
+
</div>
|
|
109
|
+
<% end %>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<% end %>
|
|
114
|
+
|
|
115
|
+
<!-- Platform Health Summary (Mobile: stack, Desktop: 3 columns) -->
|
|
116
|
+
<% if @platform_health.any? %>
|
|
117
|
+
<div class="card mb-4">
|
|
118
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
119
|
+
<h5 class="mb-0">
|
|
120
|
+
<i class="bi bi-phone me-2"></i>
|
|
121
|
+
Platform Health
|
|
122
|
+
</h5>
|
|
123
|
+
<% if RailsErrorDashboard.configuration.enable_platform_comparison %>
|
|
124
|
+
<%= link_to "Full Comparison →", platform_comparison_errors_path, class: "btn btn-sm btn-outline-primary" %>
|
|
125
|
+
<% end %>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="card-body">
|
|
128
|
+
<div class="row g-3">
|
|
129
|
+
<% @platform_health.each do |platform, health| %>
|
|
130
|
+
<div class="col-12 col-md-4">
|
|
131
|
+
<div class="card h-100 border-<%= health_status_color(health[:health_status]) %>">
|
|
132
|
+
<div class="card-body">
|
|
133
|
+
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
134
|
+
<h6 class="text-capitalize mb-0"><%= platform %></h6>
|
|
135
|
+
<span class="badge bg-<%= health_status_color(health[:health_status]) %>">
|
|
136
|
+
<%= health_status_text(health[:health_status]) %>
|
|
137
|
+
</span>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
<div class="mb-2">
|
|
141
|
+
<small class="text-muted">Stability Score</small>
|
|
142
|
+
<div class="d-flex align-items-center">
|
|
143
|
+
<h4 class="mb-0 me-2"><%= health[:stability_score] %>/100</h4>
|
|
144
|
+
<div class="progress flex-grow-1" style="height: 8px;">
|
|
145
|
+
<div class="progress-bar bg-<%= health_status_color(health[:health_status]) %>"
|
|
146
|
+
role="progressbar"
|
|
147
|
+
style="width: <%= health[:stability_score] %>%">
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="small">
|
|
154
|
+
<div class="d-flex justify-content-between mb-1">
|
|
155
|
+
<span class="text-muted">Total Errors:</span>
|
|
156
|
+
<strong><%= health[:total_errors] %></strong>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="d-flex justify-content-between mb-1">
|
|
159
|
+
<span class="text-muted">Critical:</span>
|
|
160
|
+
<strong class="text-danger"><%= health[:critical_errors] %></strong>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="d-flex justify-content-between">
|
|
163
|
+
<span class="text-muted">Unresolved:</span>
|
|
164
|
+
<strong><%= health[:unresolved_errors] %></strong>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<% end %>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<% end %>
|
|
175
|
+
|
|
176
|
+
<!-- Critical Alerts (Last hour) -->
|
|
177
|
+
<% if @critical_alerts.any? %>
|
|
178
|
+
<div class="alert alert-danger border-danger mb-4" role="alert">
|
|
179
|
+
<div class="d-flex align-items-center mb-2">
|
|
180
|
+
<i class="bi bi-exclamation-triangle-fill fs-3 me-3"></i>
|
|
181
|
+
<div>
|
|
182
|
+
<h5 class="alert-heading mb-0">
|
|
183
|
+
🚨 <%= @critical_alerts.count %> Critical/High Error<%= @critical_alerts.count != 1 ? 's' : '' %> in Last Hour
|
|
184
|
+
</h5>
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="list-group list-group-flush">
|
|
188
|
+
<% @critical_alerts.first(5).each do |error| %>
|
|
189
|
+
<%= link_to error_path(error), class: "list-group-item list-group-item-action" do %>
|
|
190
|
+
<div class="d-flex justify-content-between align-items-start">
|
|
191
|
+
<div class="flex-grow-1">
|
|
192
|
+
<div class="d-flex align-items-center mb-1">
|
|
193
|
+
<span class="badge bg-<%= severity_color(error.severity) %> me-2">
|
|
194
|
+
<%= severity_icon(error.severity) %>
|
|
195
|
+
</span>
|
|
196
|
+
<code class="small"><%= error.error_type %></code>
|
|
197
|
+
</div>
|
|
198
|
+
<p class="mb-1 small"><%= error.message&.truncate(100) %></p>
|
|
199
|
+
<small class="text-muted">
|
|
200
|
+
<i class="bi bi-clock me-1"></i>
|
|
201
|
+
<%= time_ago_in_words(error.occurred_at) %> ago
|
|
202
|
+
•
|
|
203
|
+
<i class="bi bi-arrow-repeat me-1"></i>
|
|
204
|
+
<%= error.occurrence_count %> occurrence<%= error.occurrence_count != 1 ? 's' : '' %>
|
|
205
|
+
</small>
|
|
206
|
+
</div>
|
|
207
|
+
<i class="bi bi-chevron-right"></i>
|
|
208
|
+
</div>
|
|
209
|
+
<% end %>
|
|
210
|
+
<% end %>
|
|
211
|
+
</div>
|
|
212
|
+
<% if @critical_alerts.count > 5 %>
|
|
213
|
+
<div class="mt-2">
|
|
214
|
+
<%= link_to "View all #{@critical_alerts.count} critical errors →",
|
|
215
|
+
errors_path(severity: 'critical'),
|
|
216
|
+
class: "btn btn-sm btn-outline-danger" %>
|
|
217
|
+
</div>
|
|
218
|
+
<% end %>
|
|
219
|
+
</div>
|
|
220
|
+
<% end %>
|
|
221
|
+
|
|
222
|
+
<!-- Quick Actions -->
|
|
223
|
+
<div class="row g-3">
|
|
224
|
+
<div class="col-12 col-md-6 col-lg-3">
|
|
225
|
+
<%= link_to errors_path, class: "btn btn-lg btn-outline-primary w-100" do %>
|
|
226
|
+
<i class="bi bi-list-ul d-block fs-3 mb-2"></i>
|
|
227
|
+
View All Errors
|
|
228
|
+
<% end %>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="col-12 col-md-6 col-lg-3">
|
|
231
|
+
<%= link_to analytics_errors_path, class: "btn btn-lg btn-outline-primary w-100" do %>
|
|
232
|
+
<i class="bi bi-graph-up d-block fs-3 mb-2"></i>
|
|
233
|
+
Analytics
|
|
234
|
+
<% end %>
|
|
235
|
+
</div>
|
|
236
|
+
<% if RailsErrorDashboard.configuration.enable_platform_comparison %>
|
|
237
|
+
<div class="col-12 col-md-6 col-lg-3">
|
|
238
|
+
<%= link_to platform_comparison_errors_path, class: "btn btn-lg btn-outline-primary w-100" do %>
|
|
239
|
+
<i class="bi bi-phone d-block fs-3 mb-2"></i>
|
|
240
|
+
Platform Health
|
|
241
|
+
<% end %>
|
|
242
|
+
</div>
|
|
243
|
+
<% end %>
|
|
244
|
+
<% if RailsErrorDashboard.configuration.enable_error_correlation %>
|
|
245
|
+
<div class="col-12 col-md-6 col-lg-3">
|
|
246
|
+
<%= link_to correlation_errors_path, class: "btn btn-lg btn-outline-primary w-100" do %>
|
|
247
|
+
<i class="bi bi-diagram-3 d-block fs-3 mb-2"></i>
|
|
248
|
+
Correlation
|
|
249
|
+
<% end %>
|
|
250
|
+
</div>
|
|
251
|
+
<% end %>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
@@ -290,8 +290,16 @@
|
|
|
290
290
|
datasets: [{
|
|
291
291
|
label: 'Total Errors',
|
|
292
292
|
data: <%= raw @error_rate_by_platform.values.to_json %>,
|
|
293
|
-
backgroundColor:
|
|
294
|
-
|
|
293
|
+
backgroundColor: (function() {
|
|
294
|
+
const isDark = document.body.classList.contains('dark-mode');
|
|
295
|
+
const config = getCatppuccinChartConfig(isDark);
|
|
296
|
+
return config.colors.blue.replace('rgb', 'rgba').replace(')', ', 0.5)');
|
|
297
|
+
})(),
|
|
298
|
+
borderColor: (function() {
|
|
299
|
+
const isDark = document.body.classList.contains('dark-mode');
|
|
300
|
+
const config = getCatppuccinChartConfig(isDark);
|
|
301
|
+
return config.colors.blue;
|
|
302
|
+
})(),
|
|
295
303
|
borderWidth: 1
|
|
296
304
|
}]
|
|
297
305
|
},
|
|
@@ -311,29 +319,24 @@
|
|
|
311
319
|
// Daily Trend Chart
|
|
312
320
|
const dailyTrendCtx = document.getElementById('dailyTrendChart');
|
|
313
321
|
if (dailyTrendCtx) {
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
android: 'rgba(75, 192, 192, 1)',
|
|
317
|
-
api: 'rgba(255, 206, 86, 1)',
|
|
318
|
-
web: 'rgba(153, 102, 255, 1)',
|
|
319
|
-
unknown: 'rgba(201, 203, 207, 1)'
|
|
320
|
-
};
|
|
322
|
+
// Use Catppuccin colors from global theme config
|
|
323
|
+
const isDark = document.body.classList.contains('dark-mode');
|
|
321
324
|
|
|
322
325
|
const datasets = <%= raw @daily_trends.map { |platform, data|
|
|
323
326
|
{
|
|
324
327
|
label: platform.to_s.capitalize,
|
|
325
328
|
data: data.values,
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
tension: 0.1
|
|
329
|
+
platform: platform.to_s.downcase,
|
|
330
|
+
tension: 0.4
|
|
329
331
|
}
|
|
330
332
|
}.to_json %>;
|
|
331
333
|
|
|
332
|
-
//
|
|
334
|
+
// Apply Catppuccin platform colors
|
|
333
335
|
datasets.forEach(ds => {
|
|
334
|
-
const
|
|
335
|
-
ds.borderColor =
|
|
336
|
-
|
|
336
|
+
const color = getPlatformColor(ds.platform, isDark);
|
|
337
|
+
ds.borderColor = color;
|
|
338
|
+
// Add transparency for background
|
|
339
|
+
ds.backgroundColor = color.replace('rgb', 'rgba').replace(')', ', 0.1)');
|
|
337
340
|
});
|
|
338
341
|
|
|
339
342
|
new Chart(dailyTrendCtx, {
|
|
@@ -368,8 +371,16 @@
|
|
|
368
371
|
datasets: [{
|
|
369
372
|
label: 'Hours to Resolve',
|
|
370
373
|
data: times,
|
|
371
|
-
backgroundColor:
|
|
372
|
-
|
|
374
|
+
backgroundColor: (function() {
|
|
375
|
+
const isDark = document.body.classList.contains('dark-mode');
|
|
376
|
+
const config = getCatppuccinChartConfig(isDark);
|
|
377
|
+
return config.colors.orange.replace('rgb', 'rgba').replace(')', ', 0.5)');
|
|
378
|
+
})(),
|
|
379
|
+
borderColor: (function() {
|
|
380
|
+
const isDark = document.body.classList.contains('dark-mode');
|
|
381
|
+
const config = getCatppuccinChartConfig(isDark);
|
|
382
|
+
return config.colors.orange;
|
|
383
|
+
})(),
|
|
373
384
|
borderWidth: 1
|
|
374
385
|
}]
|
|
375
386
|
},
|