rails_error_dashboard 0.1.29 → 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 +34 -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 +66 -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/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 +44 -20
- 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/dashboard_stats.rb +19 -4
- data/lib/rails_error_dashboard/queries/errors_list.rb +20 -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 +13 -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,11 @@
|
|
|
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>
|
|
925
154
|
<li class="nav-item">
|
|
926
155
|
<%= link_to correlation_errors_path(nav_params), class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}" do %>
|
|
927
156
|
<i class="bi bi-diagram-3"></i> Correlation
|
|
@@ -952,13 +181,28 @@
|
|
|
952
181
|
<% end %>
|
|
953
182
|
</li>
|
|
954
183
|
<li class="nav-item">
|
|
955
|
-
<%= link_to errors_path(nav_params.merge(platform: 'iOS')), class: "nav-link" do %>
|
|
956
|
-
<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
|
|
957
191
|
<% end %>
|
|
958
192
|
</li>
|
|
959
193
|
<li class="nav-item">
|
|
960
|
-
<%= link_to errors_path(nav_params.merge(platform: '
|
|
961
|
-
<i class="bi bi-
|
|
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
|
|
201
|
+
<% end %>
|
|
202
|
+
</li>
|
|
203
|
+
<li class="nav-item">
|
|
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
|
|
962
206
|
<% end %>
|
|
963
207
|
</li>
|
|
964
208
|
</ul>
|
|
@@ -966,7 +210,7 @@
|
|
|
966
210
|
</nav>
|
|
967
211
|
|
|
968
212
|
<!-- Main content -->
|
|
969
|
-
<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">
|
|
970
214
|
<%= yield %>
|
|
971
215
|
</main>
|
|
972
216
|
</div>
|
|
@@ -996,6 +240,10 @@
|
|
|
996
240
|
<span><i class="bi bi-graph-up text-primary"></i> Go to analytics</span>
|
|
997
241
|
<kbd class="bg-secondary text-white px-2 py-1 rounded">A</kbd>
|
|
998
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>
|
|
999
247
|
<div class="list-group-item d-flex justify-content-between align-items-center">
|
|
1000
248
|
<span><i class="bi bi-question-circle text-primary"></i> Show this help</span>
|
|
1001
249
|
<kbd class="bg-secondary text-white px-2 py-1 rounded">?</kbd>
|
|
@@ -1025,449 +273,30 @@
|
|
|
1025
273
|
<!-- Bootstrap JS -->
|
|
1026
274
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
1027
275
|
|
|
1028
|
-
<!-- Pure JavaScript Theme Toggle -->
|
|
1029
|
-
<script>
|
|
1030
|
-
// Load saved theme on page load (before DOMContentLoaded to prevent flash)
|
|
1031
|
-
(function() {
|
|
1032
|
-
const savedTheme = localStorage.getItem('theme');
|
|
1033
|
-
if (savedTheme === 'dark') {
|
|
1034
|
-
document.body.classList.add('dark-mode');
|
|
1035
|
-
}
|
|
1036
|
-
})();
|
|
1037
|
-
|
|
1038
|
-
// Theme toggle after DOM loads
|
|
1039
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
1040
|
-
const themeToggle = document.getElementById('themeToggle');
|
|
1041
|
-
const themeIcon = document.getElementById('themeIcon');
|
|
1042
|
-
|
|
1043
|
-
// Update icon based on current theme
|
|
1044
|
-
function updateIcon() {
|
|
1045
|
-
if (document.body.classList.contains('dark-mode')) {
|
|
1046
|
-
themeIcon.className = 'bi bi-sun-fill';
|
|
1047
|
-
} else {
|
|
1048
|
-
themeIcon.className = 'bi bi-moon-fill';
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
|
|
1052
|
-
// Set initial icon
|
|
1053
|
-
updateIcon();
|
|
1054
|
-
|
|
1055
|
-
// Toggle theme on button click
|
|
1056
|
-
themeToggle.addEventListener('click', function() {
|
|
1057
|
-
console.log('🎨 Theme toggle clicked');
|
|
1058
|
-
|
|
1059
|
-
document.body.classList.toggle('dark-mode');
|
|
1060
|
-
const isDark = document.body.classList.contains('dark-mode');
|
|
1061
|
-
|
|
1062
|
-
console.log('Dark mode:', isDark);
|
|
1063
|
-
|
|
1064
|
-
// Save preference
|
|
1065
|
-
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
1066
|
-
console.log('💾 Saved to localStorage:', isDark ? 'dark' : 'light');
|
|
1067
|
-
|
|
1068
|
-
// Update icon
|
|
1069
|
-
updateIcon();
|
|
1070
|
-
console.log('✅ Theme toggled successfully');
|
|
1071
|
-
|
|
1072
|
-
// Reapply chart theme
|
|
1073
|
-
applyChartTheme();
|
|
1074
|
-
|
|
1075
|
-
// Reload page to update charts properly
|
|
1076
|
-
setTimeout(() => location.reload(), 300);
|
|
1077
|
-
});
|
|
1078
|
-
|
|
1079
|
-
// Chart.js theme colors - ULTRA AGGRESSIVE setup
|
|
1080
|
-
function applyChartTheme() {
|
|
1081
|
-
if (typeof Chart !== 'undefined') {
|
|
1082
|
-
const isDark = document.body.classList.contains('dark-mode');
|
|
1083
|
-
const textColor = isDark ? '#cdd6f4' : '#1f2937';
|
|
1084
|
-
const gridColor = isDark ? 'rgba(88, 91, 112, 0.2)' : 'rgba(0, 0, 0, 0.1)';
|
|
1085
|
-
|
|
1086
|
-
console.log('📊 Setting Chart.js theme:', isDark ? 'DARK' : 'light', '| Text:', textColor);
|
|
1087
|
-
|
|
1088
|
-
// Global defaults
|
|
1089
|
-
Chart.defaults.color = textColor;
|
|
1090
|
-
Chart.defaults.borderColor = gridColor;
|
|
1091
|
-
Chart.defaults.font = Chart.defaults.font || {};
|
|
1092
|
-
Chart.defaults.font.color = textColor;
|
|
1093
|
-
|
|
1094
|
-
// Scale defaults (axes) - AGGRESSIVE
|
|
1095
|
-
if (Chart.defaults.scale) {
|
|
1096
|
-
Chart.defaults.scale.ticks = Chart.defaults.scale.ticks || {};
|
|
1097
|
-
Chart.defaults.scale.ticks.color = textColor;
|
|
1098
|
-
Chart.defaults.scale.ticks.font = Chart.defaults.scale.ticks.font || {};
|
|
1099
|
-
Chart.defaults.scale.ticks.font.color = textColor;
|
|
1100
|
-
|
|
1101
|
-
Chart.defaults.scale.grid = Chart.defaults.scale.grid || {};
|
|
1102
|
-
Chart.defaults.scale.grid.color = gridColor;
|
|
1103
|
-
|
|
1104
|
-
// Axis title (xtitle, ytitle)
|
|
1105
|
-
Chart.defaults.scale.title = Chart.defaults.scale.title || {};
|
|
1106
|
-
Chart.defaults.scale.title.color = textColor;
|
|
1107
|
-
Chart.defaults.scale.title.font = Chart.defaults.scale.title.font || {};
|
|
1108
|
-
Chart.defaults.scale.title.font.size = 14;
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// X and Y axis specific
|
|
1112
|
-
if (Chart.defaults.scales) {
|
|
1113
|
-
// X axis
|
|
1114
|
-
Chart.defaults.scales.x = Chart.defaults.scales.x || {};
|
|
1115
|
-
Chart.defaults.scales.x.ticks = Chart.defaults.scales.x.ticks || {};
|
|
1116
|
-
Chart.defaults.scales.x.ticks.color = textColor;
|
|
1117
|
-
Chart.defaults.scales.x.title = Chart.defaults.scales.x.title || {};
|
|
1118
|
-
Chart.defaults.scales.x.title.color = textColor;
|
|
1119
|
-
Chart.defaults.scales.x.grid = Chart.defaults.scales.x.grid || {};
|
|
1120
|
-
Chart.defaults.scales.x.grid.color = gridColor;
|
|
1121
|
-
|
|
1122
|
-
// Y axis
|
|
1123
|
-
Chart.defaults.scales.y = Chart.defaults.scales.y || {};
|
|
1124
|
-
Chart.defaults.scales.y.ticks = Chart.defaults.scales.y.ticks || {};
|
|
1125
|
-
Chart.defaults.scales.y.ticks.color = textColor;
|
|
1126
|
-
Chart.defaults.scales.y.title = Chart.defaults.scales.y.title || {};
|
|
1127
|
-
Chart.defaults.scales.y.title.color = textColor;
|
|
1128
|
-
Chart.defaults.scales.y.grid = Chart.defaults.scales.y.grid || {};
|
|
1129
|
-
Chart.defaults.scales.y.grid.color = gridColor;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Plugin defaults
|
|
1133
|
-
if (Chart.defaults.plugins) {
|
|
1134
|
-
// Legend
|
|
1135
|
-
if (Chart.defaults.plugins.legend) {
|
|
1136
|
-
Chart.defaults.plugins.legend.labels = Chart.defaults.plugins.legend.labels || {};
|
|
1137
|
-
Chart.defaults.plugins.legend.labels.color = textColor;
|
|
1138
|
-
Chart.defaults.plugins.legend.labels.font = Chart.defaults.plugins.legend.labels.font || {};
|
|
1139
|
-
Chart.defaults.plugins.legend.labels.font.color = textColor;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// Tooltip
|
|
1143
|
-
if (Chart.defaults.plugins.tooltip) {
|
|
1144
|
-
Chart.defaults.plugins.tooltip.backgroundColor = isDark ? '#313244' : 'rgba(255, 255, 255, 0.95)';
|
|
1145
|
-
Chart.defaults.plugins.tooltip.titleColor = isDark ? textColor : '#1f2937';
|
|
1146
|
-
Chart.defaults.plugins.tooltip.bodyColor = isDark ? textColor : '#1f2937';
|
|
1147
|
-
Chart.defaults.plugins.tooltip.borderColor = isDark ? '#585b70' : 'rgba(0, 0, 0, 0.2)';
|
|
1148
|
-
Chart.defaults.plugins.tooltip.borderWidth = 1;
|
|
1149
|
-
}
|
|
1150
|
-
|
|
1151
|
-
// Title plugin
|
|
1152
|
-
if (Chart.defaults.plugins.title) {
|
|
1153
|
-
Chart.defaults.plugins.title.color = textColor;
|
|
1154
|
-
Chart.defaults.plugins.title.font = Chart.defaults.plugins.title.font || {};
|
|
1155
|
-
Chart.defaults.plugins.title.font.color = textColor;
|
|
1156
|
-
}
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
console.log('✅ Chart.js theme applied - all text should be:', textColor);
|
|
1160
|
-
} else {
|
|
1161
|
-
console.warn('⚠️ Chart.js not loaded yet');
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// Also update Google Charts if present
|
|
1165
|
-
if (typeof google !== 'undefined' && google.visualization) {
|
|
1166
|
-
console.log('📊 Google Charts detected - applying theme');
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
// Apply chart theme on load
|
|
1171
|
-
applyChartTheme();
|
|
1172
|
-
|
|
1173
|
-
// NUCLEAR OPTION: Watch for chart creation and force colors
|
|
1174
|
-
const observer = new MutationObserver(function(mutations) {
|
|
1175
|
-
mutations.forEach(function(mutation) {
|
|
1176
|
-
mutation.addedNodes.forEach(function(node) {
|
|
1177
|
-
if (node.tagName === 'CANVAS') {
|
|
1178
|
-
console.log('🎨 New chart detected, forcing theme...');
|
|
1179
|
-
setTimeout(applyChartTheme, 100);
|
|
1180
|
-
}
|
|
1181
|
-
});
|
|
1182
|
-
});
|
|
1183
|
-
});
|
|
1184
|
-
|
|
1185
|
-
observer.observe(document.body, {
|
|
1186
|
-
childList: true,
|
|
1187
|
-
subtree: true
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
// Also listen for Chartkick chart creation events
|
|
1191
|
-
document.addEventListener('chartkick:load', function() {
|
|
1192
|
-
console.log('📊 Chartkick loaded, applying theme');
|
|
1193
|
-
applyChartTheme();
|
|
1194
|
-
});
|
|
1195
|
-
|
|
1196
|
-
// Force reapply every 500ms for the first 3 seconds (in case charts load late)
|
|
1197
|
-
let attempts = 0;
|
|
1198
|
-
const forceInterval = setInterval(function() {
|
|
1199
|
-
attempts++;
|
|
1200
|
-
applyChartTheme();
|
|
1201
|
-
console.log('🔄 Force applying theme (attempt', attempts, ')');
|
|
1202
|
-
if (attempts >= 6) {
|
|
1203
|
-
clearInterval(forceInterval);
|
|
1204
|
-
console.log('✅ Stopped force applying');
|
|
1205
|
-
}
|
|
1206
|
-
}, 500);
|
|
1207
|
-
|
|
1208
|
-
// Copy to clipboard functionality
|
|
1209
|
-
window.copyToClipboard = function(text, button) {
|
|
1210
|
-
navigator.clipboard.writeText(text).then(function() {
|
|
1211
|
-
// Store original button HTML
|
|
1212
|
-
const originalHTML = button.innerHTML;
|
|
1213
|
-
|
|
1214
|
-
// Show success state on button
|
|
1215
|
-
button.innerHTML = '<i class="bi bi-check"></i> Copied!';
|
|
1216
|
-
button.classList.remove('btn-outline-secondary');
|
|
1217
|
-
button.classList.add('btn-success');
|
|
1218
|
-
|
|
1219
|
-
// Show toast notification
|
|
1220
|
-
showToast('Copied to clipboard!', 'success');
|
|
1221
|
-
|
|
1222
|
-
// Reset button after 2 seconds
|
|
1223
|
-
setTimeout(function() {
|
|
1224
|
-
button.innerHTML = originalHTML;
|
|
1225
|
-
button.classList.remove('btn-success');
|
|
1226
|
-
button.classList.add('btn-outline-secondary');
|
|
1227
|
-
}, 2000);
|
|
1228
|
-
}).catch(function(err) {
|
|
1229
|
-
console.error('Failed to copy:', err);
|
|
1230
|
-
button.innerHTML = '<i class="bi bi-x"></i> Failed';
|
|
1231
|
-
showToast('Failed to copy to clipboard', 'danger');
|
|
1232
|
-
});
|
|
1233
|
-
};
|
|
1234
|
-
|
|
1235
|
-
// Toast notification functionality
|
|
1236
|
-
window.showToast = function(message, type) {
|
|
1237
|
-
type = type || 'success';
|
|
1238
|
-
|
|
1239
|
-
const toastId = 'toast-' + Date.now();
|
|
1240
|
-
const iconClass = type === 'success' ? 'bi-check-circle-fill' :
|
|
1241
|
-
type === 'danger' ? 'bi-exclamation-circle-fill' :
|
|
1242
|
-
'bi-info-circle-fill';
|
|
1243
|
-
const bgClass = type === 'success' ? 'bg-success' :
|
|
1244
|
-
type === 'danger' ? 'bg-danger' :
|
|
1245
|
-
'bg-info';
|
|
1246
|
-
|
|
1247
|
-
const toastHTML = `
|
|
1248
|
-
<div id="${toastId}" class="toast align-items-center text-white ${bgClass} border-0" role="alert" aria-live="assertive" aria-atomic="true">
|
|
1249
|
-
<div class="d-flex">
|
|
1250
|
-
<div class="toast-body">
|
|
1251
|
-
<i class="bi ${iconClass} me-2"></i>
|
|
1252
|
-
${message}
|
|
1253
|
-
</div>
|
|
1254
|
-
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast" aria-label="Close"></button>
|
|
1255
|
-
</div>
|
|
1256
|
-
</div>
|
|
1257
|
-
`;
|
|
1258
|
-
|
|
1259
|
-
const container = document.querySelector('.toast-container');
|
|
1260
|
-
container.insertAdjacentHTML('beforeend', toastHTML);
|
|
1261
|
-
|
|
1262
|
-
const toastElement = document.getElementById(toastId);
|
|
1263
|
-
const toast = new bootstrap.Toast(toastElement, { delay: 4000 });
|
|
1264
|
-
toast.show();
|
|
1265
276
|
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
showToast('<%= j flash[:alert] %>', 'danger');
|
|
1279
|
-
<% end %>
|
|
1280
|
-
<% if flash[:success] %>
|
|
1281
|
-
showToast('<%= j flash[:success] %>', 'success');
|
|
1282
|
-
<% end %>
|
|
1283
|
-
<% if flash[:error] %>
|
|
1284
|
-
showToast('<%= j flash[:error] %>', 'danger');
|
|
1285
|
-
<% 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');
|
|
1286
289
|
<% end %>
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
try {
|
|
1299
|
-
const date = new Date(utcString);
|
|
1300
|
-
if (isNaN(date.getTime())) return; // Invalid date
|
|
1301
|
-
|
|
1302
|
-
// Parse the format string and convert to local time
|
|
1303
|
-
const formatted = formatDateTime(date, formatString);
|
|
1304
|
-
|
|
1305
|
-
// Get timezone abbreviation
|
|
1306
|
-
const timezone = getTimezoneAbbreviation(date);
|
|
1307
|
-
|
|
1308
|
-
// Update element text
|
|
1309
|
-
element.textContent = formatted + ' ' + timezone;
|
|
1310
|
-
element.title = 'Your local time (click to see UTC)';
|
|
1311
|
-
|
|
1312
|
-
// Add click handler to toggle between local and UTC
|
|
1313
|
-
element.style.cursor = 'pointer';
|
|
1314
|
-
element.dataset.originalUtc = utcString;
|
|
1315
|
-
element.dataset.localFormatted = formatted + ' ' + timezone;
|
|
1316
|
-
element.dataset.showingLocal = 'true';
|
|
1317
|
-
|
|
1318
|
-
element.addEventListener('click', function() {
|
|
1319
|
-
if (this.dataset.showingLocal === 'true') {
|
|
1320
|
-
// Show UTC
|
|
1321
|
-
const utcDate = new Date(this.dataset.originalUtc);
|
|
1322
|
-
const utcFormatted = formatDateTime(utcDate, formatString);
|
|
1323
|
-
this.textContent = utcFormatted + ' UTC';
|
|
1324
|
-
this.title = 'UTC time (click to see local time)';
|
|
1325
|
-
this.dataset.showingLocal = 'false';
|
|
1326
|
-
} else {
|
|
1327
|
-
// Show local
|
|
1328
|
-
this.textContent = this.dataset.localFormatted;
|
|
1329
|
-
this.title = 'Your local time (click to see UTC)';
|
|
1330
|
-
this.dataset.showingLocal = 'true';
|
|
1331
|
-
}
|
|
1332
|
-
});
|
|
1333
|
-
} catch (e) {
|
|
1334
|
-
console.error('Error converting timestamp:', e);
|
|
1335
|
-
}
|
|
1336
|
-
});
|
|
1337
|
-
|
|
1338
|
-
// Convert all .local-time-ago elements (relative time)
|
|
1339
|
-
document.querySelectorAll('.local-time-ago').forEach(function(element) {
|
|
1340
|
-
const utcString = element.dataset.utc;
|
|
1341
|
-
if (!utcString) return;
|
|
1342
|
-
|
|
1343
|
-
try {
|
|
1344
|
-
const date = new Date(utcString);
|
|
1345
|
-
if (isNaN(date.getTime())) return; // Invalid date
|
|
1346
|
-
|
|
1347
|
-
// Calculate relative time
|
|
1348
|
-
const now = new Date();
|
|
1349
|
-
const diffMs = now - date;
|
|
1350
|
-
const formatted = formatRelativeTime(diffMs);
|
|
1351
|
-
|
|
1352
|
-
// Update element text
|
|
1353
|
-
element.textContent = formatted;
|
|
1354
|
-
element.title = 'Click to see exact time';
|
|
1355
|
-
|
|
1356
|
-
// Add click handler to toggle between relative and absolute
|
|
1357
|
-
element.style.cursor = 'pointer';
|
|
1358
|
-
element.dataset.originalUtc = utcString;
|
|
1359
|
-
element.dataset.showingRelative = 'true';
|
|
1360
|
-
|
|
1361
|
-
element.addEventListener('click', function() {
|
|
1362
|
-
if (this.dataset.showingRelative === 'true') {
|
|
1363
|
-
// Show absolute time
|
|
1364
|
-
const absoluteDate = new Date(this.dataset.originalUtc);
|
|
1365
|
-
const absoluteFormatted = formatDateTime(absoluteDate, '%B %d, %Y %I:%M:%S %p');
|
|
1366
|
-
const timezone = getTimezoneAbbreviation(absoluteDate);
|
|
1367
|
-
this.textContent = absoluteFormatted + ' ' + timezone;
|
|
1368
|
-
this.title = 'Click to see relative time';
|
|
1369
|
-
this.dataset.showingRelative = 'false';
|
|
1370
|
-
} else {
|
|
1371
|
-
// Show relative time
|
|
1372
|
-
const now = new Date();
|
|
1373
|
-
const date = new Date(this.dataset.originalUtc);
|
|
1374
|
-
const diffMs = now - date;
|
|
1375
|
-
this.textContent = formatRelativeTime(diffMs);
|
|
1376
|
-
this.title = 'Click to see exact time';
|
|
1377
|
-
this.dataset.showingRelative = 'true';
|
|
1378
|
-
}
|
|
1379
|
-
});
|
|
1380
|
-
} catch (e) {
|
|
1381
|
-
console.error('Error converting relative time:', e);
|
|
1382
|
-
}
|
|
1383
|
-
});
|
|
1384
|
-
}
|
|
1385
|
-
|
|
1386
|
-
// Format date according to strftime-like format string
|
|
1387
|
-
function formatDateTime(date, formatString) {
|
|
1388
|
-
const months = ['January', 'February', 'March', 'April', 'May', 'June',
|
|
1389
|
-
'July', 'August', 'September', 'October', 'November', 'December'];
|
|
1390
|
-
const monthsShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
|
1391
|
-
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
1392
|
-
const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
1393
|
-
const daysShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1394
|
-
|
|
1395
|
-
const year = date.getFullYear();
|
|
1396
|
-
const month = date.getMonth();
|
|
1397
|
-
const day = date.getDate();
|
|
1398
|
-
const hours = date.getHours();
|
|
1399
|
-
const minutes = date.getMinutes();
|
|
1400
|
-
const seconds = date.getSeconds();
|
|
1401
|
-
const dayOfWeek = date.getDay();
|
|
1402
|
-
|
|
1403
|
-
// 12-hour format
|
|
1404
|
-
const hours12 = hours % 12 || 12;
|
|
1405
|
-
const ampm = hours >= 12 ? 'PM' : 'AM';
|
|
1406
|
-
|
|
1407
|
-
// Padding helper
|
|
1408
|
-
const pad = (n) => n.toString().padStart(2, '0');
|
|
1409
|
-
|
|
1410
|
-
// Replace format specifiers
|
|
1411
|
-
let result = formatString
|
|
1412
|
-
.replace('%Y', year)
|
|
1413
|
-
.replace('%y', year.toString().substr(2))
|
|
1414
|
-
.replace('%B', months[month])
|
|
1415
|
-
.replace('%b', monthsShort[month])
|
|
1416
|
-
.replace('%m', pad(month + 1))
|
|
1417
|
-
.replace('%d', pad(day))
|
|
1418
|
-
.replace('%e', day)
|
|
1419
|
-
.replace('%A', days[dayOfWeek])
|
|
1420
|
-
.replace('%a', daysShort[dayOfWeek])
|
|
1421
|
-
.replace('%H', pad(hours))
|
|
1422
|
-
.replace('%I', pad(hours12))
|
|
1423
|
-
.replace('%M', pad(minutes))
|
|
1424
|
-
.replace('%S', pad(seconds))
|
|
1425
|
-
.replace('%p', ampm)
|
|
1426
|
-
.replace('%P', ampm.toLowerCase());
|
|
1427
|
-
|
|
1428
|
-
return result;
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
// Get timezone abbreviation (e.g., "PST", "EST", "UTC+2")
|
|
1432
|
-
function getTimezoneAbbreviation(date) {
|
|
1433
|
-
const timeZoneString = date.toLocaleTimeString('en-US', { timeZoneName: 'short' });
|
|
1434
|
-
const parts = timeZoneString.split(' ');
|
|
1435
|
-
return parts[parts.length - 1]; // Last part is timezone abbreviation
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
// Format relative time ("3 hours ago", "2 days ago")
|
|
1439
|
-
function formatRelativeTime(diffMs) {
|
|
1440
|
-
const seconds = Math.floor(diffMs / 1000);
|
|
1441
|
-
const minutes = Math.floor(seconds / 60);
|
|
1442
|
-
const hours = Math.floor(minutes / 60);
|
|
1443
|
-
const days = Math.floor(hours / 24);
|
|
1444
|
-
const months = Math.floor(days / 30);
|
|
1445
|
-
const years = Math.floor(days / 365);
|
|
1446
|
-
|
|
1447
|
-
if (seconds < 60) {
|
|
1448
|
-
return seconds <= 1 ? '1 second ago' : seconds + ' seconds ago';
|
|
1449
|
-
} else if (minutes < 60) {
|
|
1450
|
-
return minutes === 1 ? '1 minute ago' : minutes + ' minutes ago';
|
|
1451
|
-
} else if (hours < 24) {
|
|
1452
|
-
return hours === 1 ? '1 hour ago' : hours + ' hours ago';
|
|
1453
|
-
} else if (days < 30) {
|
|
1454
|
-
return days === 1 ? '1 day ago' : days + ' days ago';
|
|
1455
|
-
} else if (months < 12) {
|
|
1456
|
-
return months === 1 ? '1 month ago' : months + ' months ago';
|
|
1457
|
-
} else {
|
|
1458
|
-
return years === 1 ? '1 year ago' : years + ' years ago';
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
// Run conversion on page load
|
|
1463
|
-
convertToLocalTime();
|
|
1464
|
-
|
|
1465
|
-
// Also run after Turbo navigation (if using Turbo/Hotwire)
|
|
1466
|
-
if (typeof Turbo !== 'undefined') {
|
|
1467
|
-
document.addEventListener('turbo:load', convertToLocalTime);
|
|
1468
|
-
document.addEventListener('turbo:frame-load', convertToLocalTime);
|
|
1469
|
-
}
|
|
1470
|
-
});
|
|
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 %>
|
|
1471
300
|
</script>
|
|
1472
301
|
</body>
|
|
1473
302
|
</html>
|