rails-health-checker 0.1.0
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 +7 -0
- data/CHANGELOG.md +37 -0
- data/COMMANDS.md +118 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +222 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/Rakefile +6 -0
- data/SECURITY.md +41 -0
- data/TESTING.md +64 -0
- data/TEST_RESULTS.md +51 -0
- data/example_usage.rb +23 -0
- data/lib/rails_health_checker/checker.rb +88 -0
- data/lib/rails_health_checker/dashboard_middleware.rb +503 -0
- data/lib/rails_health_checker/gem_analyzer.rb +39 -0
- data/lib/rails_health_checker/health_middleware.rb +53 -0
- data/lib/rails_health_checker/job_analyzer.rb +108 -0
- data/lib/rails_health_checker/railtie.rb +11 -0
- data/lib/rails_health_checker/report_generator.rb +499 -0
- data/lib/rails_health_checker/system_analyzer.rb +182 -0
- data/lib/rails_health_checker/tasks.rb +63 -0
- data/lib/rails_health_checker/version.rb +3 -0
- data/lib/rails_health_checker.rb +17 -0
- data/rails_health_checker.gemspec +33 -0
- data/simple_test.rb +52 -0
- data/test_gem.rb +100 -0
- metadata +117 -0
data/TEST_RESULTS.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Test Results
|
|
2
|
+
|
|
3
|
+
## ✅ Successfully Tested
|
|
4
|
+
|
|
5
|
+
### 1. Basic Functionality
|
|
6
|
+
- ✓ Gem loads correctly
|
|
7
|
+
- ✓ Version: 0.1.0
|
|
8
|
+
- ✓ Core modules work
|
|
9
|
+
|
|
10
|
+
### 2. Rails Integration (FinaSync Project)
|
|
11
|
+
- ✓ Gem installs in Rails app
|
|
12
|
+
- ✓ Rake tasks work:
|
|
13
|
+
- `rake health:gems` → 93 total gems, 33 outdated
|
|
14
|
+
- `rake health:database` → Database connection healthy
|
|
15
|
+
- `rake health:check` → Complete health report
|
|
16
|
+
|
|
17
|
+
### 3. Health Check Results
|
|
18
|
+
```
|
|
19
|
+
=== Rails Health Check Report ===
|
|
20
|
+
Rails Version: 7.1.5.2 (healthy)
|
|
21
|
+
Ruby Version: 3.0.6 (healthy)
|
|
22
|
+
Database: healthy
|
|
23
|
+
Gems: 93 total, 33 outdated
|
|
24
|
+
Security: needs_attention
|
|
25
|
+
================================
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## How to Test:
|
|
29
|
+
|
|
30
|
+
### Quick Test:
|
|
31
|
+
```bash
|
|
32
|
+
cd rails_health_checker
|
|
33
|
+
ruby simple_test.rb
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Full Rails Test:
|
|
37
|
+
```bash
|
|
38
|
+
# Add to any Rails project Gemfile:
|
|
39
|
+
gem 'rails_health_checker', path: '/path/to/rails_health_checker'
|
|
40
|
+
|
|
41
|
+
# Install and test:
|
|
42
|
+
bundle install
|
|
43
|
+
rake health:check
|
|
44
|
+
rake health:gems
|
|
45
|
+
rake health:database
|
|
46
|
+
|
|
47
|
+
# Test HTTP endpoint:
|
|
48
|
+
curl http://localhost:3000/health
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## ✅ Gem is Ready for Use!
|
data/example_usage.rb
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Example usage in a Rails application
|
|
2
|
+
|
|
3
|
+
# 1. Add to Gemfile:
|
|
4
|
+
# gem 'rails_health_checker'
|
|
5
|
+
|
|
6
|
+
# 2. In your Rails application, you can use:
|
|
7
|
+
|
|
8
|
+
# Run complete health check
|
|
9
|
+
results = RailsHealthChecker.check
|
|
10
|
+
|
|
11
|
+
# Access specific health data
|
|
12
|
+
puts "Rails version: #{results[:rails_version][:current]}"
|
|
13
|
+
puts "Database status: #{results[:database][:status]}"
|
|
14
|
+
puts "Total gems: #{results[:gems][:total]}"
|
|
15
|
+
|
|
16
|
+
# Use rake tasks
|
|
17
|
+
# rake health:check
|
|
18
|
+
# rake health:gems
|
|
19
|
+
# rake health:database
|
|
20
|
+
|
|
21
|
+
# Access health endpoint
|
|
22
|
+
# GET /health
|
|
23
|
+
# Returns JSON with health status
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
module RailsHealthChecker
|
|
2
|
+
class Checker
|
|
3
|
+
def run
|
|
4
|
+
results = {
|
|
5
|
+
rails_version: check_rails_version,
|
|
6
|
+
ruby_version: check_ruby_version,
|
|
7
|
+
database: check_database_connection,
|
|
8
|
+
gems: check_gems_health,
|
|
9
|
+
security: check_security_vulnerabilities,
|
|
10
|
+
jobs: check_background_jobs,
|
|
11
|
+
system: check_system_details
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
generate_report(results)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def check_rails_version
|
|
20
|
+
{
|
|
21
|
+
current: Rails.version,
|
|
22
|
+
supported: rails_version_supported?,
|
|
23
|
+
status: rails_version_supported? ? "healthy" : "outdated"
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def check_ruby_version
|
|
28
|
+
{
|
|
29
|
+
current: RUBY_VERSION,
|
|
30
|
+
supported: ruby_version_supported?,
|
|
31
|
+
status: ruby_version_supported? ? "healthy" : "outdated"
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def check_database_connection
|
|
36
|
+
ActiveRecord::Base.connection.active?
|
|
37
|
+
{ status: "healthy", connected: true }
|
|
38
|
+
rescue => e
|
|
39
|
+
{ status: "unhealthy", connected: false, error: e.message }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def check_gems_health
|
|
43
|
+
GemAnalyzer.new.analyze
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check_security_vulnerabilities
|
|
47
|
+
outdated_gems = `bundle outdated --parseable`.split("\n")
|
|
48
|
+
{
|
|
49
|
+
outdated_count: outdated_gems.length,
|
|
50
|
+
status: outdated_gems.empty? ? "secure" : "needs_attention"
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def check_background_jobs
|
|
55
|
+
JobAnalyzer.new.analyze
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def check_system_details
|
|
59
|
+
SystemAnalyzer.new.analyze
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def rails_version_supported?
|
|
63
|
+
Rails.version >= "6.0"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def ruby_version_supported?
|
|
67
|
+
RUBY_VERSION >= "2.7"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def generate_report(results)
|
|
71
|
+
puts "\n=== Rails Health Check Report ==="
|
|
72
|
+
puts "Rails Version: #{results[:rails_version][:current]} (#{results[:rails_version][:status]})"
|
|
73
|
+
puts "Ruby Version: #{results[:ruby_version][:current]} (#{results[:ruby_version][:status]})"
|
|
74
|
+
puts "Database: #{results[:database][:status]}"
|
|
75
|
+
puts "Gems: #{results[:gems][:total]} total, #{results[:gems][:outdated]} outdated"
|
|
76
|
+
puts "Security: #{results[:security][:status]}"
|
|
77
|
+
puts "Background Jobs: #{results[:jobs][:status]}"
|
|
78
|
+
puts "================================\n"
|
|
79
|
+
|
|
80
|
+
# Generate markdown report
|
|
81
|
+
report_generator = ReportGenerator.new(results)
|
|
82
|
+
filename = report_generator.save_to_file
|
|
83
|
+
puts "📄 Detailed report saved to: #{filename} (previous report replaced)"
|
|
84
|
+
|
|
85
|
+
results
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
module RailsHealthChecker
|
|
2
|
+
class DashboardMiddleware
|
|
3
|
+
def initialize(app)
|
|
4
|
+
@app = app
|
|
5
|
+
require 'rack/auth/basic'
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def call(env)
|
|
9
|
+
if env['PATH_INFO'] == '/health'
|
|
10
|
+
authenticate(env) ? dashboard_response : unauthorized_response
|
|
11
|
+
else
|
|
12
|
+
@app.call(env)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def authenticate(env)
|
|
19
|
+
auth = Rack::Auth::Basic::Request.new(env)
|
|
20
|
+
auth.provided? && auth.basic? && auth.credentials &&
|
|
21
|
+
auth.credentials == [username, password]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def username
|
|
25
|
+
ENV['HEALTH_USERNAME'] || 'admin'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def password
|
|
29
|
+
ENV['HEALTH_PASSWORD'] || 'health123'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def unauthorized_response
|
|
33
|
+
[
|
|
34
|
+
401,
|
|
35
|
+
{
|
|
36
|
+
'Content-Type' => 'text/html',
|
|
37
|
+
'WWW-Authenticate' => 'Basic realm="Health Dashboard"'
|
|
38
|
+
},
|
|
39
|
+
['<h1>401 Unauthorized</h1><p>Please provide valid credentials.</p>']
|
|
40
|
+
]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def dashboard_response
|
|
44
|
+
results = RailsHealthChecker::Checker.new.run
|
|
45
|
+
|
|
46
|
+
[
|
|
47
|
+
200,
|
|
48
|
+
{ 'Content-Type' => 'text/html' },
|
|
49
|
+
[generate_dashboard_html(results)]
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate_dashboard_html(results)
|
|
54
|
+
<<~HTML
|
|
55
|
+
<!DOCTYPE html>
|
|
56
|
+
<html>
|
|
57
|
+
<head>
|
|
58
|
+
<title>Rails Health Dashboard</title>
|
|
59
|
+
<meta charset="utf-8">
|
|
60
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
61
|
+
<style>
|
|
62
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
63
|
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f7fa; }
|
|
64
|
+
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
|
65
|
+
.header { text-align: center; margin-bottom: 30px; }
|
|
66
|
+
.header h1 { color: #2d3748; font-size: 2.5rem; margin-bottom: 10px; }
|
|
67
|
+
.timestamp { color: #718096; font-size: 0.9rem; }
|
|
68
|
+
.search-container { margin: 20px 0; text-align: center; }
|
|
69
|
+
.search-box { padding: 12px 20px; border: 2px solid #e2e8f0; border-radius: 25px; width: 300px; font-size: 1rem; outline: none; }
|
|
70
|
+
.search-box:focus { border-color: #4299e1; }
|
|
71
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; }
|
|
72
|
+
.card { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); border: 1px solid #e2e8f0; }
|
|
73
|
+
.card h3 { color: #2d3748; margin-bottom: 16px; font-size: 1.2rem; display: flex; align-items: center; gap: 8px; }
|
|
74
|
+
.status-healthy { color: #38a169; }
|
|
75
|
+
.status-warning { color: #d69e2e; }
|
|
76
|
+
.status-error { color: #e53e3e; }
|
|
77
|
+
.status-loaded { color: #38a169; }
|
|
78
|
+
.status-missing { color: #e53e3e; }
|
|
79
|
+
.status-not_loaded { color: #718096; }
|
|
80
|
+
.metric { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #f7fafc; }
|
|
81
|
+
.metric:last-child { border-bottom: none; }
|
|
82
|
+
.metric-label { color: #4a5568; }
|
|
83
|
+
.metric-value { font-weight: 600; }
|
|
84
|
+
.lib-item { padding: 6px 12px; margin: 2px 0; background: #f7fafc; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; font-size: 0.9rem; }
|
|
85
|
+
.lib-required { border-left: 3px solid #4299e1; }
|
|
86
|
+
.lib-optional { border-left: 3px solid #38a169; }
|
|
87
|
+
.lib-missing { background: #fed7d7; }
|
|
88
|
+
.expandable { max-height: 200px; overflow-y: auto; }
|
|
89
|
+
.health-score { text-align: center; padding: 20px; }
|
|
90
|
+
.score-circle { width: 120px; height: 120px; border-radius: 50%; margin: 0 auto 16px; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: bold; color: white; }
|
|
91
|
+
.score-excellent { background: linear-gradient(135deg, #38a169, #48bb78); }
|
|
92
|
+
.score-good { background: linear-gradient(135deg, #d69e2e, #ecc94b); }
|
|
93
|
+
.score-warning { background: linear-gradient(135deg, #ed8936, #f6ad55); }
|
|
94
|
+
.score-critical { background: linear-gradient(135deg, #e53e3e, #fc8181); }
|
|
95
|
+
.gem-list { max-height: 200px; overflow-y: auto; }
|
|
96
|
+
.gem-item { padding: 8px 12px; margin: 4px 0; background: #f7fafc; border-radius: 6px; display: flex; justify-content: space-between; }
|
|
97
|
+
.gem-outdated { background: #fed7d7; }
|
|
98
|
+
.actions { background: white; border-radius: 12px; padding: 24px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); }
|
|
99
|
+
.action-item { padding: 12px; margin: 8px 0; border-radius: 8px; display: flex; align-items: center; gap: 12px; }
|
|
100
|
+
.action-critical { background: #fed7d7; border-left: 4px solid #e53e3e; }
|
|
101
|
+
.action-high { background: #fef5e7; border-left: 4px solid #d69e2e; }
|
|
102
|
+
.action-medium { background: #e6fffa; border-left: 4px solid #38a169; }
|
|
103
|
+
.refresh-btn { position: fixed; bottom: 20px; right: 20px; background: #4299e1; color: white; border: none; padding: 12px 24px; border-radius: 25px; cursor: pointer; box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3); }
|
|
104
|
+
.refresh-btn:hover { background: #3182ce; }
|
|
105
|
+
.auto-refresh-btn { position: fixed; bottom: 20px; right: 180px; background: #38a169; color: white; border: none; padding: 12px 24px; border-radius: 25px; cursor: pointer; box-shadow: 0 4px 12px rgba(56, 161, 105, 0.3); }
|
|
106
|
+
.auto-refresh-btn:hover { background: #2f855a; }
|
|
107
|
+
.auto-refresh-btn.disabled { background: #718096; }
|
|
108
|
+
.auto-refresh-btn.disabled:hover { background: #4a5568; }
|
|
109
|
+
.hidden { display: none; }
|
|
110
|
+
</style>
|
|
111
|
+
</head>
|
|
112
|
+
<body>
|
|
113
|
+
<div class="container">
|
|
114
|
+
<div class="header">
|
|
115
|
+
<h1>🏥 Rails Health Dashboard</h1>
|
|
116
|
+
<div class="timestamp">Last updated: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}</div>
|
|
117
|
+
<div class="search-container">
|
|
118
|
+
<input type="text" class="search-box" placeholder="Search libraries (e.g., sidekiq, redis, puma...)" id="libSearch">
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="grid">
|
|
123
|
+
#{system_overview_card(results)}
|
|
124
|
+
#{libraries_card(results)}
|
|
125
|
+
#{database_card(results)}
|
|
126
|
+
#{gems_card(results)}
|
|
127
|
+
#{security_card(results)}
|
|
128
|
+
#{jobs_card(results)}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
#{health_score_card(results)}
|
|
132
|
+
#{priority_actions_card(results)}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<button class="auto-refresh-btn" id="autoRefreshBtn" onclick="toggleAutoRefresh()">⏸️ Auto-Refresh: ON</button>
|
|
136
|
+
<button class="refresh-btn" onclick="location.reload()">🔄 Refresh</button>
|
|
137
|
+
|
|
138
|
+
<script>
|
|
139
|
+
let autoRefreshEnabled = true;
|
|
140
|
+
let refreshTimer;
|
|
141
|
+
|
|
142
|
+
function startAutoRefresh() {
|
|
143
|
+
if (autoRefreshEnabled) {
|
|
144
|
+
refreshTimer = setTimeout(() => location.reload(), 30000);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toggleAutoRefresh() {
|
|
149
|
+
const btn = document.getElementById('autoRefreshBtn');
|
|
150
|
+
autoRefreshEnabled = !autoRefreshEnabled;
|
|
151
|
+
|
|
152
|
+
if (autoRefreshEnabled) {
|
|
153
|
+
btn.textContent = '⏸️ Auto-Refresh: ON';
|
|
154
|
+
btn.classList.remove('disabled');
|
|
155
|
+
startAutoRefresh();
|
|
156
|
+
} else {
|
|
157
|
+
btn.textContent = '▶️ Auto-Refresh: OFF';
|
|
158
|
+
btn.classList.add('disabled');
|
|
159
|
+
clearTimeout(refreshTimer);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Start auto-refresh on page load
|
|
164
|
+
startAutoRefresh();
|
|
165
|
+
|
|
166
|
+
// Search functionality
|
|
167
|
+
document.getElementById('libSearch').addEventListener('input', function(e) {
|
|
168
|
+
const searchTerm = e.target.value.toLowerCase();
|
|
169
|
+
const libItems = document.querySelectorAll('.lib-item');
|
|
170
|
+
|
|
171
|
+
libItems.forEach(item => {
|
|
172
|
+
const libName = item.querySelector('.lib-name').textContent.toLowerCase();
|
|
173
|
+
const libPurpose = item.querySelector('.lib-purpose').textContent.toLowerCase();
|
|
174
|
+
|
|
175
|
+
if (libName.includes(searchTerm) || libPurpose.includes(searchTerm)) {
|
|
176
|
+
item.classList.remove('hidden');
|
|
177
|
+
} else {
|
|
178
|
+
item.classList.add('hidden');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
</script>
|
|
183
|
+
</body>
|
|
184
|
+
</html>
|
|
185
|
+
HTML
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def system_overview_card(results)
|
|
189
|
+
rails_status = results[:rails_version][:status]
|
|
190
|
+
ruby_status = results[:ruby_version][:status]
|
|
191
|
+
system = results[:system]
|
|
192
|
+
|
|
193
|
+
<<~HTML
|
|
194
|
+
<div class="card">
|
|
195
|
+
<h3>🔧 System Overview</h3>
|
|
196
|
+
<div class="metric">
|
|
197
|
+
<span class="metric-label">Rails Version</span>
|
|
198
|
+
<span class="metric-value status-#{rails_status}">#{results[:rails_version][:current]}</span>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="metric">
|
|
201
|
+
<span class="metric-label">Ruby Version</span>
|
|
202
|
+
<span class="metric-value status-#{ruby_status}">#{results[:ruby_version][:current]}</span>
|
|
203
|
+
</div>
|
|
204
|
+
<div class="metric">
|
|
205
|
+
<span class="metric-label">Environment</span>
|
|
206
|
+
<span class="metric-value">#{system[:environment_info][:rails_env]}</span>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="metric">
|
|
209
|
+
<span class="metric-label">Web Server</span>
|
|
210
|
+
<span class="metric-value">#{system[:server_requirements][:web_server].capitalize}</span>
|
|
211
|
+
</div>
|
|
212
|
+
<div class="metric">
|
|
213
|
+
<span class="metric-label">Database</span>
|
|
214
|
+
<span class="metric-value">#{system[:server_requirements][:database]}</span>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="metric">
|
|
217
|
+
<span class="metric-label">Cache Store</span>
|
|
218
|
+
<span class="metric-value">#{system[:server_requirements][:cache_store][:type].split('::').last}</span>
|
|
219
|
+
</div>
|
|
220
|
+
<div style="font-size: 0.8rem; color: #718096; margin-top: 4px;">#{system[:server_requirements][:cache_store][:explanation]}</div>
|
|
221
|
+
<div style="font-size: 0.8rem; color: #4299e1; margin-top: 2px;">💡 #{system[:server_requirements][:cache_store][:recommendation]}</div>
|
|
222
|
+
</div>
|
|
223
|
+
HTML
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def database_card(results)
|
|
227
|
+
db_status = results[:database][:status]
|
|
228
|
+
status_class = db_status == 'healthy' ? 'status-healthy' : 'status-error'
|
|
229
|
+
|
|
230
|
+
<<~HTML
|
|
231
|
+
<div class="card">
|
|
232
|
+
<h3>💾 Database</h3>
|
|
233
|
+
<div class="metric">
|
|
234
|
+
<span class="metric-label">Connection Status</span>
|
|
235
|
+
<span class="metric-value #{status_class}">#{db_status.capitalize}</span>
|
|
236
|
+
</div>
|
|
237
|
+
<div class="metric">
|
|
238
|
+
<span class="metric-label">Connected</span>
|
|
239
|
+
<span class="metric-value">#{results[:database][:connected] ? '✅ Yes' : '❌ No'}</span>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
HTML
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def gems_card(results)
|
|
246
|
+
outdated_count = results[:gems][:outdated]
|
|
247
|
+
total_count = results[:gems][:total]
|
|
248
|
+
|
|
249
|
+
<<~HTML
|
|
250
|
+
<div class="card">
|
|
251
|
+
<h3>📦 Gem Dependencies</h3>
|
|
252
|
+
<div class="metric">
|
|
253
|
+
<span class="metric-label">Total Gems</span>
|
|
254
|
+
<span class="metric-value">#{total_count}</span>
|
|
255
|
+
</div>
|
|
256
|
+
<div class="metric">
|
|
257
|
+
<span class="metric-label">Outdated</span>
|
|
258
|
+
<span class="metric-value status-#{outdated_count > 0 ? 'warning' : 'healthy'}">#{outdated_count}</span>
|
|
259
|
+
</div>
|
|
260
|
+
<div class="gem-list">
|
|
261
|
+
#{gem_list_html(results[:gems][:details])}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
HTML
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def gem_list_html(gems)
|
|
268
|
+
gems.first(10).map do |gem|
|
|
269
|
+
css_class = gem[:outdated] ? 'gem-item gem-outdated' : 'gem-item'
|
|
270
|
+
status = gem[:outdated] ? '⚠️' : '✅'
|
|
271
|
+
<<~HTML
|
|
272
|
+
<div class="#{css_class}">
|
|
273
|
+
<span>#{gem[:name]}</span>
|
|
274
|
+
<span>#{status} #{gem[:version]}</span>
|
|
275
|
+
</div>
|
|
276
|
+
HTML
|
|
277
|
+
end.join
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def security_card(results)
|
|
281
|
+
security_status = results[:security][:status]
|
|
282
|
+
outdated_count = results[:security][:outdated_count]
|
|
283
|
+
|
|
284
|
+
<<~HTML
|
|
285
|
+
<div class="card">
|
|
286
|
+
<h3>🔒 Security</h3>
|
|
287
|
+
<div class="metric">
|
|
288
|
+
<span class="metric-label">Status</span>
|
|
289
|
+
<span class="metric-value status-#{security_status == 'secure' ? 'healthy' : 'warning'}">#{security_status.capitalize}</span>
|
|
290
|
+
</div>
|
|
291
|
+
<div class="metric">
|
|
292
|
+
<span class="metric-label">Issues Found</span>
|
|
293
|
+
<span class="metric-value">#{outdated_count}</span>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
HTML
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def health_score_card(results)
|
|
300
|
+
score = calculate_health_score(results)
|
|
301
|
+
score_class, score_text = get_score_class_and_text(score)
|
|
302
|
+
reasons = get_dashboard_score_reasons(results)
|
|
303
|
+
|
|
304
|
+
<<~HTML
|
|
305
|
+
<div class="card health-score">
|
|
306
|
+
<h3>🏆 Overall Health Score</h3>
|
|
307
|
+
<div class="score-circle #{score_class}">#{score}</div>
|
|
308
|
+
<div style="font-size: 1.2rem; font-weight: 600; color: #2d3748; margin-bottom: 16px;">#{score_text}</div>
|
|
309
|
+
#{reasons}
|
|
310
|
+
</div>
|
|
311
|
+
HTML
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def priority_actions_card(results)
|
|
315
|
+
actions = generate_priority_actions(results)
|
|
316
|
+
|
|
317
|
+
<<~HTML
|
|
318
|
+
<div class="actions">
|
|
319
|
+
<h3>🎯 Priority Actions</h3>
|
|
320
|
+
#{actions.empty? ? '<p style="text-align: center; color: #38a169; font-size: 1.1rem;">✅ No critical actions required!</p>' : actions.join}
|
|
321
|
+
</div>
|
|
322
|
+
HTML
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def calculate_health_score(results)
|
|
326
|
+
score = 100
|
|
327
|
+
score -= 20 if results[:rails_version][:status] == 'outdated'
|
|
328
|
+
score -= 15 if results[:ruby_version][:status] == 'outdated'
|
|
329
|
+
score -= 30 if results[:database][:status] == 'unhealthy'
|
|
330
|
+
score -= (results[:gems][:outdated] * 2)
|
|
331
|
+
score -= (results[:security][:outdated_count] * 3)
|
|
332
|
+
[score, 0].max
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def get_score_class_and_text(score)
|
|
336
|
+
case score
|
|
337
|
+
when 90..100 then ['score-excellent', 'Excellent']
|
|
338
|
+
when 70..89 then ['score-good', 'Good']
|
|
339
|
+
when 50..69 then ['score-warning', 'Needs Attention']
|
|
340
|
+
else ['score-critical', 'Critical']
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def jobs_card(results)
|
|
345
|
+
jobs = results[:jobs]
|
|
346
|
+
job_status = jobs[:status]
|
|
347
|
+
status_class = case job_status
|
|
348
|
+
when 'healthy' then 'status-healthy'
|
|
349
|
+
when 'warning' then 'status-warning'
|
|
350
|
+
when 'critical', 'error' then 'status-error'
|
|
351
|
+
else 'status-warning'
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
<<~HTML
|
|
355
|
+
<div class="card">
|
|
356
|
+
<h3>⚙️ Background Jobs</h3>
|
|
357
|
+
<div class="metric">
|
|
358
|
+
<span class="metric-label">Overall Status</span>
|
|
359
|
+
<span class="metric-value #{status_class}">#{job_status.capitalize}</span>
|
|
360
|
+
</div>
|
|
361
|
+
#{job_details_html(jobs)}
|
|
362
|
+
</div>
|
|
363
|
+
HTML
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def job_details_html(jobs)
|
|
367
|
+
details = []
|
|
368
|
+
|
|
369
|
+
if jobs[:sidekiq][:available]
|
|
370
|
+
sidekiq = jobs[:sidekiq]
|
|
371
|
+
if sidekiq[:error]
|
|
372
|
+
details << '<div class="metric"><span class="metric-label">Sidekiq</span><span class="metric-value status-error">Error</span></div>'
|
|
373
|
+
details << "<div style=\"font-size: 0.8rem; color: #e53e3e; margin-top: 4px;\">#{sidekiq[:error]}</div>"
|
|
374
|
+
else
|
|
375
|
+
details << "<div class=\"metric\"><span class=\"metric-label\">Sidekiq Enqueued</span><span class=\"metric-value\">#{sidekiq[:enqueued]}</span></div>"
|
|
376
|
+
details << "<div class=\"metric\"><span class=\"metric-label\">Sidekiq Failed</span><span class=\"metric-value\">#{sidekiq[:failed]}</span></div>"
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
if jobs[:resque][:available]
|
|
381
|
+
resque = jobs[:resque]
|
|
382
|
+
if resque[:error]
|
|
383
|
+
details << '<div class="metric"><span class="metric-label">Resque</span><span class="metric-value status-error">Error</span></div>'
|
|
384
|
+
details << "<div style=\"font-size: 0.8rem; color: #e53e3e; margin-top: 4px;\">#{resque[:error]}</div>"
|
|
385
|
+
else
|
|
386
|
+
details << "<div class=\"metric\"><span class=\"metric-label\">Resque Pending</span><span class=\"metric-value\">#{resque[:pending]}</span></div>"
|
|
387
|
+
details << "<div class=\"metric\"><span class=\"metric-label\">Resque Failed</span><span class=\"metric-value\">#{resque[:failed]}</span></div>"
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
if jobs[:active_job][:available]
|
|
392
|
+
active_job = jobs[:active_job]
|
|
393
|
+
details << "<div class=\"metric\"><span class=\"metric-label\">ActiveJob Adapter</span><span class=\"metric-value\">#{active_job[:adapter]}</span></div>"
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
details.empty? ? '<div class="metric"><span class="metric-label">Status</span><span class="metric-value">Not Configured</span></div>' : details.join
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def generate_priority_actions(results)
|
|
400
|
+
actions = []
|
|
401
|
+
|
|
402
|
+
if results[:database][:status] == 'unhealthy'
|
|
403
|
+
actions << '<div class="action-item action-critical">🔴 <strong>CRITICAL:</strong> Fix database connection immediately</div>'
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
if results[:jobs][:status] == 'critical'
|
|
407
|
+
actions << '<div class="action-item action-critical">🔴 <strong>CRITICAL:</strong> Background job system failure</div>'
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
if results[:rails_version][:status] == 'outdated'
|
|
411
|
+
actions << '<div class="action-item action-high">🟡 <strong>HIGH:</strong> Update Rails framework</div>'
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
if results[:security][:outdated_count] > 5
|
|
415
|
+
actions << '<div class="action-item action-high">🟡 <strong>HIGH:</strong> Address security vulnerabilities</div>'
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
if results[:jobs][:status] == 'warning'
|
|
419
|
+
actions << '<div class="action-item action-medium">🟡 <strong>MEDIUM:</strong> Check background job queues</div>'
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
if results[:gems][:outdated] > 10
|
|
423
|
+
actions << '<div class="action-item action-medium">🟢 <strong>MEDIUM:</strong> Update outdated gems</div>'
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
actions
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def get_dashboard_score_reasons(results)
|
|
430
|
+
score = calculate_health_score(results)
|
|
431
|
+
return '' if score >= 90
|
|
432
|
+
|
|
433
|
+
reasons = []
|
|
434
|
+
|
|
435
|
+
if results[:database][:status] == 'unhealthy'
|
|
436
|
+
reasons << '<div style="color: #e53e3e; font-size: 0.9rem; margin: 4px 0;">❌ Database connection failed</div>'
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
if results[:rails_version][:status] == 'outdated'
|
|
440
|
+
reasons << '<div style="color: #d69e2e; font-size: 0.9rem; margin: 4px 0;">⚠️ Rails version outdated</div>'
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
if results[:gems][:outdated] > 20
|
|
444
|
+
reasons << '<div style="color: #d69e2e; font-size: 0.9rem; margin: 4px 0;">⚠️ Many outdated gems (#{results[:gems][:outdated]})</div>'
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
if results[:security][:outdated_count] > 10
|
|
448
|
+
reasons << '<div style="color: #d69e2e; font-size: 0.9rem; margin: 4px 0;">⚠️ Security vulnerabilities (#{results[:security][:outdated_count]})</div>'
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
if results[:jobs][:status] == 'critical'
|
|
452
|
+
reasons << '<div style="color: #e53e3e; font-size: 0.9rem; margin: 4px 0;">❌ Background jobs critical</div>'
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
return '' if reasons.empty?
|
|
456
|
+
|
|
457
|
+
'<div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid #e2e8f0;">' +
|
|
458
|
+
'<div style="font-size: 0.9rem; font-weight: 600; color: #4a5568; margin-bottom: 8px;">Issues affecting score:</div>' +
|
|
459
|
+
reasons.join +
|
|
460
|
+
'</div>'
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def libraries_card(results)
|
|
464
|
+
system = results[:system]
|
|
465
|
+
required_libs = system[:required_libs]
|
|
466
|
+
optional_libs = system[:optional_libs]
|
|
467
|
+
|
|
468
|
+
<<~HTML
|
|
469
|
+
<div class="card">
|
|
470
|
+
<h3>📚 Required Libraries</h3>
|
|
471
|
+
<div class="expandable">
|
|
472
|
+
#{library_list_html(required_libs, 'required')}
|
|
473
|
+
</div>
|
|
474
|
+
<h3 style="margin-top: 20px;">🔌 Optional Libraries</h3>
|
|
475
|
+
<div class="expandable">
|
|
476
|
+
#{library_list_html(optional_libs, 'optional')}
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
HTML
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def library_list_html(libraries, type)
|
|
483
|
+
libraries.map do |name, info|
|
|
484
|
+
status_class = "status-#{info[:status]}"
|
|
485
|
+
lib_class = "lib-item lib-#{type}"
|
|
486
|
+
lib_class += " lib-missing" if info[:status] == 'missing'
|
|
487
|
+
|
|
488
|
+
version_text = info[:version] ? " (#{info[:version]})" : ''
|
|
489
|
+
status_text = info[:status] == 'loaded' ? '✅' : (info[:status] == 'missing' ? '❌' : '⏸️')
|
|
490
|
+
|
|
491
|
+
<<~HTML
|
|
492
|
+
<div class="#{lib_class}">
|
|
493
|
+
<div>
|
|
494
|
+
<span class="lib-name">#{name}#{version_text}</span>
|
|
495
|
+
<div class="lib-purpose" style="font-size: 0.8rem; color: #718096;">#{info[:purpose]}</div>
|
|
496
|
+
</div>
|
|
497
|
+
<span class="#{status_class}">#{status_text}</span>
|
|
498
|
+
</div>
|
|
499
|
+
HTML
|
|
500
|
+
end.join
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
end
|