rails_error_dashboard 0.1.0 → 0.1.1
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 +257 -700
- data/app/controllers/rails_error_dashboard/application_controller.rb +18 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +47 -4
- data/app/helpers/rails_error_dashboard/application_helper.rb +17 -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_log.rb +326 -3
- data/app/models/rails_error_dashboard/error_occurrence.rb +49 -0
- data/app/views/layouts/rails_error_dashboard.html.erb +150 -9
- 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 +76 -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/analytics.html.erb +19 -39
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +373 -0
- data/app/views/rails_error_dashboard/errors/index.html.erb +215 -138
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +388 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +428 -11
- data/config/routes.rb +2 -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/lib/generators/rails_error_dashboard/install/install_generator.rb +270 -1
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +251 -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/log_error.rb +234 -7
- data/lib/rails_error_dashboard/commands/resolve_error.rb +16 -0
- data/lib/rails_error_dashboard/configuration.rb +82 -5
- data/lib/rails_error_dashboard/error_reporter.rb +15 -7
- 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/plugins/audit_log_plugin.rb +0 -1
- data/lib/rails_error_dashboard/plugins/jira_integration_plugin.rb +2 -3
- data/lib/rails_error_dashboard/plugins/metrics_plugin.rb +0 -2
- 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 +134 -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 +52 -11
- 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/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 +55 -7
- metadata +52 -9
- 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
|
@@ -1,97 +1,122 @@
|
|
|
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
122
|
<div class="card mb-4">
|
|
@@ -100,24 +125,38 @@
|
|
|
100
125
|
</div>
|
|
101
126
|
<div class="card-body">
|
|
102
127
|
<%= form_with url: errors_path, method: :get, class: "row g-3" do %>
|
|
103
|
-
<div class="col-md-
|
|
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
|
+
|
|
106
138
|
<div class="col-md-2">
|
|
107
|
-
<%= select_tag :environment, options_for_select([['All Environments', '']] + @environments.map { |e| [e.titleize, e] }, params[:environment]), class: "form-select" %>
|
|
108
|
-
</div>
|
|
109
|
-
<div class="col-md-2">
|
|
110
|
-
<%= select_tag :platform, options_for_select([['All Platforms', '']] + @platforms.map { |p| [p, p] }, params[:platform]), class: "form-select" %>
|
|
111
|
-
</div>
|
|
112
|
-
<div class="col-md-3">
|
|
113
139
|
<%= select_tag :error_type, options_for_select([['All Types', '']] + @error_types.map { |t| [t, t] }, params[:error_type]), class: "form-select" %>
|
|
114
140
|
</div>
|
|
141
|
+
|
|
115
142
|
<div class="col-md-2">
|
|
116
|
-
|
|
117
|
-
|
|
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" %>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<div class="col-auto">
|
|
153
|
+
<div class="form-check mt-2">
|
|
154
|
+
<%= check_box_tag :unresolved, "true", params[:unresolved] != "false" && params[:unresolved] != "0", class: "form-check-input", data: { uncheck_value: "false" } %>
|
|
118
155
|
<%= label_tag :unresolved, "Unresolved only", class: "form-check-label" %>
|
|
156
|
+
<%= hidden_field_tag :unresolved, "false", id: nil %>
|
|
119
157
|
</div>
|
|
120
158
|
</div>
|
|
159
|
+
|
|
121
160
|
<div class="col-12">
|
|
122
161
|
<%= submit_tag "Apply Filters", class: "btn btn-primary" %>
|
|
123
162
|
<%= link_to "Clear", errors_path, class: "btn btn-outline-secondary" %>
|
|
@@ -168,67 +207,21 @@
|
|
|
168
207
|
<th style="width: 40px;">
|
|
169
208
|
<input type="checkbox" id="select-all" class="form-check-input">
|
|
170
209
|
</th>
|
|
171
|
-
<th>
|
|
210
|
+
<th>Severity</th>
|
|
172
211
|
<th>Error Type</th>
|
|
173
212
|
<th>Message</th>
|
|
174
|
-
<th>
|
|
175
|
-
<th>
|
|
176
|
-
|
|
213
|
+
<th>Occurrences</th>
|
|
214
|
+
<th>First / Last Seen</th>
|
|
215
|
+
<% if @platforms.size > 1 %>
|
|
216
|
+
<th>Platform</th>
|
|
217
|
+
<% end %>
|
|
177
218
|
<th>Status</th>
|
|
178
219
|
<th></th>
|
|
179
220
|
</tr>
|
|
180
221
|
</thead>
|
|
181
|
-
<tbody>
|
|
222
|
+
<tbody id="error_list">
|
|
182
223
|
<% @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>
|
|
224
|
+
<%= render "error_row", error: error, show_platform: @platforms.size > 1 %>
|
|
232
225
|
<% end %>
|
|
233
226
|
</tbody>
|
|
234
227
|
</table>
|
|
@@ -240,8 +233,20 @@
|
|
|
240
233
|
</div>
|
|
241
234
|
<% else %>
|
|
242
235
|
<div class="text-center py-5">
|
|
243
|
-
<i class="bi bi-
|
|
244
|
-
<
|
|
236
|
+
<i class="bi bi-check-circle display-1 text-success mb-3"></i>
|
|
237
|
+
<h4 class="text-muted">All Clear!</h4>
|
|
238
|
+
<p class="text-muted">
|
|
239
|
+
<% if params[:search].present? || params[:error_type].present? || params[:platform].present? || params[:severity].present? %>
|
|
240
|
+
No errors match your current filters. Try adjusting your search criteria.
|
|
241
|
+
<% else %>
|
|
242
|
+
No errors have been logged yet. Your application is running smoothly!
|
|
243
|
+
<% end %>
|
|
244
|
+
</p>
|
|
245
|
+
<% unless params.values.compact.any? %>
|
|
246
|
+
<small class="text-muted d-block mt-3">
|
|
247
|
+
<i class="bi bi-lightbulb"></i> Errors will appear here automatically when they occur.
|
|
248
|
+
</small>
|
|
249
|
+
<% end %>
|
|
245
250
|
</div>
|
|
246
251
|
<% end %>
|
|
247
252
|
</div>
|
|
@@ -331,4 +336,76 @@
|
|
|
331
336
|
});
|
|
332
337
|
}
|
|
333
338
|
});
|
|
339
|
+
|
|
340
|
+
// Turbo Stream animation for new errors
|
|
341
|
+
document.addEventListener('turbo:before-stream-render', (event) => {
|
|
342
|
+
const { target, action } = event.detail.newStream;
|
|
343
|
+
|
|
344
|
+
// Highlight new errors when prepended
|
|
345
|
+
if (action === 'prepend' && target === 'error_list') {
|
|
346
|
+
setTimeout(() => {
|
|
347
|
+
const firstRow = document.querySelector('#error_list tr:first-child');
|
|
348
|
+
if (firstRow) {
|
|
349
|
+
firstRow.classList.add('new-error');
|
|
350
|
+
|
|
351
|
+
// Remove class after animation completes
|
|
352
|
+
setTimeout(() => {
|
|
353
|
+
firstRow.classList.remove('new-error');
|
|
354
|
+
}, 3000);
|
|
355
|
+
}
|
|
356
|
+
}, 10);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Pulse stats cards when updated
|
|
360
|
+
if (action === 'replace' && target === 'dashboard_stats') {
|
|
361
|
+
setTimeout(() => {
|
|
362
|
+
const statCards = document.querySelectorAll('.stat-card');
|
|
363
|
+
statCards.forEach(card => {
|
|
364
|
+
card.classList.add('updated');
|
|
365
|
+
setTimeout(() => card.classList.remove('updated'), 500);
|
|
366
|
+
});
|
|
367
|
+
}, 10);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Initialize Bootstrap tooltips
|
|
372
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
373
|
+
const tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
|
|
374
|
+
tooltipTriggerList.map(function (tooltipTriggerEl) {
|
|
375
|
+
return new bootstrap.Tooltip(tooltipTriggerEl);
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// Keyboard shortcuts
|
|
380
|
+
document.addEventListener('keydown', function(e) {
|
|
381
|
+
// Ignore if typing in input/textarea
|
|
382
|
+
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
|
383
|
+
|
|
384
|
+
// 'r' - Refresh page
|
|
385
|
+
if (e.key === 'r') {
|
|
386
|
+
location.reload();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// '/' - Focus search
|
|
390
|
+
if (e.key === '/') {
|
|
391
|
+
e.preventDefault();
|
|
392
|
+
const searchInput = document.querySelector('input[name="search"]');
|
|
393
|
+
if (searchInput) searchInput.focus();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 'a' - Go to analytics
|
|
397
|
+
if (e.key === 'a') {
|
|
398
|
+
window.location.href = '<%= analytics_errors_path %>';
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// '?' - Show keyboard shortcuts help
|
|
402
|
+
if (e.key === '?') {
|
|
403
|
+
e.preventDefault();
|
|
404
|
+
alert('Keyboard Shortcuts:\n\n' +
|
|
405
|
+
'r - Refresh page\n' +
|
|
406
|
+
'/ - Focus search\n' +
|
|
407
|
+
'a - Analytics page\n' +
|
|
408
|
+
'? - Show this help');
|
|
409
|
+
}
|
|
410
|
+
});
|
|
334
411
|
</script>
|