rails_error_dashboard 0.1.17 → 0.1.18

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 66993309911427e7fdbddadebfea4a8603fa851ce47c7d5ac6189510c741965b
4
- data.tar.gz: 86d88a89e817adcb790a9b83791decfaa13ffeac4d1b744b846d072e6610ebb5
3
+ metadata.gz: 2cdd815518aac095839eaf937dde33835b52b260cc6494d7e0a904de0c59b614
4
+ data.tar.gz: 7cd803c5e3ac4e724d3514088d46d8c76316336ce89e233a116a8ba95dad00bc
5
5
  SHA512:
6
- metadata.gz: 6581ea39ea6340b0df0dcb9835c597367f5c37459462f3ebe6f79315f99771ade90770fd04605477edef545b75aef32ae6d04acffe4caf896390b219ae11380e
7
- data.tar.gz: 1dce7070b16addd615deb960d93e3226a448cb776526a17d1f353a1fdd8d25e90c8a2916bba21482b23637bcebf5a34c153b9227113ea6a23eeba79891f6acd7
6
+ metadata.gz: a5cea12724e61c164845596926c4d6fdebacd4fada853b512c78e8c345b87f18f5347eb53c70b17f911d84963a5c2913415688b23afaebf50a47ece25ec695b5
7
+ data.tar.gz: a4110c8cf08406f79683e3a923967cd5a5fc3f383893e3a616ebd6a360f079e982e3f2be0bfea0db1c0d406e99d5cd3fada6bcba15294a0355739ccb1854a2c2
@@ -130,5 +130,68 @@ module RailsErrorDashboard
130
130
  content_tag(:code, display_sha, class: "font-monospace")
131
131
  end
132
132
  end
133
+
134
+ # Renders a timestamp that will be automatically converted to user's local timezone
135
+ # Server sends UTC timestamp, JavaScript converts to local timezone on page load
136
+ # @param time [Time, DateTime, nil] The timestamp to display
137
+ # @param format [Symbol] Format preset (:full, :short, :date_only, :time_only, :datetime)
138
+ # @param fallback [String] Text to show if time is nil
139
+ # @return [String] HTML safe span with data attributes for JS conversion
140
+ def local_time(time, format: :full, fallback: "N/A")
141
+ return fallback if time.nil?
142
+
143
+ # Convert to UTC if not already
144
+ utc_time = time.respond_to?(:utc) ? time.utc : time
145
+
146
+ # ISO 8601 format for JavaScript parsing
147
+ iso_time = utc_time.iso8601
148
+
149
+ # Format presets for data-format attribute
150
+ format_string = case format
151
+ when :full
152
+ "%B %d, %Y %I:%M:%S %p" # December 31, 2024 11:59:59 PM
153
+ when :short
154
+ "%m/%d %I:%M%p" # 12/31 11:59PM
155
+ when :date_only
156
+ "%B %d, %Y" # December 31, 2024
157
+ when :time_only
158
+ "%I:%M:%S %p" # 11:59:59 PM
159
+ when :datetime
160
+ "%b %d, %Y %H:%M" # Dec 31, 2024 23:59
161
+ else
162
+ format.to_s
163
+ end
164
+
165
+ content_tag(
166
+ :span,
167
+ utc_time.strftime(format_string + " UTC"), # Fallback for non-JS browsers
168
+ class: "local-time",
169
+ data: {
170
+ utc: iso_time,
171
+ format: format_string
172
+ }
173
+ )
174
+ end
175
+
176
+ # Renders a relative time ("3 hours ago") that updates automatically
177
+ # @param time [Time, DateTime, nil] The timestamp to display
178
+ # @param fallback [String] Text to show if time is nil
179
+ # @return [String] HTML safe span with data attributes for JS conversion
180
+ def local_time_ago(time, fallback: "N/A")
181
+ return fallback if time.nil?
182
+
183
+ # Convert to UTC if not already
184
+ utc_time = time.respond_to?(:utc) ? time.utc : time
185
+ iso_time = utc_time.iso8601
186
+
187
+ content_tag(
188
+ :span,
189
+ time_ago_in_words(time) + " ago", # Fallback for non-JS browsers
190
+ class: "local-time-ago",
191
+ data: {
192
+ utc: iso_time
193
+ }
194
+ )
195
+ end
133
196
  end
134
197
  end
@@ -1188,6 +1188,189 @@
1188
1188
  showToast('<%= j flash[:error] %>', 'danger');
1189
1189
  <% end %>
1190
1190
  <% end %>
1191
+
1192
+ // Local Timezone Conversion
1193
+ // Converts UTC timestamps to user's local timezone on page load
1194
+ function convertToLocalTime() {
1195
+ // Convert all .local-time elements (formatted timestamps)
1196
+ document.querySelectorAll('.local-time').forEach(function(element) {
1197
+ const utcString = element.dataset.utc;
1198
+ const formatString = element.dataset.format;
1199
+
1200
+ if (!utcString) return;
1201
+
1202
+ try {
1203
+ const date = new Date(utcString);
1204
+ if (isNaN(date.getTime())) return; // Invalid date
1205
+
1206
+ // Parse the format string and convert to local time
1207
+ const formatted = formatDateTime(date, formatString);
1208
+
1209
+ // Get timezone abbreviation
1210
+ const timezone = getTimezoneAbbreviation(date);
1211
+
1212
+ // Update element text
1213
+ element.textContent = formatted + ' ' + timezone;
1214
+ element.title = 'Your local time (click to see UTC)';
1215
+
1216
+ // Add click handler to toggle between local and UTC
1217
+ element.style.cursor = 'pointer';
1218
+ element.dataset.originalUtc = utcString;
1219
+ element.dataset.localFormatted = formatted + ' ' + timezone;
1220
+ element.dataset.showingLocal = 'true';
1221
+
1222
+ element.addEventListener('click', function() {
1223
+ if (this.dataset.showingLocal === 'true') {
1224
+ // Show UTC
1225
+ const utcDate = new Date(this.dataset.originalUtc);
1226
+ const utcFormatted = formatDateTime(utcDate, formatString);
1227
+ this.textContent = utcFormatted + ' UTC';
1228
+ this.title = 'UTC time (click to see local time)';
1229
+ this.dataset.showingLocal = 'false';
1230
+ } else {
1231
+ // Show local
1232
+ this.textContent = this.dataset.localFormatted;
1233
+ this.title = 'Your local time (click to see UTC)';
1234
+ this.dataset.showingLocal = 'true';
1235
+ }
1236
+ });
1237
+ } catch (e) {
1238
+ console.error('Error converting timestamp:', e);
1239
+ }
1240
+ });
1241
+
1242
+ // Convert all .local-time-ago elements (relative time)
1243
+ document.querySelectorAll('.local-time-ago').forEach(function(element) {
1244
+ const utcString = element.dataset.utc;
1245
+ if (!utcString) return;
1246
+
1247
+ try {
1248
+ const date = new Date(utcString);
1249
+ if (isNaN(date.getTime())) return; // Invalid date
1250
+
1251
+ // Calculate relative time
1252
+ const now = new Date();
1253
+ const diffMs = now - date;
1254
+ const formatted = formatRelativeTime(diffMs);
1255
+
1256
+ // Update element text
1257
+ element.textContent = formatted;
1258
+ element.title = 'Click to see exact time';
1259
+
1260
+ // Add click handler to toggle between relative and absolute
1261
+ element.style.cursor = 'pointer';
1262
+ element.dataset.originalUtc = utcString;
1263
+ element.dataset.showingRelative = 'true';
1264
+
1265
+ element.addEventListener('click', function() {
1266
+ if (this.dataset.showingRelative === 'true') {
1267
+ // Show absolute time
1268
+ const absoluteDate = new Date(this.dataset.originalUtc);
1269
+ const absoluteFormatted = formatDateTime(absoluteDate, '%B %d, %Y %I:%M:%S %p');
1270
+ const timezone = getTimezoneAbbreviation(absoluteDate);
1271
+ this.textContent = absoluteFormatted + ' ' + timezone;
1272
+ this.title = 'Click to see relative time';
1273
+ this.dataset.showingRelative = 'false';
1274
+ } else {
1275
+ // Show relative time
1276
+ const now = new Date();
1277
+ const date = new Date(this.dataset.originalUtc);
1278
+ const diffMs = now - date;
1279
+ this.textContent = formatRelativeTime(diffMs);
1280
+ this.title = 'Click to see exact time';
1281
+ this.dataset.showingRelative = 'true';
1282
+ }
1283
+ });
1284
+ } catch (e) {
1285
+ console.error('Error converting relative time:', e);
1286
+ }
1287
+ });
1288
+ }
1289
+
1290
+ // Format date according to strftime-like format string
1291
+ function formatDateTime(date, formatString) {
1292
+ const months = ['January', 'February', 'March', 'April', 'May', 'June',
1293
+ 'July', 'August', 'September', 'October', 'November', 'December'];
1294
+ const monthsShort = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
1295
+ 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
1296
+ const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
1297
+ const daysShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
1298
+
1299
+ const year = date.getFullYear();
1300
+ const month = date.getMonth();
1301
+ const day = date.getDate();
1302
+ const hours = date.getHours();
1303
+ const minutes = date.getMinutes();
1304
+ const seconds = date.getSeconds();
1305
+ const dayOfWeek = date.getDay();
1306
+
1307
+ // 12-hour format
1308
+ const hours12 = hours % 12 || 12;
1309
+ const ampm = hours >= 12 ? 'PM' : 'AM';
1310
+
1311
+ // Padding helper
1312
+ const pad = (n) => n.toString().padStart(2, '0');
1313
+
1314
+ // Replace format specifiers
1315
+ let result = formatString
1316
+ .replace('%Y', year)
1317
+ .replace('%y', year.toString().substr(2))
1318
+ .replace('%B', months[month])
1319
+ .replace('%b', monthsShort[month])
1320
+ .replace('%m', pad(month + 1))
1321
+ .replace('%d', pad(day))
1322
+ .replace('%e', day)
1323
+ .replace('%A', days[dayOfWeek])
1324
+ .replace('%a', daysShort[dayOfWeek])
1325
+ .replace('%H', pad(hours))
1326
+ .replace('%I', pad(hours12))
1327
+ .replace('%M', pad(minutes))
1328
+ .replace('%S', pad(seconds))
1329
+ .replace('%p', ampm)
1330
+ .replace('%P', ampm.toLowerCase());
1331
+
1332
+ return result;
1333
+ }
1334
+
1335
+ // Get timezone abbreviation (e.g., "PST", "EST", "UTC+2")
1336
+ function getTimezoneAbbreviation(date) {
1337
+ const timeZoneString = date.toLocaleTimeString('en-US', { timeZoneName: 'short' });
1338
+ const parts = timeZoneString.split(' ');
1339
+ return parts[parts.length - 1]; // Last part is timezone abbreviation
1340
+ }
1341
+
1342
+ // Format relative time ("3 hours ago", "2 days ago")
1343
+ function formatRelativeTime(diffMs) {
1344
+ const seconds = Math.floor(diffMs / 1000);
1345
+ const minutes = Math.floor(seconds / 60);
1346
+ const hours = Math.floor(minutes / 60);
1347
+ const days = Math.floor(hours / 24);
1348
+ const months = Math.floor(days / 30);
1349
+ const years = Math.floor(days / 365);
1350
+
1351
+ if (seconds < 60) {
1352
+ return seconds <= 1 ? '1 second ago' : seconds + ' seconds ago';
1353
+ } else if (minutes < 60) {
1354
+ return minutes === 1 ? '1 minute ago' : minutes + ' minutes ago';
1355
+ } else if (hours < 24) {
1356
+ return hours === 1 ? '1 hour ago' : hours + ' hours ago';
1357
+ } else if (days < 30) {
1358
+ return days === 1 ? '1 day ago' : days + ' days ago';
1359
+ } else if (months < 12) {
1360
+ return months === 1 ? '1 month ago' : months + ' months ago';
1361
+ } else {
1362
+ return years === 1 ? '1 year ago' : years + ' years ago';
1363
+ }
1364
+ }
1365
+
1366
+ // Run conversion on page load
1367
+ convertToLocalTime();
1368
+
1369
+ // Also run after Turbo navigation (if using Turbo/Hotwire)
1370
+ if (typeof Turbo !== 'undefined') {
1371
+ document.addEventListener('turbo:load', convertToLocalTime);
1372
+ document.addEventListener('turbo:frame-load', convertToLocalTime);
1373
+ }
1191
1374
  });
1192
1375
  </script>
1193
1376
  </body>
@@ -36,8 +36,8 @@
36
36
  </td>
37
37
  <td onclick="window.location='<%= error_path(error) %>';">
38
38
  <small>
39
- <strong>First:</strong> <%= error.first_seen_at&.strftime("%m/%d %I:%M%p") || 'N/A' %><br>
40
- <strong>Last:</strong> <%= error.last_seen_at&.strftime("%m/%d %I:%M%p") || 'N/A' %>
39
+ <strong>First:</strong> <%= local_time(error.first_seen_at, format: :short) %><br>
40
+ <strong>Last:</strong> <%= local_time(error.last_seen_at, format: :short) %>
41
41
  </small>
42
42
  </td>
43
43
  <% if local_assigns[:show_platform] %>
@@ -48,7 +48,7 @@
48
48
  </div>
49
49
 
50
50
  <small class="text-muted">
51
- <%= time_ago_in_words(error.occurred_at) %> ago
51
+ <%= local_time_ago(error.occurred_at) %>
52
52
  </small>
53
53
  </div>
54
54
 
@@ -386,8 +386,8 @@
386
386
  <td><code class="small"><%= error[:error_type] %></code></td>
387
387
  <td><span class="badge bg-danger"><%= error[:total_occurrences] %></span></td>
388
388
  <td><%= error[:duration_days] %> days</td>
389
- <td><small class="text-muted"><%= error[:first_seen].strftime("%b %d, %Y") %></small></td>
390
- <td><small class="text-muted"><%= error[:last_seen].strftime("%b %d, %Y %H:%M") %></small></td>
389
+ <td><small class="text-muted"><%= local_time(error[:first_seen], format: :date_only) %></small></td>
390
+ <td><small class="text-muted"><%= local_time(error[:last_seen], format: :datetime) %></small></td>
391
391
  <td>
392
392
  <% if error[:still_active] %>
393
393
  <span class="badge bg-warning">Active</span>
@@ -10,7 +10,7 @@
10
10
  <h2 class="mb-0"><i class="bi bi-bug-fill text-primary"></i> Error Overview</h2>
11
11
  <div class="text-muted">
12
12
  <small>
13
- Last updated: <%= Time.current.strftime("%B %d, %Y %I:%M %p") %>
13
+ Last updated: <%= local_time(Time.current, format: :full) %>
14
14
  <span class="badge bg-success ms-2" id="live-indicator">
15
15
  <i class="bi bi-broadcast"></i> Live
16
16
  </span>
@@ -8,7 +8,7 @@
8
8
  </h1>
9
9
  <div class="text-muted">
10
10
  <small>
11
- Last updated: <%= Time.current.strftime("%B %d, %Y %I:%M %p") %>
11
+ Last updated: <%= local_time(Time.current, format: :full) %>
12
12
  </small>
13
13
  </div>
14
14
  </div>
@@ -199,7 +199,7 @@
199
199
  <p class="mb-1 small"><%= error.message&.truncate(100) %></p>
200
200
  <small class="text-muted">
201
201
  <i class="bi bi-clock me-1"></i>
202
- <%= time_ago_in_words(error.occurred_at) %> ago
202
+ <%= local_time_ago(error.occurred_at) %>
203
203
 
204
204
  <i class="bi bi-arrow-repeat me-1"></i>
205
205
  <%= error.occurrence_count %> occurrence<%= error.occurrence_count != 1 ? 's' : '' %>
@@ -436,8 +436,8 @@
436
436
  <% end %>
437
437
  </div>
438
438
  <small class="text-muted">
439
- <%= comment.formatted_time %>
440
- <span class="ms-1 text-muted">(<%= time_ago_in_words(comment.created_at) %> ago)</span>
439
+ <%= local_time(comment.created_at, format: :datetime) %>
440
+ <span class="ms-1 text-muted">(<%= local_time_ago(comment.created_at) %>)</span>
441
441
  </small>
442
442
  </div>
443
443
  <div class="text-break">
@@ -682,20 +682,20 @@
682
682
  <small class="text-muted d-block mb-1">First Seen</small>
683
683
  <% if @related_errors.any? %>
684
684
  <%= link_to "#timeline", class: "text-decoration-none", data: { bs_toggle: "tooltip" }, title: "Jump to timeline" do %>
685
- <strong><%= @error.first_seen_at&.strftime("%B %d, %Y") || 'N/A' %></strong><br>
686
- <small><%= @error.first_seen_at&.strftime("%I:%M:%S %p %Z") || 'N/A' %></small>
685
+ <strong><%= local_time(@error.first_seen_at, format: :date_only) %></strong><br>
686
+ <small><%= local_time(@error.first_seen_at, format: :time_only) %></small>
687
687
  <i class="bi bi-arrow-down-circle ms-1"></i>
688
688
  <% end %>
689
689
  <% else %>
690
- <strong><%= @error.first_seen_at&.strftime("%B %d, %Y") || 'N/A' %></strong><br>
691
- <small><%= @error.first_seen_at&.strftime("%I:%M:%S %p %Z") || 'N/A' %></small>
690
+ <strong><%= local_time(@error.first_seen_at, format: :date_only) %></strong><br>
691
+ <small><%= local_time(@error.first_seen_at, format: :time_only) %></small>
692
692
  <% end %>
693
693
  </div>
694
694
 
695
695
  <div class="mb-3">
696
696
  <small class="text-muted d-block mb-1">Last Seen</small>
697
- <strong><%= @error.last_seen_at&.strftime("%B %d, %Y") || 'N/A' %></strong><br>
698
- <small><%= @error.last_seen_at&.strftime("%I:%M:%S %p %Z") || 'N/A' %></small>
697
+ <strong><%= local_time(@error.last_seen_at, format: :date_only) %></strong><br>
698
+ <small><%= local_time(@error.last_seen_at, format: :time_only) %></small>
699
699
  </div>
700
700
 
701
701
  <div class="mb-3">
@@ -751,7 +751,7 @@
751
751
  <% if @error.resolved_at.present? %>
752
752
  <br>
753
753
  <small class="text-muted mt-1 d-block">
754
- <%= @error.resolved_at.strftime("%B %d, %Y at %I:%M %p") %>
754
+ <%= local_time(@error.resolved_at, format: :full) %>
755
755
  </small>
756
756
  <% end %>
757
757
  <% else %>
@@ -773,7 +773,7 @@
773
773
  <% if @error.assigned_at.present? %>
774
774
  <br>
775
775
  <small class="text-muted">
776
- <%= time_ago_in_words(@error.assigned_at) %> ago
776
+ <%= local_time_ago(@error.assigned_at) %>
777
777
  </small>
778
778
  <% end %>
779
779
  </div>
@@ -809,7 +809,7 @@
809
809
  <div class="alert alert-warning py-2 mb-2">
810
810
  <i class="bi bi-alarm"></i>
811
811
  <strong>Snoozed</strong><br>
812
- <small>Until <%= @error.snoozed_until&.strftime("%b %d at %I:%M %p") %></small>
812
+ <small>Until <%= local_time(@error.snoozed_until, format: :datetime) %></small>
813
813
  </div>
814
814
  <%= button_to unsnooze_error_path(@error), method: :patch, class: "btn btn-sm btn-outline-warning" do %>
815
815
  <i class="bi bi-alarm-fill"></i> Unsnooze
@@ -971,7 +971,7 @@
971
971
  <small class="text-muted">
972
972
  Sample: <%= baseline.respond_to?(:sample_size) ? baseline.sample_size : 'N/A' %> periods
973
973
  <span class="text-muted">|</span>
974
- Updated: <%= baseline.respond_to?(:updated_at) ? baseline.updated_at&.strftime("%m/%d %I:%M%p") : 'N/A' %>
974
+ Updated: <%= baseline.respond_to?(:updated_at) ? local_time(baseline.updated_at, format: :short) : 'N/A' %>
975
975
  </small>
976
976
  </div>
977
977
  </div>
@@ -1,3 +1,3 @@
1
1
  module RailsErrorDashboard
2
- VERSION = "0.1.17"
2
+ VERSION = "0.1.18"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_error_dashboard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.17
4
+ version: 0.1.18
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anjan Jagirdar
@@ -379,7 +379,7 @@ metadata:
379
379
  source_code_uri: https://github.com/AnjanJ/rails_error_dashboard
380
380
  changelog_uri: https://github.com/AnjanJ/rails_error_dashboard/blob/main/CHANGELOG.md
381
381
  post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
382
- \ Rails Error Dashboard v0.1.17\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
382
+ \ Rails Error Dashboard v0.1.18\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F195
383
383
  First time? Quick start:\n rails generate rails_error_dashboard:install\n rails
384
384
  db:migrate\n # Add to config/routes.rb:\n mount RailsErrorDashboard::Engine
385
385
  => '/error_dashboard'\n\n\U0001F504 Upgrading from v0.1.x?\n rails db:migrate\n