rails_error_dashboard 0.1.0 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +305 -703
- 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/application_controller.rb +18 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +140 -4
- data/app/helpers/rails_error_dashboard/application_helper.rb +55 -0
- 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/jobs/rails_error_dashboard/application_job.rb +19 -0
- data/app/jobs/rails_error_dashboard/async_error_logging_job.rb +48 -0
- data/app/jobs/rails_error_dashboard/baseline_alert_job.rb +263 -0
- data/app/jobs/rails_error_dashboard/discord_error_notification_job.rb +4 -8
- data/app/jobs/rails_error_dashboard/email_error_notification_job.rb +2 -1
- data/app/jobs/rails_error_dashboard/pagerduty_error_notification_job.rb +5 -5
- data/app/jobs/rails_error_dashboard/slack_error_notification_job.rb +10 -6
- data/app/jobs/rails_error_dashboard/webhook_error_notification_job.rb +5 -6
- data/app/mailers/rails_error_dashboard/application_mailer.rb +1 -1
- data/app/mailers/rails_error_dashboard/error_notification_mailer.rb +1 -1
- data/app/models/rails_error_dashboard/cascade_pattern.rb +74 -0
- data/app/models/rails_error_dashboard/error_baseline.rb +100 -0
- data/app/models/rails_error_dashboard/error_comment.rb +27 -0
- data/app/models/rails_error_dashboard/error_log.rb +471 -3
- data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +816 -178
- data/app/views/layouts/rails_error_dashboard_old_backup.html.erb +383 -0
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb +3 -10
- data/app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb +1 -2
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +78 -0
- data/app/views/rails_error_dashboard/errors/_pattern_insights.html.erb +209 -0
- data/app/views/rails_error_dashboard/errors/_stats.html.erb +34 -0
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +167 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +152 -56
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +294 -138
- data/app/views/rails_error_dashboard/errors/overview.html.erb +253 -0
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +399 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +781 -65
- data/config/routes.rb +9 -0
- data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +66 -0
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +26 -0
- data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +12 -0
- data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +9 -0
- data/db/migrate/20251225100236_create_error_occurrences.rb +31 -0
- data/db/migrate/20251225101920_create_cascade_patterns.rb +33 -0
- data/db/migrate/20251225102500_create_error_baselines.rb +38 -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 +276 -1
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +272 -37
- data/lib/generators/rails_error_dashboard/solid_queue/solid_queue_generator.rb +36 -0
- data/lib/generators/rails_error_dashboard/solid_queue/templates/queue.yml +55 -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 +272 -7
- data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
- data/lib/rails_error_dashboard/configuration.rb +90 -5
- data/lib/rails_error_dashboard/error_reporter.rb +15 -7
- data/lib/rails_error_dashboard/logger.rb +105 -0
- data/lib/rails_error_dashboard/middleware/error_catcher.rb +17 -10
- data/lib/rails_error_dashboard/plugin.rb +6 -3
- data/lib/rails_error_dashboard/plugin_registry.rb +2 -2
- data/lib/rails_error_dashboard/plugins/audit_log_plugin.rb +0 -1
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +3 -4
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +1 -3
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +44 -6
- data/lib/rails_error_dashboard/queries/baseline_stats.rb +107 -0
- data/lib/rails_error_dashboard/queries/co_occurring_errors.rb +86 -0
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +242 -2
- data/lib/rails_error_dashboard/queries/error_cascades.rb +74 -0
- data/lib/rails_error_dashboard/queries/error_correlation.rb +375 -0
- data/lib/rails_error_dashboard/queries/errors_list.rb +106 -10
- data/lib/rails_error_dashboard/queries/filter_options.rb +0 -1
- data/lib/rails_error_dashboard/queries/platform_comparison.rb +254 -0
- data/lib/rails_error_dashboard/queries/similar_errors.rb +93 -0
- data/lib/rails_error_dashboard/services/backtrace_parser.rb +113 -0
- data/lib/rails_error_dashboard/services/baseline_alert_throttler.rb +88 -0
- data/lib/rails_error_dashboard/services/baseline_calculator.rb +269 -0
- data/lib/rails_error_dashboard/services/cascade_detector.rb +95 -0
- data/lib/rails_error_dashboard/services/pattern_detector.rb +268 -0
- data/lib/rails_error_dashboard/services/similarity_calculator.rb +144 -0
- data/lib/rails_error_dashboard/value_objects/error_context.rb +27 -1
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +57 -7
- metadata +69 -10
- data/app/models/rails_error_dashboard/application_record.rb +0 -5
- data/lib/rails_error_dashboard/queries/developer_insights.rb +0 -277
- data/lib/rails_error_dashboard/queries/errors_list_v2.rb +0 -149
- data/lib/tasks/rails_error_dashboard_tasks.rake +0 -4
|
@@ -1,123 +1,208 @@
|
|
|
1
|
+
<!-- Subscribe to Turbo Stream updates -->
|
|
2
|
+
<%= turbo_stream_from "error_list" %>
|
|
3
|
+
|
|
1
4
|
<div class="py-4">
|
|
2
5
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
3
6
|
<h2 class="mb-0"><i class="bi bi-bug-fill text-primary"></i> Error Overview</h2>
|
|
4
7
|
<div class="text-muted">
|
|
5
|
-
<small>
|
|
8
|
+
<small>
|
|
9
|
+
Last updated: <%= Time.current.strftime("%B %d, %Y %I:%M %p") %>
|
|
10
|
+
<span class="badge bg-success ms-2" id="live-indicator">
|
|
11
|
+
<i class="bi bi-broadcast"></i> Live
|
|
12
|
+
</span>
|
|
13
|
+
</small>
|
|
6
14
|
</div>
|
|
7
15
|
</div>
|
|
8
16
|
|
|
9
17
|
<!-- Stats Cards -->
|
|
10
|
-
<div class="
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<div class="
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
</div>
|
|
35
|
-
<div class="col-md-3">
|
|
36
|
-
<div class="card stat-card">
|
|
37
|
-
<div class="card-body">
|
|
38
|
-
<div class="stat-label mb-2">Resolved</div>
|
|
39
|
-
<div class="stat-value text-success"><%= @stats[:resolved] %></div>
|
|
18
|
+
<div id="dashboard_stats" class="mb-4">
|
|
19
|
+
<%= render "stats", stats: @stats %>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Top Error Types (Only show if there are errors) -->
|
|
23
|
+
<% if @stats[:top_errors].any? %>
|
|
24
|
+
<div class="row g-4 mb-4">
|
|
25
|
+
<div class="col-md-12">
|
|
26
|
+
<div class="card">
|
|
27
|
+
<div class="card-header bg-white">
|
|
28
|
+
<h5 class="mb-0">Top Error Types</h5>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="card-body">
|
|
31
|
+
<div class="row">
|
|
32
|
+
<% @stats[:top_errors].first(5).each do |error_type, count| %>
|
|
33
|
+
<div class="col-md-2">
|
|
34
|
+
<div class="text-center p-3 border rounded">
|
|
35
|
+
<div class="fw-bold text-danger" style="font-size: 1.5rem;"><%= count %></div>
|
|
36
|
+
<small class="text-muted text-truncate d-block" title="<%= error_type %>"><%= error_type.split('::').last %></small>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
40
42
|
</div>
|
|
41
43
|
</div>
|
|
42
44
|
</div>
|
|
43
|
-
|
|
45
|
+
<% end %>
|
|
44
46
|
|
|
45
|
-
<!--
|
|
46
|
-
|
|
47
|
-
<div class="
|
|
48
|
-
<div class="
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
<% elsif platform == 'Android' %>
|
|
60
|
-
<span class="badge badge-android">Android</span>
|
|
61
|
-
<% else %>
|
|
62
|
-
<span class="badge badge-api">API</span>
|
|
63
|
-
<% end %>
|
|
64
|
-
</span>
|
|
65
|
-
<span class="fw-bold"><%= count %></span>
|
|
66
|
-
</div>
|
|
47
|
+
<!-- Spike Detection Alert -->
|
|
48
|
+
<% if @stats[:spike_detected] %>
|
|
49
|
+
<div class="alert alert-warning mb-4" role="alert">
|
|
50
|
+
<div class="d-flex align-items-center">
|
|
51
|
+
<i class="bi bi-exclamation-triangle-fill fs-3 me-3"></i>
|
|
52
|
+
<div>
|
|
53
|
+
<h5 class="alert-heading mb-1">
|
|
54
|
+
<% case @stats[:spike_info][:severity] %>
|
|
55
|
+
<% when :critical %>
|
|
56
|
+
🚨 Critical Error Spike Detected!
|
|
57
|
+
<% when :high %>
|
|
58
|
+
⚠️ High Error Spike Detected
|
|
59
|
+
<% when :elevated %>
|
|
60
|
+
📈 Elevated Error Activity
|
|
67
61
|
<% end %>
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
62
|
+
</h5>
|
|
63
|
+
<p class="mb-0">
|
|
64
|
+
Today: <strong><%= @stats[:spike_info][:today_count] %> errors</strong>
|
|
65
|
+
(7-day avg: <%= @stats[:spike_info][:avg_count] %>) —
|
|
66
|
+
<strong><%= @stats[:spike_info][:multiplier] %>x normal levels</strong>
|
|
67
|
+
</p>
|
|
71
68
|
</div>
|
|
72
69
|
</div>
|
|
73
70
|
</div>
|
|
71
|
+
<% end %>
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
<
|
|
84
|
-
<small class="text-truncate" style="max-width: 70%;"><%= error_type %></small>
|
|
85
|
-
<span class="badge bg-secondary"><%= count %></span>
|
|
86
|
-
</div>
|
|
73
|
+
<!-- 7-Day Error Trend -->
|
|
74
|
+
<% if @stats[:errors_trend_7d]&.any? %>
|
|
75
|
+
<div class="row g-4 mb-4">
|
|
76
|
+
<div class="col-md-8">
|
|
77
|
+
<div class="card">
|
|
78
|
+
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
79
|
+
<h5 class="mb-0"><i class="bi bi-graph-up"></i> 7-Day Error Trend</h5>
|
|
80
|
+
<%= link_to analytics_errors_path, class: "btn btn-sm btn-outline-primary" do %>
|
|
81
|
+
<i class="bi bi-bar-chart"></i> Full Analytics
|
|
87
82
|
<% end %>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
</div>
|
|
84
|
+
<div class="card-body">
|
|
85
|
+
<%= line_chart @stats[:errors_trend_7d],
|
|
86
|
+
color: "#8B5CF6",
|
|
87
|
+
curve: false,
|
|
88
|
+
points: true,
|
|
89
|
+
height: "250px",
|
|
90
|
+
library: {
|
|
91
|
+
plugins: {
|
|
92
|
+
legend: { display: false }
|
|
93
|
+
},
|
|
94
|
+
scales: {
|
|
95
|
+
y: {
|
|
96
|
+
beginAtZero: true,
|
|
97
|
+
ticks: { precision: 0 }
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
} %>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
<div class="col-md-4">
|
|
105
|
+
<div class="card">
|
|
106
|
+
<div class="card-header bg-white">
|
|
107
|
+
<h5 class="mb-0"><i class="bi bi-pie-chart"></i> By Severity (7d)</h5>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="card-body">
|
|
110
|
+
<%= pie_chart @stats[:errors_by_severity_7d],
|
|
111
|
+
colors: ["#EF4444", "#F59E0B", "#3B82F6", "#6B7280"],
|
|
112
|
+
height: "250px",
|
|
113
|
+
legend: "bottom",
|
|
114
|
+
donut: true %>
|
|
115
|
+
</div>
|
|
91
116
|
</div>
|
|
92
117
|
</div>
|
|
93
118
|
</div>
|
|
94
|
-
|
|
119
|
+
<% end %>
|
|
95
120
|
|
|
96
121
|
<!-- Filters -->
|
|
97
|
-
<div class="card mb-4">
|
|
122
|
+
<div class="card mb-4" id="filters-section">
|
|
98
123
|
<div class="card-header bg-white">
|
|
99
124
|
<h5 class="mb-0">Filters & Search</h5>
|
|
100
125
|
</div>
|
|
101
126
|
<div class="card-body">
|
|
102
|
-
<%= form_with url: errors_path, method: :get, class: "row g-3" do %>
|
|
103
|
-
<div class="col-md-
|
|
127
|
+
<%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>
|
|
128
|
+
<div class="col-md-4">
|
|
104
129
|
<%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
|
|
105
130
|
</div>
|
|
131
|
+
|
|
132
|
+
<% if @platforms.size > 1 %>
|
|
133
|
+
<div class="col-md-2">
|
|
134
|
+
<%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select" %>
|
|
135
|
+
</div>
|
|
136
|
+
<% end %>
|
|
137
|
+
|
|
138
|
+
<div class="col-md-2">
|
|
139
|
+
<%= select_tag :error_type, options_for_select([['All Types', '']] + @error_types.map { |t| [t, t] }, params[:error_type]), class: "form-select" %>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
106
142
|
<div class="col-md-2">
|
|
107
|
-
<%= select_tag :
|
|
143
|
+
<%= select_tag :severity, options_for_select([
|
|
144
|
+
['All Severities', ''],
|
|
145
|
+
['🔴 Critical', 'critical'],
|
|
146
|
+
['🟠 High', 'high'],
|
|
147
|
+
['🟡 Medium', 'medium'],
|
|
148
|
+
['⚪ Low', 'low']
|
|
149
|
+
], params[:severity]), class: "form-select" %>
|
|
108
150
|
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Phase 3: Workflow Filters -->
|
|
109
153
|
<div class="col-md-2">
|
|
110
|
-
<%= select_tag :
|
|
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" %>
|
|
111
162
|
</div>
|
|
112
|
-
|
|
113
|
-
|
|
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" %>
|
|
114
170
|
</div>
|
|
171
|
+
|
|
115
172
|
<div class="col-md-2">
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
|
|
182
|
+
<div class="col-auto">
|
|
183
|
+
<div class="form-check mt-2">
|
|
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" %>
|
|
118
195
|
<%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
|
|
119
196
|
</div>
|
|
120
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" %>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
121
206
|
<div class="col-12">
|
|
122
207
|
<%= submit_tag "Apply Filters", class: "btn btn-primary" %>
|
|
123
208
|
<%= link_to "Clear", errors_path, class: "btn btn-outline-secondary" %>
|
|
@@ -168,67 +253,21 @@
|
|
|
168
253
|
<th style="width: 40px;">
|
|
169
254
|
<input type="checkbox" id="select-all" class="form-check-input">
|
|
170
255
|
</th>
|
|
171
|
-
<th>
|
|
256
|
+
<th>Severity</th>
|
|
172
257
|
<th>Error Type</th>
|
|
173
258
|
<th>Message</th>
|
|
174
|
-
<th>
|
|
175
|
-
<th>
|
|
176
|
-
|
|
259
|
+
<th>Occurrences</th>
|
|
260
|
+
<th>First / Last Seen</th>
|
|
261
|
+
<% if @platforms.size > 1 %>
|
|
262
|
+
<th>Platform</th>
|
|
263
|
+
<% end %>
|
|
177
264
|
<th>Status</th>
|
|
178
265
|
<th></th>
|
|
179
266
|
</tr>
|
|
180
267
|
</thead>
|
|
181
|
-
<tbody>
|
|
268
|
+
<tbody id="error_list">
|
|
182
269
|
<% @errors.each do |error| %>
|
|
183
|
-
|
|
184
|
-
<td onclick="event.stopPropagation();">
|
|
185
|
-
<input type="checkbox" class="error-checkbox form-check-input" value="<%= error.id %>" data-error-id="<%= error.id %>">
|
|
186
|
-
</td>
|
|
187
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
188
|
-
<small><%= error.occurred_at.strftime("%m/%d %I:%M%p") %></small>
|
|
189
|
-
</td>
|
|
190
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
191
|
-
<code class="text-danger"><%= error.error_type.split('::').last %></code>
|
|
192
|
-
</td>
|
|
193
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
194
|
-
<div class="text-truncate" style="max-width: 300px;" title="<%= error.message %>">
|
|
195
|
-
<%= error.message %>
|
|
196
|
-
</div>
|
|
197
|
-
</td>
|
|
198
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
199
|
-
<% if error.platform == 'iOS' %>
|
|
200
|
-
<span class="badge badge-ios">iOS</span>
|
|
201
|
-
<% elsif error.platform == 'Android' %>
|
|
202
|
-
<span class="badge badge-android">Android</span>
|
|
203
|
-
<% else %>
|
|
204
|
-
<span class="badge badge-api"><%= error.platform || 'API' %></span>
|
|
205
|
-
<% end %>
|
|
206
|
-
</td>
|
|
207
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
208
|
-
<span class="badge <%= error.environment == 'production' ? 'bg-danger' : 'bg-info' %>">
|
|
209
|
-
<%= error.environment %>
|
|
210
|
-
</span>
|
|
211
|
-
</td>
|
|
212
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
213
|
-
<% if error.user %>
|
|
214
|
-
<small><%= error.user.email %></small>
|
|
215
|
-
<% else %>
|
|
216
|
-
<small class="text-muted">Guest</small>
|
|
217
|
-
<% end %>
|
|
218
|
-
</td>
|
|
219
|
-
<td onclick="window.location='<%= error_path(error) %>';">
|
|
220
|
-
<% if error.resolved? %>
|
|
221
|
-
<i class="bi bi-check-circle-fill text-success" title="Resolved"></i>
|
|
222
|
-
<% else %>
|
|
223
|
-
<i class="bi bi-exclamation-circle-fill text-danger" title="Unresolved"></i>
|
|
224
|
-
<% end %>
|
|
225
|
-
</td>
|
|
226
|
-
<td onclick="event.stopPropagation();">
|
|
227
|
-
<%= link_to error_path(error), class: "btn btn-sm btn-outline-primary" do %>
|
|
228
|
-
<i class="bi bi-eye"></i>
|
|
229
|
-
<% end %>
|
|
230
|
-
</td>
|
|
231
|
-
</tr>
|
|
270
|
+
<%= render "error_row", error: error, show_platform: @platforms.size > 1 %>
|
|
232
271
|
<% end %>
|
|
233
272
|
</tbody>
|
|
234
273
|
</table>
|
|
@@ -240,8 +279,20 @@
|
|
|
240
279
|
</div>
|
|
241
280
|
<% else %>
|
|
242
281
|
<div class="text-center py-5">
|
|
243
|
-
<i class="bi bi-
|
|
244
|
-
<
|
|
282
|
+
<i class="bi bi-check-circle display-1 text-success mb-3"></i>
|
|
283
|
+
<h4 class="text-muted">All Clear!</h4>
|
|
284
|
+
<p class="text-muted">
|
|
285
|
+
<% if params[:search].present? || params[:error_type].present? || params[:platform].present? || params[:severity].present? %>
|
|
286
|
+
No errors match your current filters. Try adjusting your search criteria.
|
|
287
|
+
<% else %>
|
|
288
|
+
No errors have been logged yet. Your application is running smoothly!
|
|
289
|
+
<% end %>
|
|
290
|
+
</p>
|
|
291
|
+
<% unless params.values.compact.any? %>
|
|
292
|
+
<small class="text-muted d-block mt-3">
|
|
293
|
+
<i class="bi bi-lightbulb"></i> Errors will appear here automatically when they occur.
|
|
294
|
+
</small>
|
|
295
|
+
<% end %>
|
|
245
296
|
</div>
|
|
246
297
|
<% end %>
|
|
247
298
|
</div>
|
|
@@ -331,4 +382,109 @@
|
|
|
331
382
|
});
|
|
332
383
|
}
|
|
333
384
|
});
|
|
385
|
+
|
|
386
|
+
// Turbo Stream animation for new errors
|
|
387
|
+
document.addEventListener('turbo:before-stream-render', (event) => {
|
|
388
|
+
const { target, action } = event.detail.newStream;
|
|
389
|
+
|
|
390
|
+
// Highlight new errors when prepended
|
|
391
|
+
if (action === 'prepend' && target === 'error_list') {
|
|
392
|
+
setTimeout(() => {
|
|
393
|
+
const firstRow = document.querySelector('#error_list tr:first-child');
|
|
394
|
+
if (firstRow) {
|
|
395
|
+
firstRow.classList.add('new-error');
|
|
396
|
+
|
|
397
|
+
// Remove class after animation completes
|
|
398
|
+
setTimeout(() => {
|
|
399
|
+
firstRow.classList.remove('new-error');
|
|
400
|
+
}, 3000);
|
|
401
|
+
}
|
|
402
|
+
}, 10);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Pulse stats cards when updated
|
|
406
|
+
if (action === 'replace' && target === 'dashboard_stats') {
|
|
407
|
+
setTimeout(() => {
|
|
408
|
+
const statCards = document.querySelectorAll('.stat-card');
|
|
409
|
+
statCards.forEach(card => {
|
|
410
|
+
card.classList.add('updated');
|
|
411
|
+
setTimeout(() => card.classList.remove('updated'), 500);
|
|
412
|
+
});
|
|
413
|
+
}, 10);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
// Initialize Bootstrap tooltips
|
|
418
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
419
|
+
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
420
|
+
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
421
|
+
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Keyboard shortcuts
|
|
426
|
+
document.addEventListener('keydown', function(e) {
|
|
427
|
+
// Ignore if typing in input/textarea
|
|
428
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
429
|
+
|
|
430
|
+
// 'r' - Refresh page
|
|
431
|
+
if (e.key === 'r') {
|
|
432
|
+
location.reload();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// '/' - Focus search
|
|
436
|
+
if (e.key === '/') {
|
|
437
|
+
e.preventDefault();
|
|
438
|
+
const searchInput = document.querySelector('input[name="search"]');
|
|
439
|
+
if (searchInput) searchInput.focus();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// 'a' - Go to analytics
|
|
443
|
+
if (e.key === 'a') {
|
|
444
|
+
window.location.href = '<%= analytics_errors_path %>';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// '?' - Show keyboard shortcuts help
|
|
448
|
+
if (e.key === '?') {
|
|
449
|
+
e.preventDefault();
|
|
450
|
+
alert('Keyboard Shortcuts:\n\n' +
|
|
451
|
+
'r - Refresh page\n' +
|
|
452
|
+
'/ - Focus search\n' +
|
|
453
|
+
'a - Analytics page\n' +
|
|
454
|
+
'? - Show this help');
|
|
455
|
+
}
|
|
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
|
+
});
|
|
334
490
|
</script>
|