rails_error_dashboard 0.1.3 → 0.1.4

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.
@@ -146,6 +146,22 @@ module RailsErrorDashboard
146
146
  @resolution_rate = analytics[:resolution_rate]
147
147
  @mobile_errors = analytics[:mobile_errors]
148
148
  @api_errors = analytics[:api_errors]
149
+
150
+ # Get recurring issues data
151
+ recurring = Queries::RecurringIssues.call(days)
152
+ @recurring_data = recurring
153
+
154
+ # Get release correlation data
155
+ correlation = Queries::ErrorCorrelation.new(days: days)
156
+ @errors_by_version = correlation.errors_by_version
157
+ @problematic_releases = correlation.problematic_releases
158
+ @release_comparison = calculate_release_comparison
159
+
160
+ # Get MTTR data
161
+ mttr_data = Queries::MttrStats.call(days)
162
+ @mttr_stats = mttr_data
163
+ @overall_mttr = mttr_data[:overall_mttr]
164
+ @mttr_by_platform = mttr_data[:mttr_by_platform]
149
165
  end
150
166
 
151
167
  def platform_comparison
@@ -221,6 +237,26 @@ module RailsErrorDashboard
221
237
 
222
238
  private
223
239
 
240
+ def calculate_release_comparison
241
+ return {} if @errors_by_version.empty? || @errors_by_version.count < 2
242
+
243
+ versions_sorted = @errors_by_version.sort_by { |_, data| data[:last_seen] || Time.at(0) }.reverse
244
+ latest = versions_sorted.first
245
+ previous = versions_sorted.second
246
+
247
+ return {} if latest.nil? || previous.nil?
248
+
249
+ {
250
+ latest_version: latest[0],
251
+ latest_count: latest[1][:count],
252
+ latest_critical: latest[1][:critical_count],
253
+ previous_version: previous[0],
254
+ previous_count: previous[1][:count],
255
+ previous_critical: previous[1][:critical_count],
256
+ change_percentage: previous[1][:count] > 0 ? ((latest[1][:count] - previous[1][:count]).to_f / previous[1][:count] * 100).round(1) : 0.0
257
+ }
258
+ end
259
+
224
260
  def filter_params
225
261
  {
226
262
  error_type: params[:error_type],
@@ -228,11 +264,16 @@ module RailsErrorDashboard
228
264
  platform: params[:platform],
229
265
  search: params[:search],
230
266
  severity: params[:severity],
267
+ timeframe: params[:timeframe],
268
+ frequency: params[:frequency],
231
269
  # Phase 3: Workflow filter params
232
270
  status: params[:status],
233
271
  assigned_to: params[:assigned_to],
234
272
  priority_level: params[:priority_level],
235
- hide_snoozed: params[:hide_snoozed]
273
+ hide_snoozed: params[:hide_snoozed],
274
+ # Sorting params
275
+ sort_by: params[:sort_by],
276
+ sort_direction: params[:sort_direction]
236
277
  }
237
278
  end
238
279
 
@@ -55,5 +55,43 @@ module RailsErrorDashboard
55
55
  "var(--text-color)"
56
56
  end
57
57
  end
58
+
59
+ # Returns the current user name for filtering "My Errors"
60
+ # Uses configured dashboard username or system username
61
+ # @return [String] Current user identifier
62
+ def current_user_name
63
+ RailsErrorDashboard.configuration.dashboard_username || ENV["USER"] || "unknown"
64
+ end
65
+
66
+ # Generates a sortable column header link
67
+ # @param label [String] The column label to display
68
+ # @param column [String] The column name to sort by
69
+ # @return [String] HTML safe link with sort indicator
70
+ def sortable_header(label, column)
71
+ current_sort = params[:sort_by]
72
+ current_direction = params[:sort_direction] || "desc"
73
+
74
+ # Determine new direction: if clicking same column, toggle; otherwise default to desc
75
+ new_direction = if current_sort == column
76
+ current_direction == "asc" ? "desc" : "asc"
77
+ else
78
+ "desc"
79
+ end
80
+
81
+ # Choose icon based on current state
82
+ icon = if current_sort == column
83
+ current_direction == "asc" ? "▲" : "▼"
84
+ else
85
+ "⇅" # Unsorted indicator
86
+ end
87
+
88
+ # Preserve existing filter params while adding sort params
89
+ link_params = params.permit!.to_h.merge(sort_by: column, sort_direction: new_direction)
90
+
91
+ link_to errors_path(link_params), class: "text-decoration-none" do
92
+ content_tag(:span, "#{label} ", class: current_sort == column ? "fw-bold" : "") +
93
+ content_tag(:span, icon, class: "text-muted small")
94
+ end
95
+ end
58
96
  end
59
97
  end
@@ -133,6 +133,11 @@ module RailsErrorDashboard
133
133
  SystemStackError
134
134
  SignalException
135
135
  ActiveRecord::StatementInvalid
136
+ LoadError
137
+ SyntaxError
138
+ ActiveRecord::ConnectionNotEstablished
139
+ Redis::ConnectionError
140
+ OpenSSL::SSL::SSLError
136
141
  ].freeze
137
142
 
138
143
  HIGH_SEVERITY_ERROR_TYPES = %w[
@@ -141,6 +146,11 @@ module RailsErrorDashboard
141
146
  TypeError
142
147
  NoMethodError
143
148
  NameError
149
+ ZeroDivisionError
150
+ FloatDomainError
151
+ IndexError
152
+ KeyError
153
+ RangeError
144
154
  ].freeze
145
155
 
146
156
  MEDIUM_SEVERITY_ERROR_TYPES = %w[
@@ -148,6 +158,10 @@ module RailsErrorDashboard
148
158
  Timeout::Error
149
159
  Net::ReadTimeout
150
160
  Net::OpenTimeout
161
+ ActiveRecord::RecordNotUnique
162
+ JSON::ParserError
163
+ CSV::MalformedCSVError
164
+ Errno::ECONNREFUSED
151
165
  ].freeze
152
166
 
153
167
  # Find existing error by hash or create new one
@@ -1,16 +1,54 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <title>Rails error dashboard</title>
4
+ <title>Rails Error Dashboard</title>
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
7
 
8
8
  <%= yield :head %>
9
9
 
10
10
  <%= stylesheet_link_tag "rails_error_dashboard/application", media: "all" %>
11
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
12
+ <script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script>
11
13
  </head>
12
14
  <body>
13
15
 
16
+ <!-- Navigation -->
17
+ <nav class="navbar navbar-expand-lg navbar-dark bg-dark">
18
+ <div class="container-fluid">
19
+ <%= link_to "Error Dashboard", root_path, class: "navbar-brand" %>
20
+ <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
21
+ <span class="navbar-toggler-icon"></span>
22
+ </button>
23
+ <div class="collapse navbar-collapse" id="navbarNav">
24
+ <ul class="navbar-nav me-auto">
25
+ <li class="nav-item">
26
+ <%= link_to overview_path, class: "nav-link #{controller_name == 'errors' && action_name == 'overview' ? 'active' : ''}" do %>
27
+ <i class="bi bi-speedometer2"></i> Dashboard
28
+ <% end %>
29
+ </li>
30
+ <li class="nav-item">
31
+ <%= link_to errors_path, class: "nav-link #{controller_name == 'errors' && action_name == 'index' ? 'active' : ''}" do %>
32
+ <i class="bi bi-list-ul"></i> Errors
33
+ <% end %>
34
+ </li>
35
+ <li class="nav-item">
36
+ <%= link_to analytics_errors_path, class: "nav-link #{controller_name == 'errors' && action_name == 'analytics' ? 'active' : ''}" do %>
37
+ <i class="bi bi-graph-up"></i> Analytics
38
+ <% end %>
39
+ </li>
40
+ </ul>
41
+ <ul class="navbar-nav">
42
+ <li class="nav-item">
43
+ <a class="nav-link" href="#" id="theme-toggle">
44
+ <i class="bi bi-moon-stars"></i>
45
+ </a>
46
+ </li>
47
+ </ul>
48
+ </div>
49
+ </div>
50
+ </nav>
51
+
14
52
  <%= yield %>
15
53
 
16
54
  </body>
@@ -330,4 +330,305 @@
330
330
  </div>
331
331
  </div>
332
332
  </div>
333
+
334
+ <!-- Recurring Issues Analysis -->
335
+ <div class="card mb-4">
336
+ <div class="card-header bg-white">
337
+ <h5 class="mb-0"><i class="bi bi-arrow-repeat"></i> Recurring Issues</h5>
338
+ </div>
339
+ <div class="card-body">
340
+ <% if @recurring_data[:high_frequency_errors].any? %>
341
+ <h6 class="text-muted mb-3">High Frequency Errors</h6>
342
+ <div class="table-responsive mb-4">
343
+ <table class="table table-sm">
344
+ <thead>
345
+ <tr>
346
+ <th>Error Type</th>
347
+ <th>Total Occurrences</th>
348
+ <th>Duration</th>
349
+ <th>First Seen</th>
350
+ <th>Last Seen</th>
351
+ <th>Status</th>
352
+ </tr>
353
+ </thead>
354
+ <tbody>
355
+ <% @recurring_data[:high_frequency_errors].first(10).each do |error| %>
356
+ <tr>
357
+ <td><code class="small"><%= error[:error_type] %></code></td>
358
+ <td><span class="badge bg-danger"><%= error[:total_occurrences] %></span></td>
359
+ <td><%= error[:duration_days] %> days</td>
360
+ <td><small class="text-muted"><%= error[:first_seen].strftime("%b %d, %Y") %></small></td>
361
+ <td><small class="text-muted"><%= error[:last_seen].strftime("%b %d, %Y %H:%M") %></small></td>
362
+ <td>
363
+ <% if error[:still_active] %>
364
+ <span class="badge bg-warning">Active</span>
365
+ <% else %>
366
+ <span class="badge bg-secondary">Inactive</span>
367
+ <% end %>
368
+ </td>
369
+ </tr>
370
+ <% end %>
371
+ </tbody>
372
+ </table>
373
+ </div>
374
+ <% else %>
375
+ <p class="text-muted">No high-frequency errors found in the last <%= @days %> days.</p>
376
+ <% end %>
377
+
378
+ <% if @recurring_data[:persistent_errors].any? %>
379
+ <h6 class="text-muted mb-3 mt-4">Persistent Unresolved Errors</h6>
380
+ <div class="alert alert-info">
381
+ <i class="bi bi-info-circle"></i> These errors have been unresolved for more than 7 days.
382
+ </div>
383
+ <div class="table-responsive">
384
+ <table class="table table-sm">
385
+ <thead>
386
+ <tr>
387
+ <th>Error Type</th>
388
+ <th>Message</th>
389
+ <th>Platform</th>
390
+ <th>Count</th>
391
+ <th>Age</th>
392
+ <th>Actions</th>
393
+ </tr>
394
+ </thead>
395
+ <tbody>
396
+ <% @recurring_data[:persistent_errors].first(10).each do |error| %>
397
+ <tr>
398
+ <td><code class="small"><%= error[:error_type] %></code></td>
399
+ <td class="text-muted small"><%= error[:message] %></td>
400
+ <td><%= error[:platform] %></td>
401
+ <td><span class="badge bg-secondary"><%= error[:occurrence_count] %>x</span></td>
402
+ <td>
403
+ <span class="badge bg-warning"><%= error[:age_days] %> days</span>
404
+ </td>
405
+ <td>
406
+ <%= link_to "View", error_path(error[:id]), class: "btn btn-sm btn-outline-primary" %>
407
+ </td>
408
+ </tr>
409
+ <% end %>
410
+ </tbody>
411
+ </table>
412
+ </div>
413
+ <% end %>
414
+ </div>
415
+ </div>
416
+
417
+ <!-- Release Comparison Charts -->
418
+ <% if @errors_by_version.present? && @errors_by_version.count >= 2 %>
419
+ <div class="row g-4 mb-4">
420
+ <!-- Errors by Version Chart -->
421
+ <div class="col-md-8">
422
+ <div class="card">
423
+ <div class="card-header bg-white">
424
+ <h5 class="mb-0"><i class="bi bi-git"></i> Errors by Release Version</h5>
425
+ </div>
426
+ <div class="card-body">
427
+ <div id="errors-by-version-chart"></div>
428
+ <script>
429
+ document.addEventListener('DOMContentLoaded', function() {
430
+ const versionData = <%= raw @errors_by_version.transform_values { |v| v[:count] }.to_json %>;
431
+ const colors = window.getChartColors();
432
+
433
+ new Chartkick.ColumnChart("errors-by-version-chart", versionData, {
434
+ colors: ["#8B5CF6", "#EF4444", "#F59E0B", "#10B981"],
435
+ height: "300px",
436
+ xtitle: "Version",
437
+ ytitle: "Error Count",
438
+ library: window.getChartLibraryOptions()
439
+ });
440
+ });
441
+ </script>
442
+ </div>
443
+ </div>
444
+ </div>
445
+
446
+ <!-- Release Comparison Card -->
447
+ <div class="col-md-4">
448
+ <div class="card">
449
+ <div class="card-header bg-white">
450
+ <h5 class="mb-0">Latest vs Previous</h5>
451
+ </div>
452
+ <div class="card-body">
453
+ <% if @release_comparison.present? %>
454
+ <div class="mb-3">
455
+ <small class="text-muted">Latest Version</small>
456
+ <h4><%= @release_comparison[:latest_version] %></h4>
457
+ <p class="mb-0">
458
+ <span class="badge bg-danger"><%= @release_comparison[:latest_count] %> errors</span>
459
+ <% if @release_comparison[:latest_critical] > 0 %>
460
+ <span class="badge bg-dark ms-1"><%= @release_comparison[:latest_critical] %> critical</span>
461
+ <% end %>
462
+ </p>
463
+ </div>
464
+ <div class="mb-3">
465
+ <small class="text-muted">Previous Version</small>
466
+ <h4><%= @release_comparison[:previous_version] %></h4>
467
+ <p class="mb-0">
468
+ <span class="badge bg-secondary"><%= @release_comparison[:previous_count] %> errors</span>
469
+ <% if @release_comparison[:previous_critical] > 0 %>
470
+ <span class="badge bg-secondary ms-1"><%= @release_comparison[:previous_critical] %> critical</span>
471
+ <% end %>
472
+ </p>
473
+ </div>
474
+ <hr>
475
+ <div>
476
+ <small class="text-muted">Change</small>
477
+ <h4 class="<%= @release_comparison[:change_percentage] > 0 ? 'text-danger' : 'text-success' %>">
478
+ <%= @release_comparison[:change_percentage] > 0 ? '↑' : '↓' %>
479
+ <%= @release_comparison[:change_percentage].abs %>%
480
+ </h4>
481
+ </div>
482
+ <% else %>
483
+ <p class="text-muted">Not enough release data for comparison.</p>
484
+ <% end %>
485
+ </div>
486
+ </div>
487
+ </div>
488
+ </div>
489
+
490
+ <!-- Problematic Releases Alert -->
491
+ <% if @problematic_releases.any? %>
492
+ <div class="alert alert-warning mb-4">
493
+ <h5 class="alert-heading"><i class="bi bi-exclamation-triangle"></i> Problematic Releases Detected</h5>
494
+ <p>The following releases have significantly higher error rates (&gt;2x average):</p>
495
+ <ul class="mb-0">
496
+ <% @problematic_releases.each do |release| %>
497
+ <li>
498
+ <strong><%= release[:version] %></strong>:
499
+ <%= release[:error_count] %> errors
500
+ (<%= release[:critical_count] %> critical,
501
+ +<%= release[:deviation_from_avg] %>% from average)
502
+ </li>
503
+ <% end %>
504
+ </ul>
505
+ </div>
506
+ <% end %>
507
+ <% end %>
508
+
509
+ <!-- MTTR (Mean Time to Resolution) -->
510
+ <% if @mttr_stats[:total_resolved] > 0 %>
511
+ <div class="card mb-4">
512
+ <div class="card-header bg-white">
513
+ <h5 class="mb-0"><i class="bi bi-clock-history"></i> Resolution Performance (MTTR)</h5>
514
+ </div>
515
+ <div class="card-body">
516
+ <div class="row g-3 mb-4">
517
+ <!-- Overall MTTR -->
518
+ <div class="col-md-3">
519
+ <div class="card border-info">
520
+ <div class="card-body text-center">
521
+ <small class="text-muted d-block">Overall MTTR</small>
522
+ <h3 class="text-info mb-0"><%= @mttr_stats[:overall_mttr] %>h</h3>
523
+ <small class="text-muted">Average resolution time</small>
524
+ </div>
525
+ </div>
526
+ </div>
527
+
528
+ <!-- Fastest -->
529
+ <div class="col-md-3">
530
+ <div class="card border-success">
531
+ <div class="card-body text-center">
532
+ <small class="text-muted d-block">Fastest</small>
533
+ <h3 class="text-success mb-0"><%= @mttr_stats[:fastest_resolution] || 0 %>m</h3>
534
+ <small class="text-muted">Best resolution time</small>
535
+ </div>
536
+ </div>
537
+ </div>
538
+
539
+ <!-- Slowest -->
540
+ <div class="col-md-3">
541
+ <div class="card border-danger">
542
+ <div class="card-body text-center">
543
+ <small class="text-muted d-block">Slowest</small>
544
+ <h3 class="text-danger mb-0"><%= @mttr_stats[:slowest_resolution] || 0 %>h</h3>
545
+ <small class="text-muted">Longest resolution time</small>
546
+ </div>
547
+ </div>
548
+ </div>
549
+
550
+ <!-- Total Resolved -->
551
+ <div class="col-md-3">
552
+ <div class="card border-secondary">
553
+ <div class="card-body text-center">
554
+ <small class="text-muted d-block">Total Resolved</small>
555
+ <h3 class="text-secondary mb-0"><%= @mttr_stats[:total_resolved] %></h3>
556
+ <small class="text-muted">In last <%= @days %> days</small>
557
+ </div>
558
+ </div>
559
+ </div>
560
+ </div>
561
+
562
+ <!-- MTTR by Platform -->
563
+ <% if @mttr_by_platform.present? && @mttr_by_platform.any? %>
564
+ <div class="mt-4">
565
+ <h6 class="text-muted mb-3">MTTR by Platform</h6>
566
+ <div id="mttr-by-platform-chart"></div>
567
+ <script>
568
+ document.addEventListener('DOMContentLoaded', function() {
569
+ const colors = window.getChartColors();
570
+ new Chartkick.BarChart("mttr-by-platform-chart",
571
+ <%= raw @mttr_by_platform.to_json %>, {
572
+ suffix: " hours",
573
+ height: "200px",
574
+ colors: ["#8B5CF6"],
575
+ library: window.getChartLibraryOptions()
576
+ });
577
+ });
578
+ </script>
579
+ </div>
580
+ <% end %>
581
+
582
+ <!-- MTTR by Severity -->
583
+ <% if @mttr_stats[:mttr_by_severity].present? && @mttr_stats[:mttr_by_severity].any? %>
584
+ <div class="mt-4">
585
+ <h6 class="text-muted mb-3">MTTR by Severity</h6>
586
+ <div class="table-responsive">
587
+ <table class="table table-sm">
588
+ <thead>
589
+ <tr>
590
+ <th>Severity</th>
591
+ <th>Average Time to Resolution</th>
592
+ </tr>
593
+ </thead>
594
+ <tbody>
595
+ <% @mttr_stats[:mttr_by_severity].each do |severity, hours| %>
596
+ <tr>
597
+ <td>
598
+ <span class="badge bg-<%= severity_color(severity) %>">
599
+ <%= severity.to_s.capitalize %>
600
+ </span>
601
+ </td>
602
+ <td><strong><%= hours %> hours</strong></td>
603
+ </tr>
604
+ <% end %>
605
+ </tbody>
606
+ </table>
607
+ </div>
608
+ </div>
609
+ <% end %>
610
+
611
+ <!-- MTTR Trend -->
612
+ <% if @mttr_stats[:mttr_trend].present? && @mttr_stats[:mttr_trend].any? %>
613
+ <div class="mt-4">
614
+ <h6 class="text-muted mb-3">MTTR Trend (Weekly)</h6>
615
+ <div id="mttr-trend-chart"></div>
616
+ <script>
617
+ document.addEventListener('DOMContentLoaded', function() {
618
+ const colors = window.getChartColors();
619
+ new Chartkick.LineChart("mttr-trend-chart",
620
+ <%= raw @mttr_stats[:mttr_trend].to_json %>, {
621
+ suffix: " hours",
622
+ height: "250px",
623
+ colors: ["#10B981"],
624
+ curve: false,
625
+ library: window.getChartLibraryOptions()
626
+ });
627
+ });
628
+ </script>
629
+ </div>
630
+ <% end %>
631
+ </div>
632
+ </div>
633
+ <% end %>
333
634
  </div>
@@ -1,5 +1,7 @@
1
- <!-- Subscribe to Turbo Stream updates -->
2
- <%= turbo_stream_from "error_list" %>
1
+ <!-- Subscribe to Turbo Stream updates (only if ActionCable is available) -->
2
+ <% if defined?(ActionCable) %>
3
+ <%= turbo_stream_from "error_list" %>
4
+ <% end %>
3
5
 
4
6
  <div class="py-4">
5
7
  <div class="d-flex justify-content-between align-items-center mb-4">
@@ -124,6 +126,16 @@
124
126
  <h5 class="mb-0">Filters & Search</h5>
125
127
  </div>
126
128
  <div class="card-body">
129
+ <!-- Quick Filter Buttons -->
130
+ <div class="d-flex gap-2 mb-3">
131
+ <%= link_to "All Errors", errors_path,
132
+ class: "btn btn-sm #{params[:assigned_to].blank? ? 'btn-primary' : 'btn-outline-primary'}" %>
133
+ <%= link_to "Unassigned", errors_path(assigned_to: '__unassigned__'),
134
+ class: "btn btn-sm #{params[:assigned_to] == '__unassigned__' ? 'btn-primary' : 'btn-outline-primary'}" %>
135
+ <%= link_to "My Errors", errors_path(assigned_to: current_user_name),
136
+ class: "btn btn-sm #{params[:assigned_to] == current_user_name ? 'btn-primary' : 'btn-outline-primary'}" %>
137
+ </div>
138
+
127
139
  <%= form_with url: errors_path, method: :get, class: "row g-3", data: { turbo: false } do %>
128
140
  <div class="col-md-4">
129
141
  <%= text_field_tag :search, params[:search], placeholder: "Search errors...", class: "form-control" %>
@@ -149,6 +161,31 @@
149
161
  ], params[:severity]), class: "form-select" %>
150
162
  </div>
151
163
 
164
+ <!-- Time Range Filter -->
165
+ <div class="col-md-2">
166
+ <%= select_tag :timeframe, options_for_select([
167
+ ['All Time', ''],
168
+ ['Last Hour', 'last_hour'],
169
+ ['Today', 'today'],
170
+ ['Yesterday', 'yesterday'],
171
+ ['Last 7 Days', 'last_7_days'],
172
+ ['Last 30 Days', 'last_30_days'],
173
+ ['Last 90 Days', 'last_90_days']
174
+ ], params[:timeframe]), class: "form-select" %>
175
+ </div>
176
+
177
+ <!-- Frequency Filter -->
178
+ <div class="col-md-2">
179
+ <%= select_tag :frequency, options_for_select([
180
+ ['All Frequencies', ''],
181
+ ['Once', 'once'],
182
+ ['2-9 Times', 'few'],
183
+ ['10-99 Times', 'frequent'],
184
+ ['100+ Times', 'very_frequent'],
185
+ ['Recurring (Active)', 'recurring']
186
+ ], params[:frequency]), class: "form-select" %>
187
+ </div>
188
+
152
189
  <!-- Phase 3: Workflow Filters -->
153
190
  <div class="col-md-2">
154
191
  <%= select_tag :status, options_for_select([
@@ -253,13 +290,13 @@
253
290
  <th style="width: 40px;">
254
291
  <input type="checkbox" id="select-all" class="form-check-input">
255
292
  </th>
256
- <th>Severity</th>
257
- <th>Error Type</th>
293
+ <th><%= sortable_header("Severity", "severity") %></th>
294
+ <th><%= sortable_header("Error Type", "error_type") %></th>
258
295
  <th>Message</th>
259
- <th>Occurrences</th>
260
- <th>First / Last Seen</th>
296
+ <th><%= sortable_header("Occurrences", "occurrence_count") %></th>
297
+ <th><%= sortable_header("First / Last Seen", "last_seen_at") %></th>
261
298
  <% if @platforms.size > 1 %>
262
- <th>Platform</th>
299
+ <th><%= sortable_header("Platform", "platform") %></th>
263
300
  <% end %>
264
301
  <th>Status</th>
265
302
  <th></th>
data/config/routes.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  RailsErrorDashboard::Engine.routes.draw do
2
- root to: "errors#index"
2
+ root to: "errors#overview"
3
+
4
+ # Dashboard overview
5
+ get "overview", to: "errors#overview", as: :overview
3
6
 
4
7
  resources :errors, only: [ :index, :show ] do
5
8
  member do