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.
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