rails_error_dashboard 0.1.6 → 0.1.7
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 +9 -0
- data/app/controllers/rails_error_dashboard/errors_controller.rb +8 -5
- data/app/models/rails_error_dashboard/error_log.rb +17 -0
- data/app/views/rails_error_dashboard/errors/show.html.erb +6 -0
- data/db/migrate/20251229111223_add_additional_performance_indexes.rb +75 -0
- data/lib/rails_error_dashboard/configuration.rb +8 -0
- data/lib/rails_error_dashboard/engine.rb +5 -0
- data/lib/rails_error_dashboard/middleware/rate_limiter.rb +137 -0
- data/lib/rails_error_dashboard/queries/analytics_stats.rb +31 -13
- data/lib/rails_error_dashboard/queries/dashboard_stats.rb +46 -27
- data/lib/rails_error_dashboard/queries/errors_list.rb +13 -6
- data/lib/rails_error_dashboard/version.rb +1 -1
- data/lib/rails_error_dashboard.rb +1 -0
- metadata +13 -2
- data/app/views/layouts/rails_error_dashboard_old_backup.html.erb +0 -383
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b69a15c7ce0fdedb38654c9b4747fe8e1804419fd19ab15182182a02fa2d9fa6
|
|
4
|
+
data.tar.gz: d92fb59ecb07d9b59fbf2525e1d5a46bb0a59ff37cf259d20d3223e210d33fd9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dbdb45fbc0015b4b293b8d3648403d68a117c43db16e5dff572317821fa3b0f9e9592ee6e921fef9fda6fd471126fe2c9b9f36a8d49bd1cbdda913c2d2833d51
|
|
7
|
+
data.tar.gz: 1da1a8275bf29468e2295334655748d00bc472ab99be2fca316e0217675ce86cbd8f07ba10b3113a7857a75d4d703231fcd743eb2b4c01fc3ed4f41d9cc9d084
|
data/README.md
CHANGED
|
@@ -16,6 +16,14 @@ gem 'rails_error_dashboard'
|
|
|
16
16
|
|
|
17
17
|
**5-minute setup** · **Works out-of-the-box** · **100% Rails + Postgres** · **No vendor lock-in**
|
|
18
18
|
|
|
19
|
+
### 🎮 Try the Live Demo
|
|
20
|
+
|
|
21
|
+
**See it in action:** [https://rails-error-dashboard.anjan.dev](https://rails-error-dashboard.anjan.dev)
|
|
22
|
+
|
|
23
|
+
Username: `frodo` · Password: `precious`
|
|
24
|
+
|
|
25
|
+
Experience the full dashboard with 250+ realistic Rails errors, LOTR-themed demo data, and all features enabled.
|
|
26
|
+
|
|
19
27
|
---
|
|
20
28
|
|
|
21
29
|
### ⚠️ BETA SOFTWARE
|
|
@@ -406,6 +414,7 @@ config.webhook_urls = ['https://yourapp.com/hooks/errors']
|
|
|
406
414
|
|
|
407
415
|
### Development
|
|
408
416
|
- **[Testing](docs/development/TESTING.md)** - Multi-version testing
|
|
417
|
+
- **[Smoke Tests](SMOKE_TESTS.md)** - Deployment verification tests
|
|
409
418
|
|
|
410
419
|
**📖 [View all documentation →](docs/README.md)**
|
|
411
420
|
|
|
@@ -21,13 +21,13 @@ module RailsErrorDashboard
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
# Get critical alerts (critical/high severity errors from last hour)
|
|
24
|
+
# Filter by priority_level in database instead of loading all records into memory
|
|
24
25
|
@critical_alerts = ErrorLog
|
|
25
26
|
.where("occurred_at >= ?", 1.hour.ago)
|
|
26
27
|
.where(resolved_at: nil)
|
|
27
|
-
.
|
|
28
|
-
.
|
|
29
|
-
.
|
|
30
|
-
.first(10)
|
|
28
|
+
.where(priority_level: [ 3, 4 ]) # 3 = high, 4 = critical (based on severity enum)
|
|
29
|
+
.order(occurred_at: :desc)
|
|
30
|
+
.limit(10)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def index
|
|
@@ -47,7 +47,10 @@ module RailsErrorDashboard
|
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
def show
|
|
50
|
-
|
|
50
|
+
# Eagerly load associations to avoid N+1 queries
|
|
51
|
+
# - comments: Used in the comments section (@error.comments.count, @error.comments.recent_first)
|
|
52
|
+
# - parent_cascade_patterns/child_cascade_patterns: Used if cascade detection is enabled
|
|
53
|
+
@error = ErrorLog.includes(:comments, :parent_cascade_patterns, :child_cascade_patterns).find(params[:id])
|
|
51
54
|
@related_errors = @error.related_errors(limit: 5)
|
|
52
55
|
|
|
53
56
|
# Dispatch plugin event for error viewed
|
|
@@ -60,6 +60,10 @@ module RailsErrorDashboard
|
|
|
60
60
|
after_create_commit :broadcast_new_error
|
|
61
61
|
after_update_commit :broadcast_error_update
|
|
62
62
|
|
|
63
|
+
# Cache invalidation - clear analytics caches when errors are created/updated/deleted
|
|
64
|
+
after_save :clear_analytics_cache
|
|
65
|
+
after_destroy :clear_analytics_cache
|
|
66
|
+
|
|
63
67
|
def set_defaults
|
|
64
68
|
self.platform ||= "API"
|
|
65
69
|
end
|
|
@@ -663,5 +667,18 @@ module RailsErrorDashboard
|
|
|
663
667
|
100 # Default fallback
|
|
664
668
|
end
|
|
665
669
|
end
|
|
670
|
+
|
|
671
|
+
# Clear analytics caches when errors are created, updated, or destroyed
|
|
672
|
+
# This ensures dashboard and analytics always show fresh data
|
|
673
|
+
def clear_analytics_cache
|
|
674
|
+
# Use delete_matched to clear all cached analytics regardless of parameters
|
|
675
|
+
# Pattern matches: dashboard_stats/*, analytics_stats/*, platform_comparison/*
|
|
676
|
+
Rails.cache.delete_matched("dashboard_stats/*")
|
|
677
|
+
Rails.cache.delete_matched("analytics_stats/*")
|
|
678
|
+
Rails.cache.delete_matched("platform_comparison/*")
|
|
679
|
+
rescue => e
|
|
680
|
+
# Silently handle cache clearing errors to prevent blocking error logging
|
|
681
|
+
Rails.logger.error("Failed to clear analytics cache: #{e.message}") if Rails.logger
|
|
682
|
+
end
|
|
666
683
|
end
|
|
667
684
|
end
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
<div class="row g-4">
|
|
35
35
|
<!-- Error Information -->
|
|
36
36
|
<div class="col-md-8">
|
|
37
|
+
<% cache [@error, 'error_details_v1'] do %>
|
|
37
38
|
<div class="card mb-4">
|
|
38
39
|
<div class="card-header bg-danger text-white">
|
|
39
40
|
<h5 class="mb-0"><i class="bi bi-bug-fill"></i> <%= @error.error_type %></h5>
|
|
@@ -115,8 +116,10 @@
|
|
|
115
116
|
<% end %>
|
|
116
117
|
</div>
|
|
117
118
|
</div>
|
|
119
|
+
<% end %>
|
|
118
120
|
|
|
119
121
|
<!-- Request Context -->
|
|
122
|
+
<% cache [@error, 'request_context_v1'] do %>
|
|
120
123
|
<div class="card mb-4">
|
|
121
124
|
<div class="card-header bg-white">
|
|
122
125
|
<h5 class="mb-0"><i class="bi bi-globe"></i> Request Context</h5>
|
|
@@ -169,11 +172,13 @@
|
|
|
169
172
|
</table>
|
|
170
173
|
</div>
|
|
171
174
|
</div>
|
|
175
|
+
<% end %>
|
|
172
176
|
|
|
173
177
|
<!-- Similar Errors (Fuzzy Matching) -->
|
|
174
178
|
<% if RailsErrorDashboard.configuration.enable_similar_errors && @error.respond_to?(:similar_errors) %>
|
|
175
179
|
<% similar = @error.similar_errors(threshold: 0.6, limit: 5) %>
|
|
176
180
|
<% if similar.any? %>
|
|
181
|
+
<% cache [@error, 'similar_errors_v1', similar.maximum(:updated_at)] do %>
|
|
177
182
|
<div class="card mb-4">
|
|
178
183
|
<div class="card-header bg-white">
|
|
179
184
|
<h5 class="mb-0">
|
|
@@ -239,6 +244,7 @@
|
|
|
239
244
|
</div>
|
|
240
245
|
</div>
|
|
241
246
|
</div>
|
|
247
|
+
<% end %>
|
|
242
248
|
<% end %>
|
|
243
249
|
<% end %>
|
|
244
250
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddAdditionalPerformanceIndexes < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
# Composite index for workflow filtering (assigned errors with status)
|
|
6
|
+
# Common query: WHERE assigned_to = ? AND status = ? ORDER BY occurred_at DESC
|
|
7
|
+
# Used in: "Show me all errors assigned to John that are investigating"
|
|
8
|
+
add_index :rails_error_dashboard_error_logs, [ :assigned_to, :status, :occurred_at ],
|
|
9
|
+
name: 'index_error_logs_on_assignment_workflow',
|
|
10
|
+
if_not_exists: true
|
|
11
|
+
|
|
12
|
+
# Composite index for priority filtering with resolution status
|
|
13
|
+
# Common query: WHERE priority_level = ? AND resolved = ? ORDER BY occurred_at DESC
|
|
14
|
+
# Used in: "Show me all high priority unresolved errors"
|
|
15
|
+
add_index :rails_error_dashboard_error_logs, [ :priority_level, :resolved, :occurred_at ],
|
|
16
|
+
name: 'index_error_logs_on_priority_resolution',
|
|
17
|
+
if_not_exists: true
|
|
18
|
+
|
|
19
|
+
# Composite index for platform + status filtering (common in analytics)
|
|
20
|
+
# Common query: WHERE platform = ? AND status = ? ORDER BY occurred_at DESC
|
|
21
|
+
# Used in: "Show me all iOS errors that are new"
|
|
22
|
+
add_index :rails_error_dashboard_error_logs, [ :platform, :status, :occurred_at ],
|
|
23
|
+
name: 'index_error_logs_on_platform_status_time',
|
|
24
|
+
if_not_exists: true
|
|
25
|
+
|
|
26
|
+
# Composite index for version-based filtering
|
|
27
|
+
# Common query: WHERE app_version = ? AND resolved = ? ORDER BY occurred_at DESC
|
|
28
|
+
# Used in: "Show me all unresolved errors in version 2.1.0"
|
|
29
|
+
add_index :rails_error_dashboard_error_logs, [ :app_version, :resolved, :occurred_at ],
|
|
30
|
+
name: 'index_error_logs_on_version_resolution_time',
|
|
31
|
+
if_not_exists: true
|
|
32
|
+
|
|
33
|
+
# Composite index for snooze management
|
|
34
|
+
# Common query: WHERE snoozed_until IS NOT NULL AND snoozed_until < NOW()
|
|
35
|
+
# Used in: Finding errors that need to be unsnoozed
|
|
36
|
+
add_index :rails_error_dashboard_error_logs, [ :snoozed_until, :occurred_at ],
|
|
37
|
+
name: 'index_error_logs_on_snooze_time',
|
|
38
|
+
where: "snoozed_until IS NOT NULL",
|
|
39
|
+
if_not_exists: true
|
|
40
|
+
|
|
41
|
+
# Composite index for error hash lookups with time window
|
|
42
|
+
# Common query: WHERE error_hash = ? AND occurred_at >= ?
|
|
43
|
+
# Used in: Similar error detection within time windows
|
|
44
|
+
# Note: There's already an index on [error_hash, resolved, occurred_at]
|
|
45
|
+
# but this one is for time-based similarity without resolved filter
|
|
46
|
+
|
|
47
|
+
# Add GIN index for backtrace full-text search (PostgreSQL only)
|
|
48
|
+
# Improves search performance across both message and backtrace
|
|
49
|
+
if postgresql?
|
|
50
|
+
reversible do |dir|
|
|
51
|
+
dir.up do
|
|
52
|
+
execute <<-SQL
|
|
53
|
+
CREATE INDEX IF NOT EXISTS index_error_logs_on_searchable_text
|
|
54
|
+
ON rails_error_dashboard_error_logs
|
|
55
|
+
USING gin(to_tsvector('english',
|
|
56
|
+
COALESCE(message, '') || ' ' ||
|
|
57
|
+
COALESCE(backtrace, '') || ' ' ||
|
|
58
|
+
COALESCE(error_type, '')
|
|
59
|
+
))
|
|
60
|
+
SQL
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
dir.down do
|
|
64
|
+
execute "DROP INDEX IF EXISTS index_error_logs_on_searchable_text"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def postgresql?
|
|
73
|
+
ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -60,6 +60,10 @@ module RailsErrorDashboard
|
|
|
60
60
|
# Backtrace configuration
|
|
61
61
|
attr_accessor :max_backtrace_lines
|
|
62
62
|
|
|
63
|
+
# Rate limiting configuration
|
|
64
|
+
attr_accessor :enable_rate_limiting
|
|
65
|
+
attr_accessor :rate_limit_per_minute
|
|
66
|
+
|
|
63
67
|
# Enhanced metrics
|
|
64
68
|
attr_accessor :app_version
|
|
65
69
|
attr_accessor :git_sha
|
|
@@ -130,6 +134,10 @@ module RailsErrorDashboard
|
|
|
130
134
|
@async_adapter = :sidekiq # Battle-tested default
|
|
131
135
|
@max_backtrace_lines = 50
|
|
132
136
|
|
|
137
|
+
# Rate limiting defaults
|
|
138
|
+
@enable_rate_limiting = false # OFF by default (opt-in)
|
|
139
|
+
@rate_limit_per_minute = 100 # Requests per minute per IP for API endpoints
|
|
140
|
+
|
|
133
141
|
# Enhanced metrics defaults
|
|
134
142
|
@app_version = ENV["APP_VERSION"]
|
|
135
143
|
@git_sha = ENV["GIT_SHA"]
|
|
@@ -8,6 +8,11 @@ module RailsErrorDashboard
|
|
|
8
8
|
if RailsErrorDashboard.configuration.enable_middleware
|
|
9
9
|
app.config.middleware.insert_before 0, RailsErrorDashboard::Middleware::ErrorCatcher
|
|
10
10
|
end
|
|
11
|
+
|
|
12
|
+
# Add rate limiting middleware if enabled
|
|
13
|
+
if RailsErrorDashboard.configuration.enable_rate_limiting
|
|
14
|
+
app.config.middleware.use RailsErrorDashboard::Middleware::RateLimiter
|
|
15
|
+
end
|
|
11
16
|
end
|
|
12
17
|
|
|
13
18
|
# Subscribe to Rails error reporter
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsErrorDashboard
|
|
4
|
+
module Middleware
|
|
5
|
+
# Rate limiting middleware for Rails Error Dashboard routes
|
|
6
|
+
# Protects both dashboard UI and API endpoints from abuse
|
|
7
|
+
class RateLimiter
|
|
8
|
+
# Rate limits by endpoint type
|
|
9
|
+
LIMITS = {
|
|
10
|
+
# API endpoints (mobile/frontend) - stricter limits
|
|
11
|
+
"/error_dashboard/api" => { limit: 100, period: 60 }, # 100 req/min
|
|
12
|
+
|
|
13
|
+
# Dashboard pages (human users) - more lenient
|
|
14
|
+
"/error_dashboard" => { limit: 300, period: 60 } # 300 req/min
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(app)
|
|
18
|
+
@app = app
|
|
19
|
+
@cache = Rails.cache
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def call(env)
|
|
23
|
+
return @app.call(env) unless enabled?
|
|
24
|
+
|
|
25
|
+
request = Rack::Request.new(env)
|
|
26
|
+
|
|
27
|
+
# Only apply rate limiting to error dashboard routes
|
|
28
|
+
return @app.call(env) unless error_dashboard_route?(request.path)
|
|
29
|
+
|
|
30
|
+
# Find matching rate limit configuration
|
|
31
|
+
limit_config = find_limit_config(request.path)
|
|
32
|
+
return @app.call(env) unless limit_config
|
|
33
|
+
|
|
34
|
+
# Check rate limit
|
|
35
|
+
key = rate_limit_key(request)
|
|
36
|
+
current_count = @cache.read(key).to_i
|
|
37
|
+
|
|
38
|
+
if current_count >= limit_config[:limit]
|
|
39
|
+
return rate_limit_response(request, limit_config)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Increment counter with expiration
|
|
43
|
+
@cache.write(key, current_count + 1, expires_in: limit_config[:period].seconds)
|
|
44
|
+
|
|
45
|
+
@app.call(env)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def enabled?
|
|
51
|
+
RailsErrorDashboard.configuration.enable_rate_limiting
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def error_dashboard_route?(path)
|
|
55
|
+
path.start_with?("/error_dashboard")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def find_limit_config(path)
|
|
59
|
+
# Match most specific route first (API before dashboard)
|
|
60
|
+
LIMITS.find { |pattern, _| path.start_with?(pattern) }&.last
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def rate_limit_key(request)
|
|
64
|
+
# Key format: rate_limit:IP:path_prefix:time_window
|
|
65
|
+
# Time window ensures keys expire and reset
|
|
66
|
+
limit_config = find_limit_config(request.path)
|
|
67
|
+
time_window = Time.now.to_i / limit_config[:period]
|
|
68
|
+
|
|
69
|
+
"rate_limit:#{request.ip}:#{request.path}:#{time_window}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def rate_limit_response(request, limit_config)
|
|
73
|
+
# Return JSON for API requests, HTML for dashboard
|
|
74
|
+
if request.path.start_with?("/error_dashboard/api")
|
|
75
|
+
json_rate_limit_response(limit_config)
|
|
76
|
+
else
|
|
77
|
+
html_rate_limit_response(limit_config)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def json_rate_limit_response(limit_config)
|
|
82
|
+
[
|
|
83
|
+
429,
|
|
84
|
+
{
|
|
85
|
+
"Content-Type" => "application/json",
|
|
86
|
+
"Retry-After" => limit_config[:period].to_s,
|
|
87
|
+
"X-RateLimit-Limit" => limit_config[:limit].to_s,
|
|
88
|
+
"X-RateLimit-Period" => "#{limit_config[:period]} seconds"
|
|
89
|
+
},
|
|
90
|
+
[ { error: "Rate limit exceeded. Please try again later." }.to_json ]
|
|
91
|
+
]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def html_rate_limit_response(limit_config)
|
|
95
|
+
body = <<~HTML
|
|
96
|
+
<!DOCTYPE html>
|
|
97
|
+
<html>
|
|
98
|
+
<head>
|
|
99
|
+
<title>Rate Limit Exceeded</title>
|
|
100
|
+
<style>
|
|
101
|
+
body {
|
|
102
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
103
|
+
max-width: 600px;
|
|
104
|
+
margin: 100px auto;
|
|
105
|
+
padding: 20px;
|
|
106
|
+
text-align: center;
|
|
107
|
+
}
|
|
108
|
+
h1 { color: #dc3545; }
|
|
109
|
+
p { color: #6c757d; line-height: 1.6; }
|
|
110
|
+
.code { background: #f8f9fa; padding: 10px; border-radius: 4px; margin: 20px 0; }
|
|
111
|
+
</style>
|
|
112
|
+
</head>
|
|
113
|
+
<body>
|
|
114
|
+
<h1>⚠️ Rate Limit Exceeded</h1>
|
|
115
|
+
<p>You've made too many requests to the error dashboard.</p>
|
|
116
|
+
<div class="code">
|
|
117
|
+
<strong>Limit:</strong> #{limit_config[:limit]} requests per #{limit_config[:period]} seconds
|
|
118
|
+
</div>
|
|
119
|
+
<p>Please wait a moment before trying again.</p>
|
|
120
|
+
</body>
|
|
121
|
+
</html>
|
|
122
|
+
HTML
|
|
123
|
+
|
|
124
|
+
[
|
|
125
|
+
429,
|
|
126
|
+
{
|
|
127
|
+
"Content-Type" => "text/html",
|
|
128
|
+
"Retry-After" => limit_config[:period].to_s,
|
|
129
|
+
"X-RateLimit-Limit" => limit_config[:limit].to_s,
|
|
130
|
+
"X-RateLimit-Period" => "#{limit_config[:period]} seconds"
|
|
131
|
+
},
|
|
132
|
+
[ body ]
|
|
133
|
+
]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -15,19 +15,37 @@ module RailsErrorDashboard
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def call
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
# Cache analytics data for 5 minutes to reduce database load
|
|
19
|
+
# Cache key includes days parameter and last error update timestamp
|
|
20
|
+
Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
|
|
21
|
+
{
|
|
22
|
+
days: @days,
|
|
23
|
+
error_stats: error_statistics,
|
|
24
|
+
errors_over_time: errors_over_time,
|
|
25
|
+
errors_by_type: errors_by_type,
|
|
26
|
+
errors_by_platform: errors_by_platform,
|
|
27
|
+
errors_by_hour: errors_by_hour,
|
|
28
|
+
top_users: top_affected_users,
|
|
29
|
+
resolution_rate: resolution_rate,
|
|
30
|
+
mobile_errors: mobile_errors_count,
|
|
31
|
+
api_errors: api_errors_count,
|
|
32
|
+
pattern_insights: pattern_insights
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def cache_key
|
|
38
|
+
# Cache key includes:
|
|
39
|
+
# - Query class name
|
|
40
|
+
# - Days parameter (different time ranges = different caches)
|
|
41
|
+
# - Last error update timestamp (auto-invalidates when errors change)
|
|
42
|
+
# - Start date (ensures correct time window)
|
|
43
|
+
[
|
|
44
|
+
"analytics_stats",
|
|
45
|
+
@days,
|
|
46
|
+
ErrorLog.maximum(:updated_at)&.to_i || 0,
|
|
47
|
+
@start_date.to_date.to_s
|
|
48
|
+
].join("/")
|
|
31
49
|
end
|
|
32
50
|
|
|
33
51
|
private
|
|
@@ -10,28 +10,42 @@ module RailsErrorDashboard
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def call
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
13
|
+
# Cache dashboard stats for 1 minute to reduce database load
|
|
14
|
+
# Dashboard is viewed frequently, so short cache prevents stale data
|
|
15
|
+
Rails.cache.fetch(cache_key, expires_in: 1.minute) do
|
|
16
|
+
{
|
|
17
|
+
total_today: ErrorLog.where("occurred_at >= ?", Time.current.beginning_of_day).count,
|
|
18
|
+
total_week: ErrorLog.where("occurred_at >= ?", 7.days.ago).count,
|
|
19
|
+
total_month: ErrorLog.where("occurred_at >= ?", 30.days.ago).count,
|
|
20
|
+
unresolved: ErrorLog.unresolved.count,
|
|
21
|
+
resolved: ErrorLog.resolved.count,
|
|
22
|
+
by_platform: ErrorLog.group(:platform).count,
|
|
23
|
+
top_errors: top_errors,
|
|
24
|
+
# Trend visualizations
|
|
25
|
+
errors_trend_7d: errors_trend_7d,
|
|
26
|
+
errors_by_severity_7d: errors_by_severity_7d,
|
|
27
|
+
spike_detected: spike_detected?,
|
|
28
|
+
spike_info: spike_info,
|
|
29
|
+
# New metrics for Overview dashboard
|
|
30
|
+
error_rate: error_rate,
|
|
31
|
+
affected_users_today: affected_users_today,
|
|
32
|
+
affected_users_yesterday: affected_users_yesterday,
|
|
33
|
+
affected_users_change: affected_users_change,
|
|
34
|
+
trend_percentage: trend_percentage,
|
|
35
|
+
trend_direction: trend_direction,
|
|
36
|
+
top_errors_by_impact: top_errors_by_impact
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def cache_key
|
|
42
|
+
# Cache key includes last error update timestamp for auto-invalidation
|
|
43
|
+
# Also includes current hour to ensure fresh data
|
|
44
|
+
[
|
|
45
|
+
"dashboard_stats",
|
|
46
|
+
ErrorLog.maximum(:updated_at)&.to_i || 0,
|
|
47
|
+
Time.current.hour
|
|
48
|
+
].join("/")
|
|
35
49
|
end
|
|
36
50
|
|
|
37
51
|
private
|
|
@@ -53,14 +67,19 @@ module RailsErrorDashboard
|
|
|
53
67
|
end
|
|
54
68
|
|
|
55
69
|
# Get error counts by severity for last 7 days
|
|
70
|
+
# OPTIMIZED: Use database filtering instead of loading all records into Ruby
|
|
56
71
|
def errors_by_severity_7d
|
|
57
|
-
|
|
72
|
+
base_scope = ErrorLog.where("occurred_at >= ?", 7.days.ago)
|
|
58
73
|
|
|
59
74
|
{
|
|
60
|
-
critical:
|
|
61
|
-
high:
|
|
62
|
-
medium:
|
|
63
|
-
low:
|
|
75
|
+
critical: base_scope.where(error_type: ErrorLog::CRITICAL_ERROR_TYPES).count,
|
|
76
|
+
high: base_scope.where(error_type: ErrorLog::HIGH_SEVERITY_ERROR_TYPES).count,
|
|
77
|
+
medium: base_scope.where(error_type: ErrorLog::MEDIUM_SEVERITY_ERROR_TYPES).count,
|
|
78
|
+
low: base_scope.where.not(
|
|
79
|
+
error_type: ErrorLog::CRITICAL_ERROR_TYPES +
|
|
80
|
+
ErrorLog::HIGH_SEVERITY_ERROR_TYPES +
|
|
81
|
+
ErrorLog::MEDIUM_SEVERITY_ERROR_TYPES
|
|
82
|
+
).count
|
|
64
83
|
}
|
|
65
84
|
end
|
|
66
85
|
|
|
@@ -80,14 +80,21 @@ module RailsErrorDashboard
|
|
|
80
80
|
# Use PostgreSQL full-text search if available (much faster with GIN index)
|
|
81
81
|
# Otherwise fall back to LIKE query
|
|
82
82
|
if postgresql?
|
|
83
|
-
# Use
|
|
84
|
-
# This
|
|
85
|
-
|
|
86
|
-
query.where(
|
|
83
|
+
# Use plainto_tsquery for full-text search with GIN index created in migration
|
|
84
|
+
# This leverages index_error_logs_on_searchable_text for fast searches
|
|
85
|
+
# across message, backtrace, and error_type fields
|
|
86
|
+
query.where(
|
|
87
|
+
"to_tsvector('english', COALESCE(message, '') || ' ' || COALESCE(backtrace, '') || ' ' || COALESCE(error_type, '')) @@ plainto_tsquery('english', ?)",
|
|
88
|
+
@filters[:search]
|
|
89
|
+
)
|
|
87
90
|
else
|
|
88
|
-
# Fall back to LIKE for SQLite/MySQL
|
|
91
|
+
# Fall back to LIKE for SQLite/MySQL - search across all relevant fields
|
|
89
92
|
# Use LOWER() for case-insensitive search
|
|
90
|
-
|
|
93
|
+
search_pattern = "%#{@filters[:search]}%"
|
|
94
|
+
query.where(
|
|
95
|
+
"LOWER(message) LIKE LOWER(?) OR LOWER(COALESCE(backtrace, '')) LIKE LOWER(?) OR LOWER(error_type) LIKE LOWER(?)",
|
|
96
|
+
search_pattern, search_pattern, search_pattern
|
|
97
|
+
)
|
|
91
98
|
end
|
|
92
99
|
end
|
|
93
100
|
|
|
@@ -38,6 +38,7 @@ require "rails_error_dashboard/queries/recurring_issues"
|
|
|
38
38
|
require "rails_error_dashboard/queries/mttr_stats"
|
|
39
39
|
require "rails_error_dashboard/error_reporter"
|
|
40
40
|
require "rails_error_dashboard/middleware/error_catcher"
|
|
41
|
+
require "rails_error_dashboard/middleware/rate_limiter"
|
|
41
42
|
|
|
42
43
|
# Plugin system
|
|
43
44
|
require "rails_error_dashboard/plugin"
|
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.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Anjan Jagirdar
|
|
@@ -298,7 +298,6 @@ files:
|
|
|
298
298
|
- app/models/rails_error_dashboard/error_occurrence.rb
|
|
299
299
|
- app/views/layouts/rails_error_dashboard.html.erb
|
|
300
300
|
- app/views/layouts/rails_error_dashboard/application.html.erb
|
|
301
|
-
- app/views/layouts/rails_error_dashboard_old_backup.html.erb
|
|
302
301
|
- app/views/rails_error_dashboard/error_notification_mailer/error_alert.html.erb
|
|
303
302
|
- app/views/rails_error_dashboard/error_notification_mailer/error_alert.text.erb
|
|
304
303
|
- app/views/rails_error_dashboard/errors/_error_row.html.erb
|
|
@@ -325,6 +324,7 @@ files:
|
|
|
325
324
|
- db/migrate/20251225102500_create_error_baselines.rb
|
|
326
325
|
- db/migrate/20251226020000_add_workflow_fields_to_error_logs.rb
|
|
327
326
|
- db/migrate/20251226020100_create_error_comments.rb
|
|
327
|
+
- db/migrate/20251229111223_add_additional_performance_indexes.rb
|
|
328
328
|
- lib/generators/rails_error_dashboard/install/install_generator.rb
|
|
329
329
|
- lib/generators/rails_error_dashboard/install/templates/README
|
|
330
330
|
- lib/generators/rails_error_dashboard/install/templates/initializer.rb
|
|
@@ -341,6 +341,7 @@ files:
|
|
|
341
341
|
- lib/rails_error_dashboard/error_reporter.rb
|
|
342
342
|
- lib/rails_error_dashboard/logger.rb
|
|
343
343
|
- lib/rails_error_dashboard/middleware/error_catcher.rb
|
|
344
|
+
- lib/rails_error_dashboard/middleware/rate_limiter.rb
|
|
344
345
|
- lib/rails_error_dashboard/plugin.rb
|
|
345
346
|
- lib/rails_error_dashboard/plugin_registry.rb
|
|
346
347
|
- lib/rails_error_dashboard/plugins/audit_log_plugin.rb
|
|
@@ -375,6 +376,16 @@ metadata:
|
|
|
375
376
|
homepage_uri: https://github.com/AnjanJ/rails_error_dashboard
|
|
376
377
|
source_code_uri: https://github.com/AnjanJ/rails_error_dashboard
|
|
377
378
|
changelog_uri: https://github.com/AnjanJ/rails_error_dashboard/blob/main/CHANGELOG.md
|
|
379
|
+
post_install_message: "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n
|
|
380
|
+
\ Rails Error Dashboard v0.1.7 installed successfully!\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n\U0001F4E6
|
|
381
|
+
Next steps to get started:\n\n 1. Run the installer:\n rails generate rails_error_dashboard:install\n\n
|
|
382
|
+
\ 2. Run migrations:\n rails db:migrate\n\n 3. Mount the engine in config/routes.rb:\n
|
|
383
|
+
\ mount RailsErrorDashboard::Engine => '/error_dashboard'\n\n 4. Start your
|
|
384
|
+
server and visit:\n http://localhost:3000/error_dashboard\n\n\U0001F3AE Try
|
|
385
|
+
the live demo: https://rails-error-dashboard.anjan.dev\n (Username: frodo, Password:
|
|
386
|
+
precious)\n\n\U0001F4D6 Documentation: https://github.com/AnjanJ/rails_error_dashboard\n\U0001F4AC
|
|
387
|
+
Questions? https://github.com/AnjanJ/rails_error_dashboard/issues\n\n⚠️ BETA: API
|
|
388
|
+
may change before v1.0.0 - Use at your own discretion\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
|
|
378
389
|
rdoc_options: []
|
|
379
390
|
require_paths:
|
|
380
391
|
- lib
|
|
@@ -1,383 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<title>Error Dashboard - Audio Intelli API</title>
|
|
5
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
-
<%= csrf_meta_tags %>
|
|
7
|
-
<%= csp_meta_tag %>
|
|
8
|
-
|
|
9
|
-
<!-- Apply theme immediately to prevent flash of wrong theme -->
|
|
10
|
-
<script>
|
|
11
|
-
// This runs BEFORE body renders to prevent flash
|
|
12
|
-
(function() {
|
|
13
|
-
const savedTheme = localStorage.getItem('theme');
|
|
14
|
-
if (savedTheme === 'dark') {
|
|
15
|
-
// Add to html element so we can style body
|
|
16
|
-
document.documentElement.setAttribute('data-theme', 'dark');
|
|
17
|
-
}
|
|
18
|
-
})();
|
|
19
|
-
</script>
|
|
20
|
-
|
|
21
|
-
<!-- Bootstrap CSS -->
|
|
22
|
-
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
23
|
-
<!-- Bootstrap Icons -->
|
|
24
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
|
25
|
-
<!-- Chart.js with date adapter -->
|
|
26
|
-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
|
27
|
-
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
|
|
28
|
-
<script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1/dist/chartkick.min.js"></script>
|
|
29
|
-
|
|
30
|
-
<!-- Turbo for real-time updates -->
|
|
31
|
-
<script type="module">
|
|
32
|
-
import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.12/+esm'
|
|
33
|
-
</script>
|
|
34
|
-
|
|
35
|
-
<!-- Rails Error Dashboard Styles - Catppuccin Mocha Theme -->
|
|
36
|
-
<style>
|
|
37
|
-
<%= File.read(RailsErrorDashboard::Engine.root.join("app/assets/stylesheets/rails_error_dashboard/application.css")) %>
|
|
38
|
-
</style>
|
|
39
|
-
</head>
|
|
40
|
-
|
|
41
|
-
<body>
|
|
42
|
-
<!-- Loading Indicator -->
|
|
43
|
-
<div id="loading-indicator"></div>
|
|
44
|
-
|
|
45
|
-
<!-- Top Navbar -->
|
|
46
|
-
<nav class="navbar navbar-dark">
|
|
47
|
-
<div class="container-fluid">
|
|
48
|
-
<div class="d-flex align-items-center">
|
|
49
|
-
<!-- Mobile menu toggle -->
|
|
50
|
-
<button class="btn btn-link text-white d-md-none me-2" type="button" data-bs-toggle="offcanvas" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu">
|
|
51
|
-
<i class="bi bi-list fs-4"></i>
|
|
52
|
-
</button>
|
|
53
|
-
<a class="navbar-brand fw-bold" href="<%= root_path %>">
|
|
54
|
-
<i class="bi bi-bug-fill"></i>
|
|
55
|
-
Error Dashboard
|
|
56
|
-
</a>
|
|
57
|
-
</div>
|
|
58
|
-
<div class="d-flex align-items-center gap-3">
|
|
59
|
-
<button class="theme-toggle" id="themeToggle" onclick="toggleTheme()">
|
|
60
|
-
<i class="bi bi-moon-fill" id="themeIcon"></i>
|
|
61
|
-
</button>
|
|
62
|
-
<div class="text-white d-none d-md-block">
|
|
63
|
-
<small><%= Rails.env.titleize %> Environment</small>
|
|
64
|
-
</div>
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
</nav>
|
|
68
|
-
|
|
69
|
-
<div class="container-fluid">
|
|
70
|
-
<div class="row">
|
|
71
|
-
<!-- Sidebar - Desktop -->
|
|
72
|
-
<nav class="col-md-2 d-none d-md-block sidebar" id="sidebarDesktop">
|
|
73
|
-
<div class="position-sticky pt-3">
|
|
74
|
-
<ul class="nav flex-column">
|
|
75
|
-
<li class="nav-item">
|
|
76
|
-
<%= link_to root_path, class: "nav-link #{request.path == root_path ? 'active' : ''}" do %>
|
|
77
|
-
<i class="bi bi-speedometer2"></i> Overview
|
|
78
|
-
<% end %>
|
|
79
|
-
</li>
|
|
80
|
-
<li class="nav-item">
|
|
81
|
-
<%= link_to errors_path, class: "nav-link #{request.path == errors_path ? 'active' : ''}" do %>
|
|
82
|
-
<i class="bi bi-list-ul"></i> All Errors
|
|
83
|
-
<% end %>
|
|
84
|
-
</li>
|
|
85
|
-
<li class="nav-item">
|
|
86
|
-
<%= link_to analytics_errors_path, class: "nav-link #{request.path == analytics_errors_path ? 'active' : ''}" do %>
|
|
87
|
-
<i class="bi bi-graph-up"></i> Analytics
|
|
88
|
-
<% end %>
|
|
89
|
-
</li>
|
|
90
|
-
<% if RailsErrorDashboard.configuration.enable_platform_comparison %>
|
|
91
|
-
<li class="nav-item">
|
|
92
|
-
<%= link_to platform_comparison_errors_path, class: "nav-link #{request.path == platform_comparison_errors_path ? 'active' : ''}" do %>
|
|
93
|
-
<i class="bi bi-phone"></i> Platform Health
|
|
94
|
-
<% end %>
|
|
95
|
-
</li>
|
|
96
|
-
<% end %>
|
|
97
|
-
<% if RailsErrorDashboard.configuration.enable_error_correlation %>
|
|
98
|
-
<li class="nav-item">
|
|
99
|
-
<%= link_to correlation_errors_path, class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}" do %>
|
|
100
|
-
<i class="bi bi-diagram-3"></i> Correlation
|
|
101
|
-
<% end %>
|
|
102
|
-
</li>
|
|
103
|
-
<% end %>
|
|
104
|
-
</ul>
|
|
105
|
-
|
|
106
|
-
<hr class="my-3">
|
|
107
|
-
|
|
108
|
-
<h6 class="sidebar-heading px-3 mt-4 mb-2 text-muted text-uppercase">
|
|
109
|
-
<small>Quick Filters</small>
|
|
110
|
-
</h6>
|
|
111
|
-
<ul class="nav flex-column">
|
|
112
|
-
<li class="nav-item">
|
|
113
|
-
<%= link_to errors_path(unresolved: true), class: "nav-link" do %>
|
|
114
|
-
<i class="bi bi-exclamation-circle text-danger"></i> Unresolved
|
|
115
|
-
<% end %>
|
|
116
|
-
</li>
|
|
117
|
-
<li class="nav-item">
|
|
118
|
-
<%= link_to errors_path(platform: 'iOS'), class: "nav-link" do %>
|
|
119
|
-
<i class="bi bi-phone"></i> iOS Errors
|
|
120
|
-
<% end %>
|
|
121
|
-
</li>
|
|
122
|
-
<li class="nav-item">
|
|
123
|
-
<%= link_to errors_path(platform: 'Android'), class: "nav-link" do %>
|
|
124
|
-
<i class="bi bi-phone"></i> Android Errors
|
|
125
|
-
<% end %>
|
|
126
|
-
</li>
|
|
127
|
-
</ul>
|
|
128
|
-
</div>
|
|
129
|
-
</nav>
|
|
130
|
-
|
|
131
|
-
<!-- Sidebar - Mobile (Offcanvas) -->
|
|
132
|
-
<div class="offcanvas offcanvas-start" tabindex="-1" id="sidebarMenu" aria-labelledby="sidebarMenuLabel">
|
|
133
|
-
<div class="offcanvas-header">
|
|
134
|
-
<h5 class="offcanvas-title" id="sidebarMenuLabel">
|
|
135
|
-
<i class="bi bi-bug-fill"></i> Error Dashboard
|
|
136
|
-
</h5>
|
|
137
|
-
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
|
138
|
-
</div>
|
|
139
|
-
<div class="offcanvas-body">
|
|
140
|
-
<ul class="nav flex-column">
|
|
141
|
-
<li class="nav-item">
|
|
142
|
-
<%= link_to root_path, class: "nav-link #{request.path == root_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
|
|
143
|
-
<i class="bi bi-speedometer2"></i> Overview
|
|
144
|
-
<% end %>
|
|
145
|
-
</li>
|
|
146
|
-
<li class="nav-item">
|
|
147
|
-
<%= link_to errors_path, class: "nav-link #{request.path == errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
|
|
148
|
-
<i class="bi bi-list-ul"></i> All Errors
|
|
149
|
-
<% end %>
|
|
150
|
-
</li>
|
|
151
|
-
<li class="nav-item">
|
|
152
|
-
<%= link_to analytics_errors_path, class: "nav-link #{request.path == analytics_errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
|
|
153
|
-
<i class="bi bi-graph-up"></i> Analytics
|
|
154
|
-
<% end %>
|
|
155
|
-
</li>
|
|
156
|
-
<% if RailsErrorDashboard.configuration.enable_platform_comparison %>
|
|
157
|
-
<li class="nav-item">
|
|
158
|
-
<%= link_to platform_comparison_errors_path, class: "nav-link #{request.path == platform_comparison_errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
|
|
159
|
-
<i class="bi bi-phone"></i> Platform Health
|
|
160
|
-
<% end %>
|
|
161
|
-
</li>
|
|
162
|
-
<% end %>
|
|
163
|
-
<% if RailsErrorDashboard.configuration.enable_error_correlation %>
|
|
164
|
-
<li class="nav-item">
|
|
165
|
-
<%= link_to correlation_errors_path, class: "nav-link #{request.path == correlation_errors_path ? 'active' : ''}", data: { bs_dismiss: "offcanvas" } do %>
|
|
166
|
-
<i class="bi bi-diagram-3"></i> Correlation
|
|
167
|
-
<% end %>
|
|
168
|
-
</li>
|
|
169
|
-
<% end %>
|
|
170
|
-
</ul>
|
|
171
|
-
|
|
172
|
-
<hr class="my-3">
|
|
173
|
-
|
|
174
|
-
<h6 class="sidebar-heading px-3 mt-4 mb-2 text-muted text-uppercase">
|
|
175
|
-
<small>Quick Filters</small>
|
|
176
|
-
</h6>
|
|
177
|
-
<ul class="nav flex-column">
|
|
178
|
-
<li class="nav-item">
|
|
179
|
-
<%= link_to errors_path(unresolved: true), class: "nav-link", data: { bs_dismiss: "offcanvas" } do %>
|
|
180
|
-
<i class="bi bi-exclamation-circle text-danger"></i> Unresolved
|
|
181
|
-
<% end %>
|
|
182
|
-
</li>
|
|
183
|
-
<li class="nav-item">
|
|
184
|
-
<%= link_to errors_path(platform: 'iOS'), class: "nav-link", data: { bs_dismiss: "offcanvas" } do %>
|
|
185
|
-
<i class="bi bi-phone"></i> iOS Errors
|
|
186
|
-
<% end %>
|
|
187
|
-
</li>
|
|
188
|
-
<li class="nav-item">
|
|
189
|
-
<%= link_to errors_path(platform: 'Android'), class: "nav-link", data: { bs_dismiss: "offcanvas" } do %>
|
|
190
|
-
<i class="bi bi-phone"></i> Android Errors
|
|
191
|
-
<% end %>
|
|
192
|
-
</li>
|
|
193
|
-
</ul>
|
|
194
|
-
|
|
195
|
-
<hr class="my-3">
|
|
196
|
-
|
|
197
|
-
<div class="px-3">
|
|
198
|
-
<small class="text-muted">Environment</small>
|
|
199
|
-
<div class="fw-bold"><%= Rails.env.titleize %></div>
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
</div>
|
|
203
|
-
|
|
204
|
-
<!-- Main content -->
|
|
205
|
-
<main class="col-md-10 ms-sm-auto px-md-4">
|
|
206
|
-
<%= yield %>
|
|
207
|
-
</main>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
<!-- Bootstrap JS -->
|
|
212
|
-
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
213
|
-
|
|
214
|
-
<!-- Theme Toggle Script -->
|
|
215
|
-
<script>
|
|
216
|
-
// Catppuccin Mocha Chart Colors
|
|
217
|
-
function getCatppuccinChartConfig(isDark) {
|
|
218
|
-
if (!isDark) {
|
|
219
|
-
// Light theme - keep existing for now
|
|
220
|
-
return {
|
|
221
|
-
textColor: '#1F2937',
|
|
222
|
-
gridColor: 'rgba(0, 0, 0, 0.1)',
|
|
223
|
-
tooltipBg: 'rgba(0, 0, 0, 0.8)',
|
|
224
|
-
colors: {
|
|
225
|
-
red: '#EF4444',
|
|
226
|
-
green: '#10B981',
|
|
227
|
-
yellow: '#F59E0B',
|
|
228
|
-
blue: '#3B82F6',
|
|
229
|
-
purple: '#8B5CF6',
|
|
230
|
-
pink: '#EC4899',
|
|
231
|
-
teal: '#14B8A6',
|
|
232
|
-
orange: '#F97316',
|
|
233
|
-
sapphire: '#3B82F6'
|
|
234
|
-
}
|
|
235
|
-
};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Dark theme - Catppuccin Mocha
|
|
239
|
-
return {
|
|
240
|
-
textColor: '#cdd6f4', // ctp-text
|
|
241
|
-
gridColor: 'rgba(88, 91, 112, 0.3)', // ctp-surface2 with opacity
|
|
242
|
-
tooltipBg: '#313244', // ctp-surface0
|
|
243
|
-
colors: {
|
|
244
|
-
red: '#f38ba8', // ctp-red
|
|
245
|
-
green: '#a6e3a1', // ctp-green
|
|
246
|
-
yellow: '#f9e2af', // ctp-yellow
|
|
247
|
-
blue: '#89b4fa', // ctp-blue
|
|
248
|
-
purple: '#cba6f7', // ctp-mauve
|
|
249
|
-
pink: '#f5c2e7', // ctp-pink
|
|
250
|
-
teal: '#94e2d5', // ctp-teal
|
|
251
|
-
orange: '#fab387', // ctp-peach
|
|
252
|
-
sapphire: '#74c7ec' // ctp-sapphire
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Get platform-specific color
|
|
258
|
-
function getPlatformColor(platform, isDark) {
|
|
259
|
-
const config = getCatppuccinChartConfig(isDark);
|
|
260
|
-
const platformMap = {
|
|
261
|
-
'ios': isDark ? config.textColor : '#000000',
|
|
262
|
-
'android': config.colors.green,
|
|
263
|
-
'web': config.colors.blue,
|
|
264
|
-
'api': config.colors.sapphire
|
|
265
|
-
};
|
|
266
|
-
return platformMap[platform.toLowerCase()] || config.colors.purple;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Update Chart.js defaults for dark/light theme
|
|
270
|
-
function updateChartTheme(isDark) {
|
|
271
|
-
if (typeof Chart !== 'undefined') {
|
|
272
|
-
const config = getCatppuccinChartConfig(isDark);
|
|
273
|
-
|
|
274
|
-
Chart.defaults.color = config.textColor;
|
|
275
|
-
Chart.defaults.borderColor = config.gridColor;
|
|
276
|
-
Chart.defaults.scale.grid.color = config.gridColor;
|
|
277
|
-
Chart.defaults.scale.ticks.color = config.textColor;
|
|
278
|
-
Chart.defaults.plugins.legend.labels.color = config.textColor;
|
|
279
|
-
Chart.defaults.plugins.tooltip.backgroundColor = config.tooltipBg;
|
|
280
|
-
Chart.defaults.plugins.tooltip.titleColor = config.textColor;
|
|
281
|
-
Chart.defaults.plugins.tooltip.bodyColor = config.textColor;
|
|
282
|
-
Chart.defaults.plugins.tooltip.borderColor = config.gridColor;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// Load theme from localStorage on page load
|
|
287
|
-
document.addEventListener('DOMContentLoaded', function() {
|
|
288
|
-
console.log('📄 DOMContentLoaded - Loading theme from localStorage');
|
|
289
|
-
|
|
290
|
-
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
291
|
-
const isDark = savedTheme === 'dark';
|
|
292
|
-
|
|
293
|
-
console.log('Saved theme:', savedTheme, '| isDark:', isDark);
|
|
294
|
-
|
|
295
|
-
if (isDark) {
|
|
296
|
-
document.body.classList.add('dark-mode');
|
|
297
|
-
document.documentElement.setAttribute('data-theme', 'dark');
|
|
298
|
-
console.log('✅ Applied dark theme (body.dark-mode + html[data-theme=dark])');
|
|
299
|
-
updateThemeIcon(true);
|
|
300
|
-
} else {
|
|
301
|
-
// Ensure light mode is clean
|
|
302
|
-
document.body.classList.remove('dark-mode');
|
|
303
|
-
document.documentElement.removeAttribute('data-theme');
|
|
304
|
-
console.log('✅ Applied light theme (removed classes)');
|
|
305
|
-
updateThemeIcon(false);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
// Update Chart.js theme
|
|
309
|
-
updateChartTheme(isDark);
|
|
310
|
-
console.log('📊 Chart.js theme updated');
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
function toggleTheme() {
|
|
314
|
-
try {
|
|
315
|
-
console.log('🎨 Toggle theme clicked');
|
|
316
|
-
|
|
317
|
-
const body = document.body;
|
|
318
|
-
const isDark = body.classList.toggle('dark-mode');
|
|
319
|
-
|
|
320
|
-
console.log('Dark mode:', isDark);
|
|
321
|
-
|
|
322
|
-
// Sync with html data attribute
|
|
323
|
-
if (isDark) {
|
|
324
|
-
document.documentElement.setAttribute('data-theme', 'dark');
|
|
325
|
-
console.log('✅ Set data-theme=dark');
|
|
326
|
-
} else {
|
|
327
|
-
document.documentElement.removeAttribute('data-theme');
|
|
328
|
-
console.log('✅ Removed data-theme');
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Save preference
|
|
332
|
-
localStorage.setItem('theme', isDark ? 'dark' : 'light');
|
|
333
|
-
console.log('💾 Saved to localStorage:', isDark ? 'dark' : 'light');
|
|
334
|
-
|
|
335
|
-
// Update icon
|
|
336
|
-
updateThemeIcon(isDark);
|
|
337
|
-
console.log('🌙 Updated icon');
|
|
338
|
-
|
|
339
|
-
// Update Chart.js theme
|
|
340
|
-
updateChartTheme(isDark);
|
|
341
|
-
console.log('📊 Updated Chart.js');
|
|
342
|
-
|
|
343
|
-
// Reload page to apply chart theme (Chart.js requires re-render)
|
|
344
|
-
console.log('🔄 Reloading page...');
|
|
345
|
-
setTimeout(() => location.reload(), 100);
|
|
346
|
-
} catch (error) {
|
|
347
|
-
console.error('❌ Error in toggleTheme:', error);
|
|
348
|
-
alert('Error toggling theme: ' + error.message);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function updateThemeIcon(isDark) {
|
|
353
|
-
const icon = document.getElementById('themeIcon');
|
|
354
|
-
if (isDark) {
|
|
355
|
-
icon.className = 'bi bi-sun-fill';
|
|
356
|
-
} else {
|
|
357
|
-
icon.className = 'bi bi-moon-fill';
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
// Loading indicator for form submissions and link clicks
|
|
362
|
-
const loadingIndicator = document.getElementById('loading-indicator');
|
|
363
|
-
|
|
364
|
-
// Show loading on form submit
|
|
365
|
-
document.addEventListener('submit', function() {
|
|
366
|
-
loadingIndicator.classList.add('active');
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
// Show loading on link clicks (except anchors)
|
|
370
|
-
document.addEventListener('click', function(e) {
|
|
371
|
-
const link = e.target.closest('a');
|
|
372
|
-
if (link && link.href && !link.href.startsWith('#') && !link.target) {
|
|
373
|
-
loadingIndicator.classList.add('active');
|
|
374
|
-
}
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
// Hide loading when page loads
|
|
378
|
-
window.addEventListener('load', function() {
|
|
379
|
-
loadingIndicator.classList.remove('active');
|
|
380
|
-
});
|
|
381
|
-
</script>
|
|
382
|
-
</body>
|
|
383
|
-
</html>
|