rails_error_dashboard 0.1.28 → 0.1.30
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 +50 -6
- data/app/controllers/rails_error_dashboard/errors_controller.rb +22 -0
- data/app/helpers/rails_error_dashboard/application_helper.rb +79 -7
- data/app/helpers/rails_error_dashboard/backtrace_helper.rb +149 -0
- data/app/models/rails_error_dashboard/application.rb +1 -1
- data/app/models/rails_error_dashboard/error_log.rb +44 -16
- data/app/views/layouts/rails_error_dashboard.html.erb +71 -1237
- data/app/views/rails_error_dashboard/errors/_error_row.html.erb +10 -2
- data/app/views/rails_error_dashboard/errors/_source_code.html.erb +76 -0
- data/app/views/rails_error_dashboard/errors/_timeline.html.erb +18 -82
- data/app/views/rails_error_dashboard/errors/_user_errors_table.html.erb +70 -0
- data/app/views/rails_error_dashboard/errors/analytics.html.erb +9 -37
- data/app/views/rails_error_dashboard/errors/correlation.html.erb +11 -37
- data/app/views/rails_error_dashboard/errors/index.html.erb +64 -31
- data/app/views/rails_error_dashboard/errors/overview.html.erb +181 -3
- data/app/views/rails_error_dashboard/errors/platform_comparison.html.erb +2 -1
- data/app/views/rails_error_dashboard/errors/settings/_value_badge.html.erb +286 -0
- data/app/views/rails_error_dashboard/errors/settings.html.erb +146 -480
- data/app/views/rails_error_dashboard/errors/show.html.erb +102 -76
- data/db/migrate/20251223000000_create_rails_error_dashboard_complete_schema.rb +188 -0
- data/db/migrate/20251224000001_create_rails_error_dashboard_error_logs.rb +5 -0
- data/db/migrate/20251224081522_add_better_tracking_to_error_logs.rb +3 -0
- data/db/migrate/20251224101217_add_controller_action_to_error_logs.rb +3 -0
- data/db/migrate/20251225071314_add_optimized_indexes_to_error_logs.rb +4 -0
- data/db/migrate/20251225074653_remove_environment_from_error_logs.rb +3 -0
- data/db/migrate/20251225085859_add_enhanced_metrics_to_error_logs.rb +3 -0
- data/db/migrate/20251225093603_add_similarity_tracking_to_error_logs.rb +3 -0
- data/db/migrate/20251225100236_create_error_occurrences.rb +3 -0
- data/db/migrate/20251225101920_create_cascade_patterns.rb +3 -0
- data/db/migrate/20251225102500_create_error_baselines.rb +3 -0
- data/db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb +3 -0
- data/db/migrate/20251226020100_create_error_comments.rb +3 -0
- data/db/migrate/20251229111223_add_additional_performance_indexes.rb +4 -0
- data/db/migrate/20260106094220_create_rails_error_dashboard_applications.rb +3 -0
- data/db/migrate/20260106094233_add_application_to_error_logs.rb +3 -0
- data/db/migrate/20260106094318_finalize_application_foreign_key.rb +5 -0
- data/lib/generators/rails_error_dashboard/install/install_generator.rb +32 -0
- data/lib/generators/rails_error_dashboard/install/templates/initializer.rb +37 -4
- data/lib/rails_error_dashboard/configuration.rb +160 -3
- data/lib/rails_error_dashboard/configuration_error.rb +24 -0
- data/lib/rails_error_dashboard/engine.rb +17 -0
- data/lib/rails_error_dashboard/helpers/user_model_detector.rb +138 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +1 -2
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +19 -4
- data/lib/rails_error_dashboard/queries/errors_list.rb +27 -8
- data/lib/rails_error_dashboard/services/error_normalizer.rb +143 -0
- data/lib/rails_error_dashboard/services/git_blame_reader.rb +195 -0
- data/lib/rails_error_dashboard/services/github_link_generator.rb +159 -0
- data/lib/rails_error_dashboard/services/source_code_reader.rb +214 -0
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +6 -0
- metadata +14 -10
- data/app/assets/stylesheets/rails_error_dashboard/_catppuccin_mocha.scss +0 -107
- data/app/assets/stylesheets/rails_error_dashboard/_components.scss +0 -625
- data/app/assets/stylesheets/rails_error_dashboard/_layout.scss +0 -257
- data/app/assets/stylesheets/rails_error_dashboard/_theme_variables.scss +0 -203
- data/app/assets/stylesheets/rails_error_dashboard/application.css +0 -15
- data/app/assets/stylesheets/rails_error_dashboard/application.css.map +0 -7
- data/app/assets/stylesheets/rails_error_dashboard/application.scss +0 -61
- data/app/views/layouts/rails_error_dashboard/application.html.erb +0 -55
|
@@ -20,795 +20,14 @@
|
|
|
20
20
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
|
21
21
|
<script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script>
|
|
22
22
|
|
|
23
|
-
<!--
|
|
24
|
-
<
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
--ctp-red: #f38ba8;
|
|
32
|
-
--ctp-maroon: #eba0ac;
|
|
33
|
-
--ctp-peach: #fab387;
|
|
34
|
-
--ctp-yellow: #f9e2af;
|
|
35
|
-
--ctp-green: #a6e3a1;
|
|
36
|
-
--ctp-teal: #94e2d5;
|
|
37
|
-
--ctp-sky: #89dceb;
|
|
38
|
-
--ctp-sapphire: #74c7ec;
|
|
39
|
-
--ctp-blue: #89b4fa;
|
|
40
|
-
--ctp-lavender: #b4befe;
|
|
41
|
-
--ctp-text: #cdd6f4;
|
|
42
|
-
--ctp-subtext1: #bac2de;
|
|
43
|
-
--ctp-subtext0: #a6adc8;
|
|
44
|
-
--ctp-overlay2: #9399b2;
|
|
45
|
-
--ctp-overlay1: #7f849c;
|
|
46
|
-
--ctp-overlay0: #6c7086;
|
|
47
|
-
--ctp-surface2: #585b70;
|
|
48
|
-
--ctp-surface1: #45475a;
|
|
49
|
-
--ctp-surface0: #313244;
|
|
50
|
-
--ctp-base: #1e1e2e;
|
|
51
|
-
--ctp-mantle: #181825;
|
|
52
|
-
--ctp-crust: #11111b;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/* Light Theme (Default) */
|
|
56
|
-
body {
|
|
57
|
-
background-color: #f3f4f6;
|
|
58
|
-
color: #1f2937;
|
|
59
|
-
transition: background-color 0.3s, color 0.3s;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/* Dark Theme */
|
|
63
|
-
body.dark-mode {
|
|
64
|
-
background-color: var(--ctp-base);
|
|
65
|
-
color: var(--ctp-text);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* Navbar */
|
|
69
|
-
.navbar {
|
|
70
|
-
background: linear-gradient(135deg, #8B5CF6, #6D28D9) !important;
|
|
71
|
-
color: white !important;
|
|
72
|
-
}
|
|
73
|
-
.navbar * {
|
|
74
|
-
color: white !important;
|
|
75
|
-
}
|
|
76
|
-
.navbar-brand {
|
|
77
|
-
font-weight: bold;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/* Theme Toggle Button */
|
|
81
|
-
.theme-toggle {
|
|
82
|
-
cursor: pointer;
|
|
83
|
-
padding: 0.5rem 1rem;
|
|
84
|
-
border-radius: 0.5rem;
|
|
85
|
-
background-color: rgba(255, 255, 255, 0.1);
|
|
86
|
-
color: white;
|
|
87
|
-
border: none;
|
|
88
|
-
transition: background-color 0.2s;
|
|
89
|
-
}
|
|
90
|
-
.theme-toggle:hover {
|
|
91
|
-
background-color: rgba(255, 255, 255, 0.2);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/* App Switcher Button - must override navbar * rule */
|
|
95
|
-
.app-switcher-btn {
|
|
96
|
-
background-color: rgba(255, 255, 255, 0.1) !important;
|
|
97
|
-
color: white !important;
|
|
98
|
-
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
|
99
|
-
transition: background-color 0.2s;
|
|
100
|
-
}
|
|
101
|
-
.app-switcher-btn:hover {
|
|
102
|
-
background-color: rgba(255, 255, 255, 0.2) !important;
|
|
103
|
-
}
|
|
104
|
-
.app-switcher-btn i,
|
|
105
|
-
.app-switcher-btn * {
|
|
106
|
-
color: white !important;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/* Sidebar */
|
|
110
|
-
.sidebar {
|
|
111
|
-
background: white;
|
|
112
|
-
min-height: calc(100vh - 56px);
|
|
113
|
-
box-shadow: 2px 0 4px rgba(0, 0, 0, 0.05);
|
|
114
|
-
}
|
|
115
|
-
body.dark-mode .sidebar {
|
|
116
|
-
background: var(--ctp-mantle);
|
|
117
|
-
}
|
|
118
|
-
.sidebar .nav-link {
|
|
119
|
-
color: #1f2937;
|
|
120
|
-
padding: 0.75rem 1.5rem;
|
|
121
|
-
border-left: 3px solid transparent;
|
|
122
|
-
transition: all 0.2s;
|
|
123
|
-
}
|
|
124
|
-
body.dark-mode .sidebar .nav-link {
|
|
125
|
-
color: var(--ctp-text);
|
|
126
|
-
}
|
|
127
|
-
.sidebar .nav-link:hover {
|
|
128
|
-
background-color: #f3f4f6;
|
|
129
|
-
color: #8B5CF6;
|
|
130
|
-
border-left-color: #8B5CF6;
|
|
131
|
-
}
|
|
132
|
-
body.dark-mode .sidebar .nav-link:hover {
|
|
133
|
-
background-color: var(--ctp-surface0);
|
|
134
|
-
color: var(--ctp-mauve);
|
|
135
|
-
border-left-color: var(--ctp-mauve);
|
|
136
|
-
}
|
|
137
|
-
.sidebar .nav-link.active {
|
|
138
|
-
background-color: #f3f4f6;
|
|
139
|
-
color: #8B5CF6;
|
|
140
|
-
border-left-color: #8B5CF6;
|
|
141
|
-
font-weight: 600;
|
|
142
|
-
}
|
|
143
|
-
body.dark-mode .sidebar .nav-link.active {
|
|
144
|
-
background-color: var(--ctp-surface0);
|
|
145
|
-
color: var(--ctp-mauve);
|
|
146
|
-
border-left-color: var(--ctp-mauve);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/* Cards */
|
|
150
|
-
.card {
|
|
151
|
-
background: white;
|
|
152
|
-
border: 1px solid #e5e7eb;
|
|
153
|
-
transition: background-color 0.3s, border-color 0.3s;
|
|
154
|
-
}
|
|
155
|
-
body.dark-mode .card {
|
|
156
|
-
background: var(--ctp-surface0);
|
|
157
|
-
border-color: var(--ctp-surface2);
|
|
158
|
-
color: var(--ctp-text);
|
|
159
|
-
}
|
|
160
|
-
body.dark-mode .card-header {
|
|
161
|
-
background-color: var(--ctp-surface1);
|
|
162
|
-
border-color: var(--ctp-surface2);
|
|
163
|
-
color: var(--ctp-text);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/* Tables */
|
|
167
|
-
body.dark-mode .table {
|
|
168
|
-
color: var(--ctp-text);
|
|
169
|
-
}
|
|
170
|
-
body.dark-mode .table-hover tbody tr:hover {
|
|
171
|
-
background-color: var(--ctp-surface1);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/* Sticky table header for error list */
|
|
175
|
-
.table-responsive thead th {
|
|
176
|
-
position: sticky;
|
|
177
|
-
top: 0;
|
|
178
|
-
z-index: 10;
|
|
179
|
-
background-color: #f8f9fa;
|
|
180
|
-
}
|
|
181
|
-
body.dark-mode .table-responsive thead th {
|
|
182
|
-
background-color: var(--ctp-mantle);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/* Badges - Platform Colors */
|
|
186
|
-
.badge-ios {
|
|
187
|
-
background-color: #000;
|
|
188
|
-
color: white;
|
|
189
|
-
}
|
|
190
|
-
body.dark-mode .badge-ios {
|
|
191
|
-
background-color: var(--ctp-overlay0);
|
|
192
|
-
color: var(--ctp-text);
|
|
193
|
-
border: 1px solid var(--ctp-surface2);
|
|
194
|
-
}
|
|
195
|
-
.badge-android {
|
|
196
|
-
background-color: #3DDC84;
|
|
197
|
-
color: white;
|
|
198
|
-
}
|
|
199
|
-
body.dark-mode .badge-android {
|
|
200
|
-
background-color: rgba(166, 227, 161, 0.2);
|
|
201
|
-
color: var(--ctp-green);
|
|
202
|
-
border: 1px solid var(--ctp-green);
|
|
203
|
-
}
|
|
204
|
-
.badge-web {
|
|
205
|
-
background-color: #3B82F6;
|
|
206
|
-
color: white;
|
|
207
|
-
}
|
|
208
|
-
body.dark-mode .badge-web {
|
|
209
|
-
background-color: rgba(137, 180, 250, 0.2);
|
|
210
|
-
color: var(--ctp-blue);
|
|
211
|
-
border: 1px solid var(--ctp-blue);
|
|
212
|
-
}
|
|
213
|
-
.badge-api {
|
|
214
|
-
background-color: #8B5CF6;
|
|
215
|
-
color: white;
|
|
216
|
-
}
|
|
217
|
-
body.dark-mode .badge-api {
|
|
218
|
-
background-color: rgba(116, 199, 236, 0.2);
|
|
219
|
-
color: var(--ctp-sapphire);
|
|
220
|
-
border: 1px solid var(--ctp-sapphire);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/* Forms */
|
|
224
|
-
body.dark-mode .form-control,
|
|
225
|
-
body.dark-mode .form-select {
|
|
226
|
-
background-color: var(--ctp-surface0);
|
|
227
|
-
border-color: var(--ctp-surface2);
|
|
228
|
-
color: var(--ctp-text);
|
|
229
|
-
}
|
|
230
|
-
body.dark-mode .form-control:focus,
|
|
231
|
-
body.dark-mode .form-select:focus {
|
|
232
|
-
background-color: var(--ctp-surface1);
|
|
233
|
-
border-color: var(--ctp-mauve);
|
|
234
|
-
color: var(--ctp-text);
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/* Code Blocks */
|
|
238
|
-
.code-block,
|
|
239
|
-
pre,
|
|
240
|
-
code {
|
|
241
|
-
background-color: #f9fafb;
|
|
242
|
-
color: #1f2937;
|
|
243
|
-
}
|
|
244
|
-
body.dark-mode .code-block,
|
|
245
|
-
body.dark-mode pre,
|
|
246
|
-
body.dark-mode code {
|
|
247
|
-
background-color: var(--ctp-mantle) !important;
|
|
248
|
-
color: var(--ctp-text) !important;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/* Alerts */
|
|
252
|
-
body.dark-mode .alert {
|
|
253
|
-
background-color: var(--ctp-surface0);
|
|
254
|
-
border-color: var(--ctp-surface2);
|
|
255
|
-
color: var(--ctp-text);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/* Text colors */
|
|
259
|
-
body.dark-mode .text-muted {
|
|
260
|
-
color: var(--ctp-subtext0) !important;
|
|
261
|
-
}
|
|
262
|
-
body.dark-mode .text-primary {
|
|
263
|
-
color: var(--ctp-mauve) !important;
|
|
264
|
-
}
|
|
265
|
-
body.dark-mode .text-danger {
|
|
266
|
-
color: var(--ctp-red) !important;
|
|
267
|
-
}
|
|
268
|
-
body.dark-mode .text-success {
|
|
269
|
-
color: var(--ctp-green) !important;
|
|
270
|
-
}
|
|
271
|
-
body.dark-mode .text-warning {
|
|
272
|
-
color: var(--ctp-peach) !important;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/* Override Bootstrap bg-white and bg-light */
|
|
276
|
-
body.dark-mode .bg-white {
|
|
277
|
-
background-color: var(--ctp-surface0) !important;
|
|
278
|
-
}
|
|
279
|
-
body.dark-mode .bg-light {
|
|
280
|
-
background-color: var(--ctp-surface1) !important;
|
|
281
|
-
color: var(--ctp-text) !important;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/* Borders */
|
|
285
|
-
body.dark-mode .border {
|
|
286
|
-
border-color: var(--ctp-surface2) !important;
|
|
287
|
-
}
|
|
288
|
-
body.dark-mode .rounded {
|
|
289
|
-
border-color: var(--ctp-surface2) !important;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/* Quick Filters Sidebar Section */
|
|
293
|
-
.sidebar h6 {
|
|
294
|
-
font-size: 0.75rem;
|
|
295
|
-
text-transform: uppercase;
|
|
296
|
-
letter-spacing: 0.05em;
|
|
297
|
-
padding: 0.75rem 1.5rem;
|
|
298
|
-
margin: 0;
|
|
299
|
-
color: #6B7280;
|
|
300
|
-
}
|
|
301
|
-
body.dark-mode .sidebar h6 {
|
|
302
|
-
color: var(--ctp-subtext0);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
/* Stat Cards */
|
|
306
|
-
.stat-card {
|
|
307
|
-
border-radius: 0.75rem;
|
|
308
|
-
transition: transform 0.2s, box-shadow 0.2s;
|
|
309
|
-
}
|
|
310
|
-
.stat-card:hover {
|
|
311
|
-
transform: translateY(-2px);
|
|
312
|
-
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/* Badges for severity */
|
|
316
|
-
.badge.bg-danger {
|
|
317
|
-
background-color: #EF4444 !important;
|
|
318
|
-
}
|
|
319
|
-
.badge.bg-warning {
|
|
320
|
-
background-color: #F59E0B !important;
|
|
321
|
-
}
|
|
322
|
-
.badge.bg-info {
|
|
323
|
-
background-color: #3B82F6 !important;
|
|
324
|
-
}
|
|
325
|
-
.badge.bg-secondary {
|
|
326
|
-
background-color: #6B7280 !important;
|
|
327
|
-
}
|
|
328
|
-
.badge.bg-success {
|
|
329
|
-
background-color: #10B981 !important;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
body.dark-mode .badge.bg-danger {
|
|
333
|
-
background-color: var(--ctp-red) !important;
|
|
334
|
-
color: var(--ctp-base) !important;
|
|
335
|
-
}
|
|
336
|
-
body.dark-mode .badge.bg-warning {
|
|
337
|
-
background-color: var(--ctp-peach) !important;
|
|
338
|
-
color: var(--ctp-base) !important;
|
|
339
|
-
}
|
|
340
|
-
body.dark-mode .badge.bg-info {
|
|
341
|
-
background-color: var(--ctp-blue) !important;
|
|
342
|
-
color: var(--ctp-base) !important;
|
|
343
|
-
}
|
|
344
|
-
body.dark-mode .badge.bg-secondary {
|
|
345
|
-
background-color: var(--ctp-overlay1) !important;
|
|
346
|
-
}
|
|
347
|
-
body.dark-mode .badge.bg-success {
|
|
348
|
-
background-color: var(--ctp-green) !important;
|
|
349
|
-
color: var(--ctp-base) !important;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/* Links */
|
|
353
|
-
a {
|
|
354
|
-
color: #8B5CF6;
|
|
355
|
-
text-decoration: none;
|
|
356
|
-
}
|
|
357
|
-
a:hover {
|
|
358
|
-
color: #6D28D9;
|
|
359
|
-
text-decoration: underline;
|
|
360
|
-
}
|
|
361
|
-
body.dark-mode a {
|
|
362
|
-
color: var(--ctp-mauve);
|
|
363
|
-
}
|
|
364
|
-
body.dark-mode a:hover {
|
|
365
|
-
color: var(--ctp-pink);
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
/* Buttons */
|
|
369
|
-
body.dark-mode .btn-primary {
|
|
370
|
-
background-color: var(--ctp-mauve);
|
|
371
|
-
border-color: var(--ctp-mauve);
|
|
372
|
-
color: var(--ctp-base);
|
|
373
|
-
}
|
|
374
|
-
body.dark-mode .btn-primary:hover {
|
|
375
|
-
background-color: var(--ctp-pink);
|
|
376
|
-
border-color: var(--ctp-pink);
|
|
377
|
-
}
|
|
378
|
-
body.dark-mode .btn-outline-primary {
|
|
379
|
-
color: var(--ctp-mauve);
|
|
380
|
-
border-color: var(--ctp-mauve);
|
|
381
|
-
}
|
|
382
|
-
body.dark-mode .btn-outline-primary:hover {
|
|
383
|
-
background-color: var(--ctp-mauve);
|
|
384
|
-
color: var(--ctp-base);
|
|
385
|
-
}
|
|
386
|
-
body.dark-mode .btn-secondary,
|
|
387
|
-
body.dark-mode .btn-outline-secondary {
|
|
388
|
-
background-color: var(--ctp-surface1);
|
|
389
|
-
border-color: var(--ctp-surface2);
|
|
390
|
-
color: var(--ctp-text);
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/* Modal dialogs */
|
|
394
|
-
body.dark-mode .modal-content {
|
|
395
|
-
background-color: var(--ctp-surface0);
|
|
396
|
-
color: var(--ctp-text);
|
|
397
|
-
border-color: var(--ctp-surface2);
|
|
398
|
-
}
|
|
399
|
-
body.dark-mode .modal-header {
|
|
400
|
-
background-color: var(--ctp-surface1);
|
|
401
|
-
border-bottom-color: var(--ctp-surface2);
|
|
402
|
-
color: var(--ctp-text);
|
|
403
|
-
}
|
|
404
|
-
body.dark-mode .modal-footer {
|
|
405
|
-
background-color: var(--ctp-surface1);
|
|
406
|
-
border-top-color: var(--ctp-surface2);
|
|
407
|
-
}
|
|
408
|
-
body.dark-mode .modal-title {
|
|
409
|
-
color: var(--ctp-text);
|
|
410
|
-
}
|
|
411
|
-
body.dark-mode .btn-close {
|
|
412
|
-
filter: invert(1);
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
/* Table headers - CRITICAL FIX */
|
|
416
|
-
body.dark-mode thead,
|
|
417
|
-
body.dark-mode thead th {
|
|
418
|
-
background-color: var(--ctp-surface1) !important;
|
|
419
|
-
color: var(--ctp-text) !important;
|
|
420
|
-
border-color: var(--ctp-surface2) !important;
|
|
421
|
-
}
|
|
422
|
-
body.dark-mode tbody tr {
|
|
423
|
-
border-color: var(--ctp-surface2) !important;
|
|
424
|
-
background-color: transparent !important;
|
|
425
|
-
}
|
|
426
|
-
body.dark-mode tbody td {
|
|
427
|
-
border-color: var(--ctp-surface2) !important;
|
|
428
|
-
background-color: transparent !important;
|
|
429
|
-
color: var(--ctp-text) !important;
|
|
430
|
-
}
|
|
431
|
-
body.dark-mode tbody th {
|
|
432
|
-
background-color: var(--ctp-surface0) !important;
|
|
433
|
-
color: var(--ctp-text) !important;
|
|
434
|
-
border-color: var(--ctp-surface2) !important;
|
|
435
|
-
}
|
|
436
|
-
body.dark-mode table {
|
|
437
|
-
color: var(--ctp-text) !important;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
/* Chart canvas backgrounds */
|
|
441
|
-
body.dark-mode canvas {
|
|
442
|
-
background-color: transparent !important;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/* Chart labels and text */
|
|
446
|
-
body.dark-mode .chart-container text {
|
|
447
|
-
fill: var(--ctp-text) !important;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/* Form placeholders */
|
|
451
|
-
body.dark-mode .form-control::placeholder,
|
|
452
|
-
body.dark-mode .form-select::placeholder {
|
|
453
|
-
color: var(--ctp-overlay0);
|
|
454
|
-
opacity: 1;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
/* Dropdown menus */
|
|
458
|
-
.dropdown-menu {
|
|
459
|
-
background-color: white;
|
|
460
|
-
border: 1px solid rgba(0, 0, 0, 0.15);
|
|
461
|
-
}
|
|
462
|
-
.dropdown-item {
|
|
463
|
-
color: #1f2937 !important;
|
|
464
|
-
}
|
|
465
|
-
.dropdown-item:hover,
|
|
466
|
-
.dropdown-item:focus {
|
|
467
|
-
background-color: #f3f4f6;
|
|
468
|
-
color: #8B5CF6 !important;
|
|
469
|
-
}
|
|
470
|
-
.dropdown-item.active {
|
|
471
|
-
background-color: #8B5CF6;
|
|
472
|
-
color: white !important;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
body.dark-mode .dropdown-menu {
|
|
476
|
-
background-color: var(--ctp-surface0);
|
|
477
|
-
border-color: var(--ctp-surface2);
|
|
478
|
-
}
|
|
479
|
-
body.dark-mode .dropdown-item {
|
|
480
|
-
color: var(--ctp-text) !important;
|
|
481
|
-
}
|
|
482
|
-
body.dark-mode .dropdown-item:hover,
|
|
483
|
-
body.dark-mode .dropdown-item:focus {
|
|
484
|
-
background-color: var(--ctp-surface1);
|
|
485
|
-
color: var(--ctp-mauve) !important;
|
|
486
|
-
}
|
|
487
|
-
body.dark-mode .dropdown-item.active {
|
|
488
|
-
background-color: var(--ctp-mauve);
|
|
489
|
-
color: var(--ctp-base) !important;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
/* Pagination */
|
|
493
|
-
body.dark-mode .pagination .page-link {
|
|
494
|
-
background-color: var(--ctp-surface0);
|
|
495
|
-
border-color: var(--ctp-surface2);
|
|
496
|
-
color: var(--ctp-text);
|
|
497
|
-
}
|
|
498
|
-
body.dark-mode .pagination .page-link:hover {
|
|
499
|
-
background-color: var(--ctp-surface1);
|
|
500
|
-
color: var(--ctp-mauve);
|
|
501
|
-
}
|
|
502
|
-
body.dark-mode .pagination .page-item.active .page-link {
|
|
503
|
-
background-color: var(--ctp-mauve);
|
|
504
|
-
border-color: var(--ctp-mauve);
|
|
505
|
-
color: var(--ctp-base);
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/* Progress bars */
|
|
509
|
-
body.dark-mode .progress {
|
|
510
|
-
background-color: var(--ctp-surface1);
|
|
511
|
-
}
|
|
512
|
-
body.dark-mode .progress-bar {
|
|
513
|
-
background-color: var(--ctp-mauve);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
/* Horizontal rules */
|
|
517
|
-
body.dark-mode hr {
|
|
518
|
-
border-color: var(--ctp-surface2);
|
|
519
|
-
opacity: 1;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/* List groups */
|
|
523
|
-
body.dark-mode .list-group-item {
|
|
524
|
-
background-color: var(--ctp-surface0);
|
|
525
|
-
border-color: var(--ctp-surface2);
|
|
526
|
-
color: var(--ctp-text);
|
|
527
|
-
}
|
|
528
|
-
body.dark-mode .list-group-item:hover {
|
|
529
|
-
background-color: var(--ctp-surface1);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
/* Offcanvas (mobile menu) */
|
|
533
|
-
body.dark-mode .offcanvas {
|
|
534
|
-
background-color: var(--ctp-mantle);
|
|
535
|
-
color: var(--ctp-text);
|
|
536
|
-
}
|
|
537
|
-
body.dark-mode .offcanvas-header {
|
|
538
|
-
border-bottom-color: var(--ctp-surface2);
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
/* Small text and labels */
|
|
542
|
-
body.dark-mode small {
|
|
543
|
-
color: var(--ctp-subtext1) !important;
|
|
544
|
-
}
|
|
545
|
-
body.dark-mode label {
|
|
546
|
-
color: var(--ctp-text);
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
/* Breadcrumbs */
|
|
550
|
-
body.dark-mode .breadcrumb {
|
|
551
|
-
background-color: var(--ctp-surface0);
|
|
552
|
-
}
|
|
553
|
-
body.dark-mode .breadcrumb-item {
|
|
554
|
-
color: var(--ctp-text);
|
|
555
|
-
}
|
|
556
|
-
body.dark-mode .breadcrumb-item.active {
|
|
557
|
-
color: var(--ctp-subtext0);
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
/* Tooltips */
|
|
561
|
-
body.dark-mode .tooltip-inner {
|
|
562
|
-
background-color: var(--ctp-surface0);
|
|
563
|
-
color: var(--ctp-text);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/* Checkboxes and radios */
|
|
567
|
-
body.dark-mode .form-check-input {
|
|
568
|
-
background-color: var(--ctp-surface1);
|
|
569
|
-
border-color: var(--ctp-surface2);
|
|
570
|
-
}
|
|
571
|
-
body.dark-mode .form-check-input:checked {
|
|
572
|
-
background-color: var(--ctp-mauve);
|
|
573
|
-
border-color: var(--ctp-mauve);
|
|
574
|
-
}
|
|
575
|
-
body.dark-mode .form-check-label {
|
|
576
|
-
color: var(--ctp-text);
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/* Nav tabs */
|
|
580
|
-
body.dark-mode .nav-tabs {
|
|
581
|
-
border-bottom-color: var(--ctp-surface2);
|
|
582
|
-
}
|
|
583
|
-
body.dark-mode .nav-tabs .nav-link {
|
|
584
|
-
color: var(--ctp-text);
|
|
585
|
-
background-color: transparent;
|
|
586
|
-
border-color: transparent;
|
|
587
|
-
}
|
|
588
|
-
body.dark-mode .nav-tabs .nav-link:hover {
|
|
589
|
-
border-color: var(--ctp-surface2);
|
|
590
|
-
background-color: var(--ctp-surface1);
|
|
591
|
-
}
|
|
592
|
-
body.dark-mode .nav-tabs .nav-link.active {
|
|
593
|
-
background-color: var(--ctp-surface0);
|
|
594
|
-
border-color: var(--ctp-surface2) var(--ctp-surface2) var(--ctp-surface0);
|
|
595
|
-
color: var(--ctp-mauve);
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
/* Collapsible sections - AGGRESSIVE */
|
|
599
|
-
body.dark-mode .accordion-item {
|
|
600
|
-
background-color: var(--ctp-surface0) !important;
|
|
601
|
-
border-color: var(--ctp-surface2) !important;
|
|
602
|
-
}
|
|
603
|
-
body.dark-mode .accordion-button {
|
|
604
|
-
background-color: var(--ctp-surface1) !important;
|
|
605
|
-
color: var(--ctp-text) !important;
|
|
606
|
-
}
|
|
607
|
-
body.dark-mode .accordion-button.bg-light {
|
|
608
|
-
background-color: var(--ctp-surface1) !important;
|
|
609
|
-
}
|
|
610
|
-
body.dark-mode .accordion-button:not(.collapsed) {
|
|
611
|
-
background-color: var(--ctp-surface0) !important;
|
|
612
|
-
color: var(--ctp-mauve) !important;
|
|
613
|
-
}
|
|
614
|
-
body.dark-mode .accordion-button::after {
|
|
615
|
-
filter: invert(1);
|
|
616
|
-
}
|
|
617
|
-
body.dark-mode .accordion-body {
|
|
618
|
-
background-color: var(--ctp-surface0) !important;
|
|
619
|
-
color: var(--ctp-text) !important;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
/* Heatmap specific styling - AGGRESSIVE */
|
|
623
|
-
body.dark-mode .heatmap-cell {
|
|
624
|
-
border-color: var(--ctp-surface2) !important;
|
|
625
|
-
color: var(--ctp-text) !important;
|
|
626
|
-
}
|
|
627
|
-
body.dark-mode .heatmap-hour {
|
|
628
|
-
color: var(--ctp-sky) !important;
|
|
629
|
-
font-weight: 600 !important;
|
|
630
|
-
font-size: 0.75rem !important;
|
|
631
|
-
}
|
|
632
|
-
body.dark-mode .heatmap-count {
|
|
633
|
-
color: var(--ctp-peach) !important;
|
|
634
|
-
font-weight: 600 !important;
|
|
635
|
-
}
|
|
636
|
-
body.dark-mode .heatmap-count.text-white {
|
|
637
|
-
color: var(--ctp-text) !important;
|
|
638
|
-
}
|
|
639
|
-
body.dark-mode .heatmap-count.text-dark {
|
|
640
|
-
color: var(--ctp-peach) !important;
|
|
641
|
-
}
|
|
642
|
-
body.dark-mode .heatmap-grid {
|
|
643
|
-
background-color: var(--ctp-surface0);
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/* Definition lists (dl, dt, dd) - used in Request Context */
|
|
647
|
-
body.dark-mode dl {
|
|
648
|
-
color: var(--ctp-text);
|
|
649
|
-
}
|
|
650
|
-
body.dark-mode dt {
|
|
651
|
-
color: var(--ctp-subtext1);
|
|
652
|
-
font-weight: 600;
|
|
653
|
-
}
|
|
654
|
-
body.dark-mode dd {
|
|
655
|
-
color: var(--ctp-text);
|
|
656
|
-
background-color: var(--ctp-surface0);
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
/* Override any remaining white backgrounds */
|
|
660
|
-
body.dark-mode div[style*="background-color: white"],
|
|
661
|
-
body.dark-mode div[style*="background-color: #fff"],
|
|
662
|
-
body.dark-mode div[style*="background-color:#fff"],
|
|
663
|
-
body.dark-mode div[style*="background: white"],
|
|
664
|
-
body.dark-mode div[style*="background: #fff"] {
|
|
665
|
-
background-color: var(--ctp-surface0) !important;
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
/* Chart.js specific fixes for axis labels */
|
|
669
|
-
body.dark-mode .chartjs-render-monitor {
|
|
670
|
-
background-color: transparent !important;
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
/* Make sure all headings are visible */
|
|
674
|
-
body.dark-mode h1, body.dark-mode h2, body.dark-mode h3,
|
|
675
|
-
body.dark-mode h4, body.dark-mode h5, body.dark-mode h6 {
|
|
676
|
-
color: var(--ctp-text);
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/* Stronger text colors for better visibility */
|
|
680
|
-
body.dark-mode .text-secondary {
|
|
681
|
-
color: var(--ctp-subtext1) !important;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
/* SVG text elements (for charts) - MORE AGGRESSIVE */
|
|
685
|
-
body.dark-mode svg text {
|
|
686
|
-
fill: var(--ctp-text) !important;
|
|
687
|
-
}
|
|
688
|
-
body.dark-mode svg .domain,
|
|
689
|
-
body.dark-mode svg .tick line {
|
|
690
|
-
stroke: var(--ctp-surface2) !important;
|
|
691
|
-
}
|
|
692
|
-
body.dark-mode svg tspan {
|
|
693
|
-
fill: var(--ctp-text) !important;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
/* Details/Summary (collapsible sections) */
|
|
697
|
-
body.dark-mode details {
|
|
698
|
-
background-color: var(--ctp-surface0) !important;
|
|
699
|
-
border-color: var(--ctp-surface2) !important;
|
|
700
|
-
}
|
|
701
|
-
body.dark-mode summary {
|
|
702
|
-
background-color: var(--ctp-surface0) !important;
|
|
703
|
-
color: var(--ctp-text) !important;
|
|
704
|
-
border-color: var(--ctp-surface2) !important;
|
|
705
|
-
}
|
|
706
|
-
body.dark-mode details[open] summary {
|
|
707
|
-
background-color: var(--ctp-surface1) !important;
|
|
708
|
-
border-bottom-color: var(--ctp-surface2) !important;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/* Button-like summary elements */
|
|
712
|
-
body.dark-mode .btn.collapsed,
|
|
713
|
-
body.dark-mode [data-bs-toggle="collapse"] {
|
|
714
|
-
background-color: var(--ctp-surface0) !important;
|
|
715
|
-
color: var(--ctp-text) !important;
|
|
716
|
-
border-color: var(--ctp-surface2) !important;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
/* Specific override for white backgrounds in backtrace sections */
|
|
720
|
-
body.dark-mode .card .card-body > div[style*="background"],
|
|
721
|
-
body.dark-mode .card-body > button[style*="background"] {
|
|
722
|
-
background-color: var(--ctp-surface0) !important;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
/* Error header/banner */
|
|
726
|
-
body.dark-mode .alert-danger {
|
|
727
|
-
background-color: rgba(243, 139, 168, 0.2) !important;
|
|
728
|
-
border-color: var(--ctp-red) !important;
|
|
729
|
-
color: var(--ctp-text) !important;
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/* Chartkick specific - force text visibility */
|
|
733
|
-
body.dark-mode #chart-1 text,
|
|
734
|
-
body.dark-mode [id^="chart-"] text {
|
|
735
|
-
fill: var(--ctp-text) !important;
|
|
736
|
-
color: var(--ctp-text) !important;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/* Force all text in charts to be visible */
|
|
740
|
-
body.dark-mode canvas + div text,
|
|
741
|
-
body.dark-mode .chartjs-size-monitor text {
|
|
742
|
-
color: var(--ctp-text) !important;
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/* ULTRA AGGRESSIVE - Chart.js axis labels and titles */
|
|
746
|
-
body.dark-mode canvas {
|
|
747
|
-
color: var(--ctp-text) !important;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
/* Target Google Charts (alternative library) */
|
|
751
|
-
body.dark-mode svg > g > g > text,
|
|
752
|
-
body.dark-mode svg g text {
|
|
753
|
-
fill: var(--ctp-text) !important;
|
|
754
|
-
font-size: 12px !important;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/* Chart.js specific selectors - NUCLEAR OPTION */
|
|
758
|
-
body.dark-mode .chartjs-render-monitor + div text,
|
|
759
|
-
body.dark-mode [class*="chart"] text,
|
|
760
|
-
body.dark-mode div[id*="chart"] text {
|
|
761
|
-
fill: var(--ctp-text) !important;
|
|
762
|
-
color: var(--ctp-text) !important;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
/* Ensure axis tick labels are visible */
|
|
766
|
-
body.dark-mode g.tick text,
|
|
767
|
-
body.dark-mode .tick text,
|
|
768
|
-
body.dark-mode text.highcharts-axis-title,
|
|
769
|
-
body.dark-mode .highcharts-axis-labels text {
|
|
770
|
-
fill: var(--ctp-text) !important;
|
|
771
|
-
color: var(--ctp-text) !important;
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
/* Force visibility on ALL text elements inside chart containers */
|
|
775
|
-
body.dark-mode .card-body text,
|
|
776
|
-
body.dark-mode .card text {
|
|
777
|
-
fill: var(--ctp-text) !important;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/* Toast notifications */
|
|
781
|
-
.toast {
|
|
782
|
-
min-width: 250px;
|
|
783
|
-
}
|
|
784
|
-
.toast-container {
|
|
785
|
-
max-width: 350px;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
/* Filter pills */
|
|
789
|
-
.filter-pill {
|
|
790
|
-
display: inline-flex;
|
|
791
|
-
align-items: center;
|
|
792
|
-
padding: 0.5rem 0.75rem;
|
|
793
|
-
font-size: 0.875rem;
|
|
794
|
-
border-radius: 0.375rem;
|
|
795
|
-
transition: all 0.2s ease;
|
|
796
|
-
}
|
|
797
|
-
.filter-pill:hover {
|
|
798
|
-
opacity: 0.85;
|
|
799
|
-
transform: translateY(-1px);
|
|
800
|
-
}
|
|
801
|
-
.filter-pill .bi-x {
|
|
802
|
-
font-size: 1.1rem;
|
|
803
|
-
font-weight: bold;
|
|
804
|
-
}
|
|
805
|
-
body.dark-mode .filter-pill.bg-primary {
|
|
806
|
-
background-color: var(--ctp-mauve) !important;
|
|
807
|
-
}
|
|
808
|
-
body.dark-mode .filter-pill.bg-secondary {
|
|
809
|
-
background-color: var(--ctp-surface2) !important;
|
|
810
|
-
}
|
|
811
|
-
</style>
|
|
23
|
+
<!-- Syntax Highlighting for Source Code Viewer -->
|
|
24
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@catppuccin/highlightjs@1.0.0/css/catppuccin-mocha.min.css">
|
|
25
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlightjs-line-numbers.js@2.8.0/dist/styles.min.css">
|
|
26
|
+
<script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.9.0/build/highlight.min.js"></script>
|
|
27
|
+
<script src="https://cdn.jsdelivr.net/npm/highlightjs-line-numbers.js@2.8.0/dist/highlightjs-line-numbers.min.js"></script>
|
|
28
|
+
|
|
29
|
+
<!-- Dashboard CSS -->
|
|
30
|
+
<link rel="stylesheet" href="/rails_error_dashboard/css/dashboard.css">
|
|
812
31
|
</head>
|
|
813
32
|
|
|
814
33
|
<body>
|
|
@@ -821,9 +40,14 @@
|
|
|
821
40
|
<nav class="navbar navbar-dark">
|
|
822
41
|
<div class="container-fluid">
|
|
823
42
|
<div class="d-flex align-items-center">
|
|
43
|
+
<!-- Mobile menu toggle -->
|
|
824
44
|
<button class="btn btn-link text-white d-md-none me-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu">
|
|
825
45
|
<i class="bi bi-list fs-4"></i>
|
|
826
46
|
</button>
|
|
47
|
+
<!-- Desktop sidebar toggle -->
|
|
48
|
+
<button class="btn btn-link text-white d-none d-md-block me-2" type="button" id="sidebarToggle" title="Toggle sidebar">
|
|
49
|
+
<i class="bi bi-layout-sidebar-inset fs-5"></i>
|
|
50
|
+
</button>
|
|
827
51
|
<%
|
|
828
52
|
# Check if main app has a root route defined
|
|
829
53
|
begin
|
|
@@ -900,7 +124,7 @@
|
|
|
900
124
|
<div class="container-fluid">
|
|
901
125
|
<div class="row">
|
|
902
126
|
<!-- Sidebar -->
|
|
903
|
-
<nav class="col-md-2 d-none d-md-block sidebar">
|
|
127
|
+
<nav class="col-md-2 d-none d-md-block sidebar" id="sidebar">
|
|
904
128
|
<div class="position-sticky pt-3">
|
|
905
129
|
<%
|
|
906
130
|
# Preserve application_id across navigation
|
|
@@ -913,7 +137,7 @@
|
|
|
913
137
|
<% end %>
|
|
914
138
|
</li>
|
|
915
139
|
<li class="nav-item">
|
|
916
|
-
<%= link_to errors_path(nav_params), class: "nav-link #{
|
|
140
|
+
<%= link_to errors_path(nav_params), class: "nav-link #{(controller_name == 'errors' && action_name == 'index') ? 'active' : ''}" do %>
|
|
917
141
|
<i class="bi bi-list-ul"></i> All Errors
|
|
918
142
|
<% end %>
|
|
919
143
|
</li>
|
|
@@ -922,6 +146,16 @@
|
|
|
922
146
|
<i class="bi bi-graph-up"></i> Analytics
|
|
923
147
|
<% end %>
|
|
924
148
|
</li>
|
|
149
|
+
<li class="nav-item">
|
|
150
|
+
<%= link_to platform_comparison_errors_path(nav_params), class: "nav-link #{request.path == platform_comparison_errors_path ? 'active' : ''}" do %>
|
|
151
|
+
<i class="bi bi-heart-pulse"></i> Platform Health
|
|
152
|
+
<% end %>
|
|
153
|
+
</li>
|
|
154
|
+
<li class="nav-item">
|
|
155
|
+
<%= link_to correlation_errors_path(nav_params), class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}" do %>
|
|
156
|
+
<i class="bi bi-diagram-3"></i> Correlation
|
|
157
|
+
<% end %>
|
|
158
|
+
</li>
|
|
925
159
|
<li class="nav-item">
|
|
926
160
|
<%= link_to settings_path(nav_params), class: "nav-link #{request.path == settings_path ? 'active' : ''}" do %>
|
|
927
161
|
<i class="bi bi-gear"></i> Settings
|
|
@@ -947,13 +181,28 @@
|
|
|
947
181
|
<% end %>
|
|
948
182
|
</li>
|
|
949
183
|
<li class="nav-item">
|
|
950
|
-
<%= link_to errors_path(nav_params.merge(platform: 'iOS')), class: "nav-link" do %>
|
|
951
|
-
<i class="bi bi-
|
|
184
|
+
<%= link_to errors_path(nav_params.merge(platform: 'iOS')), class: "nav-link #{params[:platform] == 'iOS' ? 'active' : ''}" do %>
|
|
185
|
+
<i class="bi bi-apple"></i> iOS Errors
|
|
186
|
+
<% end %>
|
|
187
|
+
</li>
|
|
188
|
+
<li class="nav-item">
|
|
189
|
+
<%= link_to errors_path(nav_params.merge(platform: 'Android')), class: "nav-link #{params[:platform] == 'Android' ? 'active' : ''}" do %>
|
|
190
|
+
<i class="bi bi-android2"></i> Android Errors
|
|
191
|
+
<% end %>
|
|
192
|
+
</li>
|
|
193
|
+
<li class="nav-item">
|
|
194
|
+
<%= link_to errors_path(nav_params.merge(platform: 'Web')), class: "nav-link #{params[:platform] == 'Web' ? 'active' : ''}" do %>
|
|
195
|
+
<i class="bi bi-globe"></i> Web Errors
|
|
196
|
+
<% end %>
|
|
197
|
+
</li>
|
|
198
|
+
<li class="nav-item">
|
|
199
|
+
<%= link_to errors_path(nav_params.merge(platform: 'API')), class: "nav-link #{params[:platform] == 'API' ? 'active' : ''}" do %>
|
|
200
|
+
<i class="bi bi-server"></i> API Errors
|
|
952
201
|
<% end %>
|
|
953
202
|
</li>
|
|
954
203
|
<li class="nav-item">
|
|
955
|
-
<%= link_to errors_path(nav_params.merge(platform: '
|
|
956
|
-
<i class="bi bi-
|
|
204
|
+
<%= link_to errors_path(nav_params.merge(platform: 'Background Jobs')), class: "nav-link #{params[:platform] == 'Background Jobs' ? 'active' : ''}" do %>
|
|
205
|
+
<i class="bi bi-gear-fill"></i> Background Jobs
|
|
957
206
|
<% end %>
|
|
958
207
|
</li>
|
|
959
208
|
</ul>
|
|
@@ -961,7 +210,7 @@
|
|
|
961
210
|
</nav>
|
|
962
211
|
|
|
963
212
|
<!-- Main content -->
|
|
964
|
-
<main class="col-md-10 ms-sm-auto px-md-4">
|
|
213
|
+
<main class="col-md-10 ms-sm-auto px-md-4" id="mainContent">
|
|
965
214
|
<%= yield %>
|
|
966
215
|
</main>
|
|
967
216
|
</div>
|
|
@@ -991,6 +240,10 @@
|
|
|
991
240
|
<span><i class="bi bi-graph-up text-primary"></i> Go to analytics</span>
|
|
992
241
|
<kbd class="bg-secondary text-white px-2 py-1 rounded">A</kbd>
|
|
993
242
|
</div>
|
|
243
|
+
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
244
|
+
<span><i class="bi bi-layout-sidebar text-primary"></i> Toggle sidebar</span>
|
|
245
|
+
<kbd class="bg-secondary text-white px-2 py-1 rounded">S</kbd>
|
|
246
|
+
</div>
|
|
994
247
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
995
248
|
<span><i class="bi bi-question-circle text-primary"></i> Show this help</span>
|
|
996
249
|
<kbd class="bg-secondary text-white px-2 py-1 rounded">?</kbd>
|
|
@@ -1020,449 +273,30 @@
|
|
|
1020
273
|
<!-- Bootstrap JS -->
|
|
1021
274
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
1022
275
|
|
|
1023
|
-
<!-- Pure JavaScript Theme Toggle -->
|
|
1024
|
-
<script>
|
|
1025
|
-
// Load saved theme on page load (before DOMContentLoaded to prevent flash)
|
|
1026
|
-
(function() {
|
|
1027
|
-
const savedTheme = localStorage.getItem('theme');
|
|
1028
|
-
if (savedTheme === 'dark') {
|
|
1029
|
-
document.body.classList.add('dark-mode');
|
|
1030
|
-
}
|
|
1031
|
-
})();
|
|
1032
|
-
|
|
1033
|
-
// Theme toggle after DOM loads
|
|
1034
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
1035
|
-
const themeToggle = document.getElementById('themeToggle');
|
|
1036
|
-
const themeIcon = document.getElementById('themeIcon');
|
|
1037
|
-
|
|
1038
|
-
// Update icon based on current theme
|
|
1039
|
-
function updateIcon() {
|
|
1040
|
-
if (document.body.classList.contains('dark-mode')) {
|
|
1041
|
-
themeIcon.className = 'bi bi-sun-fill';
|
|
1042
|
-
} else {
|
|
1043
|
-
themeIcon.className = 'bi bi-moon-fill';
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// Set initial icon
|
|
1048
|
-
updateIcon();
|
|
1049
|
-
|
|
1050
|
-
// Toggle theme on button click
|
|
1051
|
-
themeToggle.addEventListener('click', function() {
|
|
1052
|
-
console.log('🎨 Theme toggle clicked');
|
|
1053
|
-
|
|
1054
|
-
document.body.classList.toggle('dark-mode');
|
|
1055
|
-
const isDark = document.body.classList.contains('dark-mode');
|
|
1056
|
-
|
|
1057
|
-
console.log('Dark mode:', isDark);
|
|
1058
|
-
|
|
1059
|
-
// Save preference
|
|
1060
|
-
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
1061
|
-
console.log('💾 Saved to localStorage:', isDark ? 'dark' : 'light');
|
|
1062
|
-
|
|
1063
|
-
// Update icon
|
|
1064
|
-
updateIcon();
|
|
1065
|
-
console.log('✅ Theme toggled successfully');
|
|
1066
|
-
|
|
1067
|
-
// Reapply chart theme
|
|
1068
|
-
applyChartTheme();
|
|
1069
|
-
|
|
1070
|
-
// Reload page to update charts properly
|
|
1071
|
-
setTimeout(() => location.reload(), 300);
|
|
1072
|
-
});
|
|
1073
|
-
|
|
1074
|
-
// Chart.js theme colors - ULTRA AGGRESSIVE setup
|
|
1075
|
-
function applyChartTheme() {
|
|
1076
|
-
if (typeof Chart !== 'undefined') {
|
|
1077
|
-
const isDark = document.body.classList.contains('dark-mode');
|
|
1078
|
-
const textColor = isDark ? '#cdd6f4' : '#1f2937';
|
|
1079
|
-
const gridColor = isDark ? 'rgba(88, 91, 112, 0.2)' : 'rgba(0, 0, 0, 0.1)';
|
|
1080
|
-
|
|
1081
|
-
console.log('📊 Setting Chart.js theme:', isDark ? 'DARK' : 'light', '| Text:', textColor);
|
|
1082
|
-
|
|
1083
|
-
// Global defaults
|
|
1084
|
-
Chart.defaults.color = textColor;
|
|
1085
|
-
Chart.defaults.borderColor = gridColor;
|
|
1086
|
-
Chart.defaults.font = Chart.defaults.font || {};
|
|
1087
|
-
Chart.defaults.font.color = textColor;
|
|
1088
|
-
|
|
1089
|
-
// Scale defaults (axes) - AGGRESSIVE
|
|
1090
|
-
if (Chart.defaults.scale) {
|
|
1091
|
-
Chart.defaults.scale.ticks = Chart.defaults.scale.ticks || {};
|
|
1092
|
-
Chart.defaults.scale.ticks.color = textColor;
|
|
1093
|
-
Chart.defaults.scale.ticks.font = Chart.defaults.scale.ticks.font || {};
|
|
1094
|
-
Chart.defaults.scale.ticks.font.color = textColor;
|
|
1095
|
-
|
|
1096
|
-
Chart.defaults.scale.grid = Chart.defaults.scale.grid || {};
|
|
1097
|
-
Chart.defaults.scale.grid.color = gridColor;
|
|
1098
|
-
|
|
1099
|
-
// Axis title (xtitle, ytitle)
|
|
1100
|
-
Chart.defaults.scale.title = Chart.defaults.scale.title || {};
|
|
1101
|
-
Chart.defaults.scale.title.color = textColor;
|
|
1102
|
-
Chart.defaults.scale.title.font = Chart.defaults.scale.title.font || {};
|
|
1103
|
-
Chart.defaults.scale.title.font.size = 14;
|
|
1104
|
-
}
|
|
1105
|
-
|
|
1106
|
-
// X and Y axis specific
|
|
1107
|
-
if (Chart.defaults.scales) {
|
|
1108
|
-
// X axis
|
|
1109
|
-
Chart.defaults.scales.x = Chart.defaults.scales.x || {};
|
|
1110
|
-
Chart.defaults.scales.x.ticks = Chart.defaults.scales.x.ticks || {};
|
|
1111
|
-
Chart.defaults.scales.x.ticks.color = textColor;
|
|
1112
|
-
Chart.defaults.scales.x.title = Chart.defaults.scales.x.title || {};
|
|
1113
|
-
Chart.defaults.scales.x.title.color = textColor;
|
|
1114
|
-
Chart.defaults.scales.x.grid = Chart.defaults.scales.x.grid || {};
|
|
1115
|
-
Chart.defaults.scales.x.grid.color = gridColor;
|
|
1116
|
-
|
|
1117
|
-
// Y axis
|
|
1118
|
-
Chart.defaults.scales.y = Chart.defaults.scales.y || {};
|
|
1119
|
-
Chart.defaults.scales.y.ticks = Chart.defaults.scales.y.ticks || {};
|
|
1120
|
-
Chart.defaults.scales.y.ticks.color = textColor;
|
|
1121
|
-
Chart.defaults.scales.y.title = Chart.defaults.scales.y.title || {};
|
|
1122
|
-
Chart.defaults.scales.y.title.color = textColor;
|
|
1123
|
-
Chart.defaults.scales.y.grid = Chart.defaults.scales.y.grid || {};
|
|
1124
|
-
Chart.defaults.scales.y.grid.color = gridColor;
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
// Plugin defaults
|
|
1128
|
-
if (Chart.defaults.plugins) {
|
|
1129
|
-
// Legend
|
|
1130
|
-
if (Chart.defaults.plugins.legend) {
|
|
1131
|
-
Chart.defaults.plugins.legend.labels = Chart.defaults.plugins.legend.labels || {};
|
|
1132
|
-
Chart.defaults.plugins.legend.labels.color = textColor;
|
|
1133
|
-
Chart.defaults.plugins.legend.labels.font = Chart.defaults.plugins.legend.labels.font || {};
|
|
1134
|
-
Chart.defaults.plugins.legend.labels.font.color = textColor;
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
// Tooltip
|
|
1138
|
-
if (Chart.defaults.plugins.tooltip) {
|
|
1139
|
-
Chart.defaults.plugins.tooltip.backgroundColor = isDark ? '#313244' : 'rgba(255, 255, 255, 0.95)';
|
|
1140
|
-
Chart.defaults.plugins.tooltip.titleColor = isDark ? textColor : '#1f2937';
|
|
1141
|
-
Chart.defaults.plugins.tooltip.bodyColor = isDark ? textColor : '#1f2937';
|
|
1142
|
-
Chart.defaults.plugins.tooltip.borderColor = isDark ? '#585b70' : 'rgba(0, 0, 0, 0.2)';
|
|
1143
|
-
Chart.defaults.plugins.tooltip.borderWidth = 1;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
|
-
// Title plugin
|
|
1147
|
-
if (Chart.defaults.plugins.title) {
|
|
1148
|
-
Chart.defaults.plugins.title.color = textColor;
|
|
1149
|
-
Chart.defaults.plugins.title.font = Chart.defaults.plugins.title.font || {};
|
|
1150
|
-
Chart.defaults.plugins.title.font.color = textColor;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
console.log('✅ Chart.js theme applied - all text should be:', textColor);
|
|
1155
|
-
} else {
|
|
1156
|
-
console.warn('⚠️ Chart.js not loaded yet');
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// Also update Google Charts if present
|
|
1160
|
-
if (typeof google !== 'undefined' && google.visualization) {
|
|
1161
|
-
console.log('📊 Google Charts detected - applying theme');
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
// Apply chart theme on load
|
|
1166
|
-
applyChartTheme();
|
|
1167
|
-
|
|
1168
|
-
// NUCLEAR OPTION: Watch for chart creation and force colors
|
|
1169
|
-
const observer = new MutationObserver(function(mutations) {
|
|
1170
|
-
mutations.forEach(function(mutation) {
|
|
1171
|
-
mutation.addedNodes.forEach(function(node) {
|
|
1172
|
-
if (node.tagName === 'CANVAS') {
|
|
1173
|
-
console.log('🎨 New chart detected, forcing theme...');
|
|
1174
|
-
setTimeout(applyChartTheme, 100);
|
|
1175
|
-
}
|
|
1176
|
-
});
|
|
1177
|
-
});
|
|
1178
|
-
});
|
|
1179
|
-
|
|
1180
|
-
observer.observe(document.body, {
|
|
1181
|
-
childList: true,
|
|
1182
|
-
subtree: true
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
// Also listen for Chartkick chart creation events
|
|
1186
|
-
document.addEventListener('chartkick:load', function() {
|
|
1187
|
-
console.log('📊 Chartkick loaded, applying theme');
|
|
1188
|
-
applyChartTheme();
|
|
1189
|
-
});
|
|
1190
|
-
|
|
1191
|
-
// Force reapply every 500ms for the first 3 seconds (in case charts load late)
|
|
1192
|
-
let attempts = 0;
|
|
1193
|
-
const forceInterval = setInterval(function() {
|
|
1194
|
-
attempts++;
|
|
1195
|
-
applyChartTheme();
|
|
1196
|
-
console.log('🔄 Force applying theme (attempt', attempts, ')');
|
|
1197
|
-
if (attempts >= 6) {
|
|
1198
|
-
clearInterval(forceInterval);
|
|
1199
|
-
console.log('✅ Stopped force applying');
|
|
1200
|
-
}
|
|
1201
|
-
}, 500);
|
|
1202
|
-
|
|
1203
|
-
// Copy to clipboard functionality
|
|
1204
|
-
window.copyToClipboard = function(text, button) {
|
|
1205
|
-
navigator.clipboard.writeText(text).then(function() {
|
|
1206
|
-
// Store original button HTML
|
|
1207
|
-
const originalHTML = button.innerHTML;
|
|
1208
|
-
|
|
1209
|
-
// Show success state on button
|
|
1210
|
-
button.innerHTML = '<i class="bi bi-check"></i> Copied!';
|
|
1211
|
-
button.classList.remove('btn-outline-secondary');
|
|
1212
|
-
button.classList.add('btn-success');
|
|
1213
|
-
|
|
1214
|
-
// Show toast notification
|
|
1215
|
-
showToast('Copied to clipboard!', 'success');
|
|
1216
|
-
|
|
1217
|
-
// Reset button after 2 seconds
|
|
1218
|
-
setTimeout(function() {
|
|
1219
|
-
button.innerHTML = originalHTML;
|
|
1220
|
-
button.classList.remove('btn-success');
|
|
1221
|
-
button.classList.add('btn-outline-secondary');
|
|
1222
|
-
}, 2000);
|
|
1223
|
-
}).catch(function(err) {
|
|
1224
|
-
console.error('Failed to copy:', err);
|
|
1225
|
-
button.innerHTML = '<i class="bi bi-x"></i> Failed';
|
|
1226
|
-
showToast('Failed to copy to clipboard', 'danger');
|
|
1227
|
-
});
|
|
1228
|
-
};
|
|
1229
|
-
|
|
1230
|
-
// Toast notification functionality
|
|
1231
|
-
window.showToast = function(message, type) {
|
|
1232
|
-
type = type || 'success';
|
|
1233
|
-
|
|
1234
|
-
const toastId = 'toast-' + Date.now();
|
|
1235
|
-
const iconClass = type === 'success' ? 'bi-check-circle-fill' :
|
|
1236
|
-
type === 'danger' ? 'bi-exclamation-circle-fill' :
|
|
1237
|
-
'bi-info-circle-fill';
|
|
1238
|
-
const bgClass = type === 'success' ? 'bg-success' :
|
|
1239
|
-
type === 'danger' ? 'bg-danger' :
|
|
1240
|
-
'bg-info';
|
|
1241
|
-
|
|
1242
|
-
const toastHTML = `
|
|
1243
|
-
<div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
|
1244
|
-
<div class="d-flex">
|
|
1245
|
-
<div class="toast-body">
|
|
1246
|
-
<i class="bi ${iconClass} me-2"></i>
|
|
1247
|
-
${message}
|
|
1248
|
-
</div>
|
|
1249
|
-
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
1250
|
-
</div>
|
|
1251
|
-
</div>
|
|
1252
|
-
`;
|
|
1253
|
-
|
|
1254
|
-
const container = document.querySelector('.toast-container');
|
|
1255
|
-
container.insertAdjacentHTML('beforeend', toastHTML);
|
|
1256
|
-
|
|
1257
|
-
const toastElement = document.getElementById(toastId);
|
|
1258
|
-
const toast = new bootstrap.Toast(toastElement, { delay: 4000 });
|
|
1259
|
-
toast.show();
|
|
1260
|
-
|
|
1261
|
-
// Remove toast element after it's hidden
|
|
1262
|
-
toastElement.addEventListener('hidden.bs.toast', function() {
|
|
1263
|
-
toastElement.remove();
|
|
1264
|
-
});
|
|
1265
|
-
};
|
|
1266
276
|
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
showToast('<%= j flash[:error] %>', 'danger');
|
|
1280
|
-
<% end %>
|
|
277
|
+
<!-- Dashboard JavaScript -->
|
|
278
|
+
<script src="/rails_error_dashboard/js/syntax-highlighting.js"></script>
|
|
279
|
+
<script src="/rails_error_dashboard/js/theme-toggle.js"></script>
|
|
280
|
+
<script src="/rails_error_dashboard/js/utilities.js"></script>
|
|
281
|
+
<script src="/rails_error_dashboard/js/sidebar-toggle.js"></script>
|
|
282
|
+
|
|
283
|
+
<!-- Flash Messages -->
|
|
284
|
+
<script>
|
|
285
|
+
// Show flash messages as toasts
|
|
286
|
+
<% if defined?(flash) && flash.present? %>
|
|
287
|
+
<% if flash[:notice] %>
|
|
288
|
+
showToast('<%= j flash[:notice] %>', 'success');
|
|
1281
289
|
<% end %>
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
try {
|
|
1294
|
-
const date = new Date(utcString);
|
|
1295
|
-
if (isNaN(date.getTime())) return; // Invalid date
|
|
1296
|
-
|
|
1297
|
-
// Parse the format string and convert to local time
|
|
1298
|
-
const formatted = formatDateTime(date, formatString);
|
|
1299
|
-
|
|
1300
|
-
// Get timezone abbreviation
|
|
1301
|
-
const timezone = getTimezoneAbbreviation(date);
|
|
1302
|
-
|
|
1303
|
-
// Update element text
|
|
1304
|
-
element.textContent = formatted + ' ' + timezone;
|
|
1305
|
-
element.title = 'Your local time (click to see UTC)';
|
|
1306
|
-
|
|
1307
|
-
// Add click handler to toggle between local and UTC
|
|
1308
|
-
element.style.cursor = 'pointer';
|
|
1309
|
-
element.dataset.originalUtc = utcString;
|
|
1310
|
-
element.dataset.localFormatted = formatted + ' ' + timezone;
|
|
1311
|
-
element.dataset.showingLocal = 'true';
|
|
1312
|
-
|
|
1313
|
-
element.addEventListener('click', function() {
|
|
1314
|
-
if (this.dataset.showingLocal === 'true') {
|
|
1315
|
-
// Show UTC
|
|
1316
|
-
const utcDate = new Date(this.dataset.originalUtc);
|
|
1317
|
-
const utcFormatted = formatDateTime(utcDate, formatString);
|
|
1318
|
-
this.textContent = utcFormatted + ' UTC';
|
|
1319
|
-
this.title = 'UTC time (click to see local time)';
|
|
1320
|
-
this.dataset.showingLocal = 'false';
|
|
1321
|
-
} else {
|
|
1322
|
-
// Show local
|
|
1323
|
-
this.textContent = this.dataset.localFormatted;
|
|
1324
|
-
this.title = 'Your local time (click to see UTC)';
|
|
1325
|
-
this.dataset.showingLocal = 'true';
|
|
1326
|
-
}
|
|
1327
|
-
});
|
|
1328
|
-
} catch (e) {
|
|
1329
|
-
console.error('Error converting timestamp:', e);
|
|
1330
|
-
}
|
|
1331
|
-
});
|
|
1332
|
-
|
|
1333
|
-
// Convert all .local-time-ago elements (relative time)
|
|
1334
|
-
document.querySelectorAll('.local-time-ago').forEach(function(element) {
|
|
1335
|
-
const utcString = element.dataset.utc;
|
|
1336
|
-
if (!utcString) return;
|
|
1337
|
-
|
|
1338
|
-
try {
|
|
1339
|
-
const date = new Date(utcString);
|
|
1340
|
-
if (isNaN(date.getTime())) return; // Invalid date
|
|
1341
|
-
|
|
1342
|
-
// Calculate relative time
|
|
1343
|
-
const now = new Date();
|
|
1344
|
-
const diffMs = now - date;
|
|
1345
|
-
const formatted = formatRelativeTime(diffMs);
|
|
1346
|
-
|
|
1347
|
-
// Update element text
|
|
1348
|
-
element.textContent = formatted;
|
|
1349
|
-
element.title = 'Click to see exact time';
|
|
1350
|
-
|
|
1351
|
-
// Add click handler to toggle between relative and absolute
|
|
1352
|
-
element.style.cursor = 'pointer';
|
|
1353
|
-
element.dataset.originalUtc = utcString;
|
|
1354
|
-
element.dataset.showingRelative = 'true';
|
|
1355
|
-
|
|
1356
|
-
element.addEventListener('click', function() {
|
|
1357
|
-
if (this.dataset.showingRelative === 'true') {
|
|
1358
|
-
// Show absolute time
|
|
1359
|
-
const absoluteDate = new Date(this.dataset.originalUtc);
|
|
1360
|
-
const absoluteFormatted = formatDateTime(absoluteDate, '%B %d, %Y %I:%M:%S %p');
|
|
1361
|
-
const timezone = getTimezoneAbbreviation(absoluteDate);
|
|
1362
|
-
this.textContent = absoluteFormatted + ' ' + timezone;
|
|
1363
|
-
this.title = 'Click to see relative time';
|
|
1364
|
-
this.dataset.showingRelative = 'false';
|
|
1365
|
-
} else {
|
|
1366
|
-
// Show relative time
|
|
1367
|
-
const now = new Date();
|
|
1368
|
-
const date = new Date(this.dataset.originalUtc);
|
|
1369
|
-
const diffMs = now - date;
|
|
1370
|
-
this.textContent = formatRelativeTime(diffMs);
|
|
1371
|
-
this.title = 'Click to see exact time';
|
|
1372
|
-
this.dataset.showingRelative = 'true';
|
|
1373
|
-
}
|
|
1374
|
-
});
|
|
1375
|
-
} catch (e) {
|
|
1376
|
-
console.error('Error converting relative time:', e);
|
|
1377
|
-
}
|
|
1378
|
-
});
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
// Format date according to strftime-like format string
|
|
1382
|
-
function formatDateTime(date, formatString) {
|
|
1383
|
-
const months = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
1384
|
-
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
1385
|
-
const monthsShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
1386
|
-
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
1387
|
-
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
1388
|
-
const daysShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1389
|
-
|
|
1390
|
-
const year = date.getFullYear();
|
|
1391
|
-
const month = date.getMonth();
|
|
1392
|
-
const day = date.getDate();
|
|
1393
|
-
const hours = date.getHours();
|
|
1394
|
-
const minutes = date.getMinutes();
|
|
1395
|
-
const seconds = date.getSeconds();
|
|
1396
|
-
const dayOfWeek = date.getDay();
|
|
1397
|
-
|
|
1398
|
-
// 12-hour format
|
|
1399
|
-
const hours12 = hours % 12 || 12;
|
|
1400
|
-
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
1401
|
-
|
|
1402
|
-
// Padding helper
|
|
1403
|
-
const pad = (n) => n.toString().padStart(2, '0');
|
|
1404
|
-
|
|
1405
|
-
// Replace format specifiers
|
|
1406
|
-
let result = formatString
|
|
1407
|
-
.replace('%Y', year)
|
|
1408
|
-
.replace('%y', year.toString().substr(2))
|
|
1409
|
-
.replace('%B', months[month])
|
|
1410
|
-
.replace('%b', monthsShort[month])
|
|
1411
|
-
.replace('%m', pad(month + 1))
|
|
1412
|
-
.replace('%d', pad(day))
|
|
1413
|
-
.replace('%e', day)
|
|
1414
|
-
.replace('%A', days[dayOfWeek])
|
|
1415
|
-
.replace('%a', daysShort[dayOfWeek])
|
|
1416
|
-
.replace('%H', pad(hours))
|
|
1417
|
-
.replace('%I', pad(hours12))
|
|
1418
|
-
.replace('%M', pad(minutes))
|
|
1419
|
-
.replace('%S', pad(seconds))
|
|
1420
|
-
.replace('%p', ampm)
|
|
1421
|
-
.replace('%P', ampm.toLowerCase());
|
|
1422
|
-
|
|
1423
|
-
return result;
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
// Get timezone abbreviation (e.g., "PST", "EST", "UTC+2")
|
|
1427
|
-
function getTimezoneAbbreviation(date) {
|
|
1428
|
-
const timeZoneString = date.toLocaleTimeString('en-US', { timeZoneName: 'short' });
|
|
1429
|
-
const parts = timeZoneString.split(' ');
|
|
1430
|
-
return parts[parts.length - 1]; // Last part is timezone abbreviation
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
|
-
// Format relative time ("3 hours ago", "2 days ago")
|
|
1434
|
-
function formatRelativeTime(diffMs) {
|
|
1435
|
-
const seconds = Math.floor(diffMs / 1000);
|
|
1436
|
-
const minutes = Math.floor(seconds / 60);
|
|
1437
|
-
const hours = Math.floor(minutes / 60);
|
|
1438
|
-
const days = Math.floor(hours / 24);
|
|
1439
|
-
const months = Math.floor(days / 30);
|
|
1440
|
-
const years = Math.floor(days / 365);
|
|
1441
|
-
|
|
1442
|
-
if (seconds < 60) {
|
|
1443
|
-
return seconds <= 1 ? '1 second ago' : seconds + ' seconds ago';
|
|
1444
|
-
} else if (minutes < 60) {
|
|
1445
|
-
return minutes === 1 ? '1 minute ago' : minutes + ' minutes ago';
|
|
1446
|
-
} else if (hours < 24) {
|
|
1447
|
-
return hours === 1 ? '1 hour ago' : hours + ' hours ago';
|
|
1448
|
-
} else if (days < 30) {
|
|
1449
|
-
return days === 1 ? '1 day ago' : days + ' days ago';
|
|
1450
|
-
} else if (months < 12) {
|
|
1451
|
-
return months === 1 ? '1 month ago' : months + ' months ago';
|
|
1452
|
-
} else {
|
|
1453
|
-
return years === 1 ? '1 year ago' : years + ' years ago';
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
// Run conversion on page load
|
|
1458
|
-
convertToLocalTime();
|
|
1459
|
-
|
|
1460
|
-
// Also run after Turbo navigation (if using Turbo/Hotwire)
|
|
1461
|
-
if (typeof Turbo !== 'undefined') {
|
|
1462
|
-
document.addEventListener('turbo:load', convertToLocalTime);
|
|
1463
|
-
document.addEventListener('turbo:frame-load', convertToLocalTime);
|
|
1464
|
-
}
|
|
1465
|
-
});
|
|
290
|
+
<% if flash[:alert] %>
|
|
291
|
+
showToast('<%= j flash[:alert] %>', 'danger');
|
|
292
|
+
<% end %>
|
|
293
|
+
<% if flash[:success] %>
|
|
294
|
+
showToast('<%= j flash[:success] %>', 'success');
|
|
295
|
+
<% end %>
|
|
296
|
+
<% if flash[:error] %>
|
|
297
|
+
showToast('<%= j flash[:error] %>', 'danger');
|
|
298
|
+
<% end %>
|
|
299
|
+
<% end %>
|
|
1466
300
|
</script>
|
|
1467
301
|
</body>
|
|
1468
302
|
</html>
|