solidstats 2.0.0 → 3.0.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.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +12 -63
  3. data/README.md +27 -0
  4. data/Rakefile +3 -3
  5. data/app/assets/javascripts/solidstats/dashboard.js +0 -46
  6. data/app/assets/stylesheets/solidstats/dashboard.css +48 -0
  7. data/app/controllers/solidstats/dashboard_controller.rb +73 -61
  8. data/app/controllers/solidstats/logs_controller.rb +72 -0
  9. data/app/controllers/solidstats/performance_controller.rb +25 -0
  10. data/app/controllers/solidstats/productivity_controller.rb +39 -0
  11. data/app/controllers/solidstats/quality_controller.rb +152 -0
  12. data/app/controllers/solidstats/securities_controller.rb +30 -0
  13. data/app/helpers/solidstats/application_helper.rb +124 -11
  14. data/app/helpers/solidstats/performance_helper.rb +87 -0
  15. data/app/helpers/solidstats/productivity_helper.rb +38 -0
  16. data/app/services/solidstats/bundler_audit_service.rb +206 -0
  17. data/app/services/solidstats/coverage_compass_service.rb +335 -0
  18. data/app/services/solidstats/load_lens_service.rb +474 -0
  19. data/app/services/solidstats/log_size_monitor_service.rb +197 -66
  20. data/app/services/solidstats/my_todo_service.rb +242 -0
  21. data/app/services/solidstats/style_patrol_service.rb +319 -0
  22. data/app/views/layouts/solidstats/application.html.erb +8 -2
  23. data/app/views/layouts/solidstats/dashboard.html.erb +84 -0
  24. data/app/views/solidstats/dashboard/dashboard.html.erb +39 -0
  25. data/app/views/solidstats/logs/logs_size.html.erb +409 -0
  26. data/app/views/solidstats/performance/load_lens.html.erb +158 -0
  27. data/app/views/solidstats/productivity/_todo_list.html.erb +49 -0
  28. data/app/views/solidstats/productivity/my_todos.html.erb +84 -0
  29. data/app/views/solidstats/quality/coverage_compass.html.erb +420 -0
  30. data/app/views/solidstats/quality/style_patrol.html.erb +463 -0
  31. data/app/views/solidstats/securities/bundler_audit.html.erb +345 -0
  32. data/app/views/solidstats/shared/_dashboard_card.html.erb +160 -0
  33. data/app/views/solidstats/shared/_quick_actions.html.erb +26 -0
  34. data/config/routes.rb +31 -6
  35. data/lib/generators/solidstats/clean/clean_generator.rb +24 -0
  36. data/lib/generators/solidstats/clean/templates/README +8 -0
  37. data/lib/generators/solidstats/install/install_generator.rb +51 -10
  38. data/lib/generators/solidstats/install/templates/README +7 -0
  39. data/lib/solidstats/engine.rb +6 -71
  40. data/lib/solidstats/version.rb +1 -1
  41. data/lib/solidstats.rb +19 -303
  42. data/lib/tasks/solidstats.rake +67 -0
  43. data/lib/tasks/solidstats_performance.rake +61 -0
  44. data/lib/tasks/solidstats_tasks.rake +16 -4
  45. metadata +33 -95
  46. data/app/assets/javascripts/solidstats/gem_metadata.js +0 -554
  47. data/app/assets/stylesheets/solidstats/components/action_button.css +0 -99
  48. data/app/assets/stylesheets/solidstats/components/dashboard.css +0 -151
  49. data/app/assets/stylesheets/solidstats/components/dashboard_header.css +0 -93
  50. data/app/assets/stylesheets/solidstats/components/dashboard_layout.css +0 -97
  51. data/app/assets/stylesheets/solidstats/components/gem_metadata.css +0 -1403
  52. data/app/assets/stylesheets/solidstats/components/navigation.css +0 -80
  53. data/app/assets/stylesheets/solidstats/components/quick_navigation.css +0 -54
  54. data/app/assets/stylesheets/solidstats/components/security.css +0 -332
  55. data/app/assets/stylesheets/solidstats/components/status_badge.css +0 -58
  56. data/app/assets/stylesheets/solidstats/components/summary_card.css +0 -66
  57. data/app/assets/stylesheets/solidstats/components/tab_navigation.css +0 -95
  58. data/app/components/solidstats/base_component.rb +0 -88
  59. data/app/components/solidstats/code_quality/code_quality_section_component.html.erb +0 -0
  60. data/app/components/solidstats/code_quality/code_quality_section_component.rb +0 -0
  61. data/app/components/solidstats/code_quality/section_component.html.erb +0 -45
  62. data/app/components/solidstats/code_quality/section_component.rb +0 -34
  63. data/app/components/solidstats/dashboard_header_component.html.erb +0 -39
  64. data/app/components/solidstats/dashboard_header_component.rb +0 -33
  65. data/app/components/solidstats/previews/action_button_component_preview/button_vs_link.html.erb +0 -6
  66. data/app/components/solidstats/previews/action_button_component_preview/sizes.html.erb +0 -6
  67. data/app/components/solidstats/previews/action_button_component_preview/variants.html.erb +0 -6
  68. data/app/components/solidstats/previews/action_button_component_preview/with_icons.html.erb +0 -6
  69. data/app/components/solidstats/previews/action_button_component_preview.rb +0 -64
  70. data/app/components/solidstats/previews/navigation_component_preview.rb +0 -74
  71. data/app/components/solidstats/previews/stats_overview_component_preview.rb +0 -100
  72. data/app/components/solidstats/previews/status_badge_component_preview/sizes.html.erb +0 -6
  73. data/app/components/solidstats/previews/status_badge_component_preview/statuses.html.erb +0 -6
  74. data/app/components/solidstats/previews/status_badge_component_preview/with_icons.html.erb +0 -6
  75. data/app/components/solidstats/previews/status_badge_component_preview.rb +0 -49
  76. data/app/components/solidstats/previews/summary_card_component_preview/clickable.html.erb +0 -9
  77. data/app/components/solidstats/previews/summary_card_component_preview/dashboard_layout.html.erb +0 -9
  78. data/app/components/solidstats/previews/summary_card_component_preview/statuses.html.erb +0 -6
  79. data/app/components/solidstats/previews/summary_card_component_preview/value_formats.html.erb +0 -6
  80. data/app/components/solidstats/previews/summary_card_component_preview.rb +0 -67
  81. data/app/components/solidstats/quick_navigation_component.html.erb +0 -8
  82. data/app/components/solidstats/quick_navigation_component.rb +0 -21
  83. data/app/components/solidstats/security/gem_impact_analysis_component.html.erb +0 -44
  84. data/app/components/solidstats/security/gem_impact_analysis_component.rb +0 -45
  85. data/app/components/solidstats/security/overview_component.html.erb +0 -21
  86. data/app/components/solidstats/security/overview_component.rb +0 -104
  87. data/app/components/solidstats/security/section_component.html.erb +0 -26
  88. data/app/components/solidstats/security/section_component.rb +0 -52
  89. data/app/components/solidstats/security/timeline_component.html.erb +0 -39
  90. data/app/components/solidstats/security/timeline_component.rb +0 -43
  91. data/app/components/solidstats/tasks_section_component.html.erb +0 -17
  92. data/app/components/solidstats/tasks_section_component.rb +0 -22
  93. data/app/components/solidstats/ui/action_button_component.html.erb +0 -6
  94. data/app/components/solidstats/ui/action_button_component.rb +0 -71
  95. data/app/components/solidstats/ui/dashboard_layout_component.html.erb +0 -19
  96. data/app/components/solidstats/ui/dashboard_layout_component.rb +0 -85
  97. data/app/components/solidstats/ui/navigation_component.html.erb +0 -34
  98. data/app/components/solidstats/ui/navigation_component.rb +0 -72
  99. data/app/components/solidstats/ui/stats_overview_component.html.erb +0 -14
  100. data/app/components/solidstats/ui/stats_overview_component.rb +0 -78
  101. data/app/components/solidstats/ui/status_badge_component.html.erb +0 -6
  102. data/app/components/solidstats/ui/status_badge_component.rb +0 -42
  103. data/app/components/solidstats/ui/summary_card_component.html.erb +0 -12
  104. data/app/components/solidstats/ui/summary_card_component.rb +0 -63
  105. data/app/components/solidstats/ui/tab_navigation_component.html.erb +0 -22
  106. data/app/components/solidstats/ui/tab_navigation_component.rb +0 -79
  107. data/app/services/solidstats/audit_service.rb +0 -56
  108. data/app/services/solidstats/data_collector_service.rb +0 -83
  109. data/app/services/solidstats/gem_metadata/fetcher_service.rb +0 -136
  110. data/app/services/solidstats/todo_service.rb +0 -114
  111. data/app/views/solidstats/dashboard/_log_monitor.html.erb +0 -759
  112. data/app/views/solidstats/dashboard/_todos.html.erb +0 -151
  113. data/app/views/solidstats/dashboard/audit/_additional_styles.css +0 -22
  114. data/app/views/solidstats/dashboard/audit/_audit_badge.html.erb +0 -5
  115. data/app/views/solidstats/dashboard/audit/_audit_details.html.erb +0 -495
  116. data/app/views/solidstats/dashboard/audit/_audit_summary.html.erb +0 -26
  117. data/app/views/solidstats/dashboard/audit/_no_vulnerabilities.html.erb +0 -3
  118. data/app/views/solidstats/dashboard/audit/_security_audit.html.erb +0 -14
  119. data/app/views/solidstats/dashboard/audit/_vulnerabilities_table.html.erb +0 -1120
  120. data/app/views/solidstats/dashboard/audit/_vulnerability_details.html.erb +0 -63
  121. data/app/views/solidstats/dashboard/index.html.erb +0 -81
  122. data/app/views/solidstats/gem_metadata/_panel.html.erb +0 -419
  123. data/lib/generators/solidstats/feature/feature_generator.rb +0 -170
  124. data/lib/generators/solidstats/feature/templates/component.html.erb +0 -84
  125. data/lib/generators/solidstats/feature/templates/component.rb.erb +0 -103
  126. data/lib/generators/solidstats/feature/templates/component.scss +0 -243
  127. data/lib/generators/solidstats/feature/templates/component_test.rb.erb +0 -183
  128. data/lib/generators/solidstats/feature/templates/controller.rb.erb +0 -44
  129. data/lib/generators/solidstats/feature/templates/controller_test.rb.erb +0 -111
  130. data/lib/generators/solidstats/feature/templates/detail_view.html.erb +0 -755
  131. data/lib/generators/solidstats/feature/templates/preview.rb.erb +0 -107
  132. data/lib/generators/solidstats/feature/templates/service.rb.erb +0 -132
  133. data/lib/generators/solidstats/feature/templates/service_test.rb.erb +0 -109
  134. data/lib/generators/solidstats/install_generator.rb +0 -109
  135. data/lib/tasks/solidstats_install.rake +0 -133
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solidstats
4
+ class QualityController < ApplicationController
5
+ layout "solidstats/dashboard"
6
+
7
+ # Display Standard gem code quality analysis
8
+ def style_patrol
9
+ @analysis_data = Solidstats::StylePatrolService.collect_data
10
+ @summary = Solidstats::StylePatrolService.get_summary
11
+
12
+ # Group issues by file for better display
13
+ if @analysis_data[:issues].present?
14
+ @issues_by_file = @analysis_data[:issues].group_by { |issue| issue[:file] }
15
+ @issues_by_severity = @analysis_data[:issues].group_by { |issue| issue[:severity] }
16
+ else
17
+ @issues_by_file = {}
18
+ @issues_by_severity = {}
19
+ end
20
+
21
+ render "style_patrol"
22
+ end
23
+
24
+ # Force refresh of style patrol data
25
+ def refresh_style_patrol
26
+ @analysis_data = Solidstats::StylePatrolService.refresh_cache
27
+ redirect_to quality_style_patrol_path, notice: "Code quality analysis refreshed successfully."
28
+ rescue => e
29
+ redirect_to quality_style_patrol_path, alert: "Error refreshing analysis: #{e.message}"
30
+ end
31
+
32
+ # Display SimpleCov code coverage analysis
33
+ def coverage_compass
34
+ # Get coverage data using the correct method
35
+ @analysis_data = Solidstats::CoverageCompassService.collect_data
36
+
37
+ # Handle different response types from service
38
+ if @analysis_data
39
+ if @analysis_data[:setup_required]
40
+ @setup_instructions = @analysis_data[:instructions]
41
+ @show_setup = true
42
+ @analysis_data[:status] = "setup_required"
43
+ elsif @analysis_data[:stale_data]
44
+ @stale_warning = true
45
+ @data_age_hours = @analysis_data[:data_age_hours]
46
+ @suggestions = @analysis_data[:suggestions]
47
+ @analysis_data = @analysis_data[:last_coverage] || @analysis_data
48
+ @analysis_data[:status] = "stale"
49
+ elsif @analysis_data[:error]
50
+ @error_message = @analysis_data[:error]
51
+ @analysis_data[:status] = "error"
52
+ else
53
+ @analysis_data[:status] = "success"
54
+ end
55
+
56
+ # Organize file coverage data for better display
57
+ organize_coverage_data
58
+ else
59
+ set_error_state
60
+ @analysis_data = { status: "error" }
61
+ end
62
+
63
+ render "coverage_compass"
64
+ end
65
+
66
+ # Force refresh of coverage compass data
67
+ def refresh_coverage_compass
68
+ Solidstats::CoverageCompassService.refresh_cache
69
+ redirect_to quality_coverage_compass_path, notice: "Code coverage analysis refreshed successfully."
70
+ rescue => e
71
+ redirect_to quality_coverage_compass_path, alert: "Error refreshing coverage analysis: #{e.message}"
72
+ end
73
+
74
+ private
75
+
76
+ def organize_coverage_data
77
+ file_coverage_data = @analysis_data[:file_coverage]
78
+ if file_coverage_data.present?
79
+ @file_coverage = file_coverage_data.map do |path, data|
80
+ {
81
+ file_path: path,
82
+ coverage_percent: data[:coverage_percentage] || 0,
83
+ total_lines: data[:total_lines] || 0,
84
+ covered_lines: data[:covered_lines] || 0,
85
+ missed_lines: data[:missed_lines] || []
86
+ }
87
+ end
88
+
89
+ # Group files by coverage ranges for better visualization
90
+ @coverage_ranges = {
91
+ excellent: @file_coverage.select { |f| f[:coverage_percent] >= 90 },
92
+ good: @file_coverage.select { |f| f[:coverage_percent] >= 70 && f[:coverage_percent] < 90 },
93
+ needs_improvement: @file_coverage.select { |f| f[:coverage_percent] >= 50 && f[:coverage_percent] < 70 },
94
+ poor: @file_coverage.select { |f| f[:coverage_percent] < 50 }
95
+ }
96
+ else
97
+ @file_coverage = []
98
+ @coverage_ranges = { excellent: [], good: [], needs_improvement: [], poor: [] }
99
+ end
100
+
101
+ # Set summary data
102
+ @summary = {
103
+ overall_coverage: @analysis_data[:overall_coverage] || 0,
104
+ coverage_percent: @analysis_data[:overall_coverage] || 0,
105
+ total_lines: @analysis_data[:total_lines] || 0,
106
+ covered_lines: @analysis_data[:covered_lines] || 0,
107
+ missed_lines: (@analysis_data[:total_lines] || 0) - (@analysis_data[:covered_lines] || 0),
108
+ total_files: @file_coverage.size,
109
+ health_score: calculate_health_score(@analysis_data[:overall_coverage] || 0),
110
+ coverage_grade: determine_coverage_grade(@analysis_data[:overall_coverage] || 0)
111
+ }
112
+ end
113
+
114
+ def set_error_state
115
+ @error_message = "Failed to retrieve coverage data"
116
+ @file_coverage = []
117
+ @coverage_ranges = { excellent: [], good: [], needs_improvement: [], poor: [] }
118
+ @summary = {
119
+ overall_coverage: 0,
120
+ coverage_percent: 0,
121
+ total_lines: 0,
122
+ covered_lines: 0,
123
+ missed_lines: 0,
124
+ total_files: 0,
125
+ health_score: 0,
126
+ coverage_grade: "F"
127
+ }
128
+ end
129
+
130
+ def calculate_health_score(coverage_percent)
131
+ case coverage_percent
132
+ when 90..100 then 95
133
+ when 80..89 then 85
134
+ when 70..79 then 75
135
+ when 60..69 then 65
136
+ when 50..59 then 55
137
+ else 25
138
+ end
139
+ end
140
+
141
+ def determine_coverage_grade(coverage_percent)
142
+ case coverage_percent
143
+ when 90..100 then "A+"
144
+ when 80..89 then "A"
145
+ when 70..79 then "B"
146
+ when 60..69 then "C"
147
+ when 50..59 then "D"
148
+ else "F"
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solidstats
4
+ class SecuritiesController < ApplicationController
5
+ layout "solidstats/dashboard"
6
+
7
+ # Display bundler audit security vulnerabilities
8
+ def bundler_audit
9
+ @vulnerabilities_data = Solidstats::BundlerAuditService.fetch_vulnerabilities
10
+ @vulnerabilities = @vulnerabilities_data.dig("output", "results") || []
11
+ @summary = Solidstats::BundlerAuditService.summary
12
+ @last_updated = @vulnerabilities_data.dig("output", "created_at")
13
+
14
+ # Group vulnerabilities by severity for better display
15
+ @vulnerabilities_by_severity = @vulnerabilities.group_by do |vuln|
16
+ vuln.dig("advisory", "criticality") || "unknown"
17
+ end
18
+
19
+ render "bundler_audit"
20
+ end
21
+
22
+ # Force refresh of bundler audit data
23
+ def refresh_bundler_audit
24
+ @vulnerabilities_data = Solidstats::BundlerAuditService.scan_and_cache
25
+ redirect_to securities_bundler_audit_path, notice: "Security vulnerabilities refreshed successfully."
26
+ rescue => e
27
+ redirect_to securities_bundler_audit_path, alert: "Error refreshing vulnerabilities: #{e.message}"
28
+ end
29
+ end
30
+ end
@@ -1,16 +1,52 @@
1
1
  module Solidstats
2
2
  module ApplicationHelper
3
+ def time_ago_in_words(from_time)
4
+ return "just now" if from_time.nil?
5
+
6
+ distance_in_seconds = (Time.current - from_time).to_i
7
+
8
+ case distance_in_seconds
9
+ when 0..29
10
+ "just now"
11
+ when 30..59
12
+ "#{distance_in_seconds} seconds"
13
+ when 60..3599
14
+ minutes = distance_in_seconds / 60
15
+ "#{minutes} minute#{'s' if minutes != 1}"
16
+ when 3600..86399
17
+ hours = distance_in_seconds / 3600
18
+ "#{hours} hour#{'s' if hours != 1}"
19
+ else
20
+ days = distance_in_seconds / 86400
21
+ "#{days} day#{'s' if days != 1}"
22
+ end
23
+ end
24
+
25
+ def quick_actions
26
+ [
27
+ { icon: "refresh-ccw", label: "Refresh All", color: "blue" },
28
+ { icon: "download", label: "Export Data", color: "green" },
29
+ { icon: "bar-chart-2", label: "View Reports", color: "purple" },
30
+ { icon: "tool", label: "Settings", color: "orange" }
31
+ ]
32
+ end
33
+
3
34
  # Inline CSS helper - reads all CSS files and returns them as a style tag
4
35
  def solidstats_styles
5
36
  return @solidstats_styles_cache if defined?(@solidstats_styles_cache)
6
37
 
7
38
  begin
8
39
  engine_root = Solidstats::Engine.root
9
- css_files = Dir.glob("#{engine_root}/app/assets/stylesheets/solidstats/components/*.css")
40
+ css_files = Dir.glob("#{engine_root}/app/assets/stylesheets/solidstats/*.css")
10
41
 
11
42
  combined_css = css_files.map do |file_path|
12
- "/* === #{File.basename(file_path)} === */\n#{File.read(file_path)}"
13
- end.join("\n\n")
43
+ relative_path = file_path.gsub("#{engine_root}/app/assets/stylesheets/solidstats/", "")
44
+
45
+ # Skip manifest files (application.css with require statements)
46
+ next if relative_path == "application.css"
47
+
48
+ "/* === #{relative_path} === */\n#{File.read(file_path)}"
49
+ end.compact.join("\n\n")
14
50
 
15
51
  @solidstats_styles_cache = content_tag(:style, combined_css.html_safe, type: "text/css")
16
52
  rescue => e
@@ -25,22 +61,99 @@ module Solidstats
25
61
 
26
62
  begin
27
63
  engine_root = Solidstats::Engine.root
28
- js_file = "#{engine_root}/app/assets/javascripts/solidstats/application.js"
64
+ js_files = Dir.glob("#{engine_root}/app/assets/javascripts/solidstats/**/*.js")
65
+
66
+ if js_files.any?
67
+ combined_js = js_files.map do |file_path|
68
+ relative_path = file_path.gsub("#{engine_root}/app/assets/javascripts/solidstats/", "")
69
+ javascript_content = File.read(file_path)
70
+ # Remove Sprockets directives since we're inlining
71
+ cleaned_js = javascript_content.gsub(/^\/\/=.*$/, "").strip
72
+
73
+ "/* === #{relative_path} === */\n#{cleaned_js}"
74
+ end.join("\n\n")
75
+
76
+ dashboard_js = <<~JS
77
+ /* === Dashboard Core Scripts === */
78
+ document.addEventListener('DOMContentLoaded', function() {
79
+ // Initialize Feather icons if available
80
+ if (typeof feather !== 'undefined') {
81
+ feather.replace();
82
+ }
83
+ #{' '}
84
+ // Auto-refresh functionality with animations
85
+ setInterval(function() {
86
+ document.querySelectorAll('.card').forEach(function(card) {
87
+ card.style.transform = 'scale(1.02)';
88
+ setTimeout(function() {
89
+ card.style.transform = '';
90
+ }, 200);
91
+ });
92
+ }, 30000);
29
93
 
30
- if File.exist?(js_file)
31
- javascript_content = File.read(js_file)
32
- # Remove Sprockets directives since we're inlining
33
- cleaned_js = javascript_content.gsub(/^\/\/=.*$/, "").strip
94
+ // Add loading states to forms
95
+ document.querySelectorAll('form[data-turbo-submits-with]').forEach(function(form) {
96
+ form.addEventListener('submit', function() {
97
+ var submitBtn = form.querySelector('button[type="submit"]');
98
+ if (submitBtn) {
99
+ submitBtn.classList.add('loading');
100
+ submitBtn.disabled = true;
101
+ }
102
+ });
103
+ });
104
+ });
105
+ JS
34
106
 
35
- @solidstats_scripts_cache = content_tag(:script, cleaned_js.html_safe, type: "text/javascript")
107
+ final_js = combined_js + "\n\n" + dashboard_js
36
108
  else
37
- Rails.logger.warn "Solidstats JavaScript file not found: #{js_file}"
38
- content_tag(:script, "console.log('Solidstats JavaScript not found');", type: "text/javascript")
109
+ # No JS files found, just include dashboard scripts
110
+ final_js = <<~JS
111
+ /* === Dashboard Core Scripts === */
112
+ document.addEventListener('DOMContentLoaded', function() {
113
+ // Initialize Feather icons if available
114
+ if (typeof feather !== 'undefined') {
115
+ feather.replace();
116
+ }
117
+ #{' '}
118
+ // Auto-refresh functionality with animations
119
+ setInterval(function() {
120
+ document.querySelectorAll('.card').forEach(function(card) {
121
+ card.style.transform = 'scale(1.02)';
122
+ setTimeout(function() {
123
+ card.style.transform = '';
124
+ }, 200);
125
+ });
126
+ }, 30000);
127
+
128
+ // Add loading states to forms
129
+ document.querySelectorAll('form[data-turbo-submits-with]').forEach(function(form) {
130
+ form.addEventListener('submit', function() {
131
+ var submitBtn = form.querySelector('button[type="submit"]');
132
+ if (submitBtn) {
133
+ submitBtn.classList.add('loading');
134
+ submitBtn.disabled = true;
135
+ }
136
+ });
137
+ });
138
+ });
139
+ JS
39
140
  end
141
+
142
+ @solidstats_scripts_cache = content_tag(:script, final_js.html_safe, type: "text/javascript")
40
143
  rescue => e
41
144
  Rails.logger.error "Solidstats JavaScript loading error: #{e.message}"
42
145
  content_tag(:script, "console.error('Solidstats JavaScript loading failed: #{e.message}');", type: "text/javascript")
43
146
  end
44
147
  end
148
+
149
+ # CDN dependencies helper
150
+ def solidstats_cdn_dependencies
151
+ [
152
+ tag(:meta, name: "viewport", content: "width=device-width,initial-scale=1"),
153
+ stylesheet_link_tag("https://cdn.jsdelivr.net/npm/daisyui@4.12.10/dist/full.min.css", media: "all"),
154
+ javascript_include_tag("https://cdn.tailwindcss.com"),
155
+ javascript_include_tag("https://unpkg.com/feather-icons")
156
+ ].join.html_safe
157
+ end
45
158
  end
46
159
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solidstats
4
+ # LoadLens Performance Helper
5
+ # Provides view helpers for displaying performance metrics
6
+ module PerformanceHelper
7
+ def load_lens_status_class(avg_response_time, error_rate)
8
+ return "text-error" if error_rate > 10
9
+ return "text-warning" if avg_response_time > 1000
10
+ "text-success"
11
+ end
12
+
13
+ def load_lens_status_icon(avg_response_time, error_rate)
14
+ return "alert-circle" if error_rate > 10
15
+ return "alert-triangle" if avg_response_time > 1000
16
+ "check-circle"
17
+ end
18
+
19
+ def format_response_time(time_ms)
20
+ return "0ms" if time_ms.nil? || time_ms == 0
21
+ "#{time_ms.round(1)}ms"
22
+ end
23
+
24
+ def format_percentage(value)
25
+ return "0%" if value.nil? || value == 0
26
+ "#{value.round(1)}%"
27
+ end
28
+
29
+ def request_status_badge_class(status)
30
+ case status.to_i
31
+ when 200..299
32
+ "badge-success"
33
+ when 300..399
34
+ "badge-info"
35
+ when 400..499
36
+ "badge-warning"
37
+ when 500..599
38
+ "badge-error"
39
+ else
40
+ "badge-neutral"
41
+ end
42
+ end
43
+
44
+ def response_time_color_class(time_ms)
45
+ return "text-base-content" if time_ms.nil? || time_ms == 0
46
+
47
+ case time_ms.to_f
48
+ when 0..100
49
+ "text-success"
50
+ when 100..500
51
+ "text-info"
52
+ when 500..1000
53
+ "text-warning"
54
+ else
55
+ "text-error"
56
+ end
57
+ end
58
+
59
+ def performance_trend_indicator(current, previous)
60
+ return "" if current.nil? || previous.nil? || previous == 0
61
+
62
+ percentage_change = ((current - previous) / previous.to_f) * 100
63
+
64
+ if percentage_change > 5
65
+ content_tag(:span, "↑", class: "text-error", title: "#{percentage_change.round(1)}% slower")
66
+ elsif percentage_change < -5
67
+ content_tag(:span, "↓", class: "text-success", title: "#{percentage_change.abs.round(1)}% faster")
68
+ else
69
+ content_tag(:span, "→", class: "text-info", title: "Similar performance")
70
+ end
71
+ end
72
+
73
+ def load_lens_metric_badge(value, threshold_warning, threshold_error, unit = "ms")
74
+ formatted_value = "#{value}#{unit}"
75
+
76
+ badge_class = if value > threshold_error
77
+ "badge-error"
78
+ elsif value > threshold_warning
79
+ "badge-warning"
80
+ else
81
+ "badge-success"
82
+ end
83
+
84
+ content_tag(:span, formatted_value, class: "badge #{badge_class}")
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solidstats
4
+ module ProductivityHelper
5
+ def todo_type_color(type)
6
+ case type.to_s.downcase
7
+ when "todo" then "primary"
8
+ when "fixme" then "error"
9
+ when "hack" then "warning"
10
+ when "note" then "info"
11
+ when "bug" then "error"
12
+ else "neutral"
13
+ end
14
+ end
15
+
16
+ def todo_type_icon(type)
17
+ case type.to_s.downcase
18
+ when "todo" then "📝"
19
+ when "fixme" then "🔧"
20
+ when "hack" then "⚠️"
21
+ when "note" then "📋"
22
+ when "bug" then "🐛"
23
+ else "📌"
24
+ end
25
+ end
26
+
27
+ def todo_priority_class(type)
28
+ case type.to_s.downcase
29
+ when "bug" then "priority-high"
30
+ when "fixme" then "priority-high"
31
+ when "hack" then "priority-medium"
32
+ when "todo" then "priority-low"
33
+ when "note" then "priority-low"
34
+ else "priority-low"
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Solidstats
4
+ # Service to collect and process bundler audit security vulnerability data
5
+ class BundlerAuditService
6
+ CACHE_FILE = Rails.root.join("solidstats", "bundler_audit.json")
7
+ CACHE_HOURS = 24 # Cache for 24 hours
8
+
9
+ class << self
10
+ # Get cached vulnerabilities or scan if cache is stale
11
+ # @return [Hash] The vulnerability data from JSON file
12
+ def fetch_vulnerabilities
13
+ if cache_stale?
14
+ scan_and_cache
15
+ end
16
+
17
+ load_cached_data
18
+ end
19
+
20
+ # Force a fresh scan and update cache
21
+ # @return [Hash] Fresh vulnerability data
22
+ def scan_and_cache
23
+ Rails.logger.info("Running bundler audit scan...")
24
+
25
+ begin
26
+ vulnerabilities_data = collect_bundler_audit_data
27
+ save_to_cache(vulnerabilities_data)
28
+ update_summary_json(vulnerabilities_data)
29
+ vulnerabilities_data
30
+ rescue => e
31
+ Rails.logger.error("Error running bundler audit: #{e.message}")
32
+ { "output" => { "results" => [], "error" => e.message } }
33
+ end
34
+ end
35
+
36
+ # Get summary for dashboard display
37
+ # @return [Hash] Summary information with status, count, and message
38
+ def summary
39
+ data = fetch_vulnerabilities
40
+ results = data.dig("output", "results") || []
41
+ vuln_count = results.count
42
+
43
+ {
44
+ count: vuln_count,
45
+ status: determine_status(vuln_count),
46
+ message: generate_message(vuln_count),
47
+ last_updated: data.dig("output", "created_at") || Time.current
48
+ }
49
+ end
50
+
51
+ private
52
+
53
+ # Check if cache file exists and is fresh
54
+ # @return [Boolean] true if cache is stale or missing
55
+ def cache_stale?
56
+ return true unless File.exist?(CACHE_FILE)
57
+
58
+ file_age = Time.current - File.mtime(CACHE_FILE)
59
+ file_age > CACHE_HOURS.hours
60
+ end
61
+
62
+ # Load data from cache file
63
+ # @return [Hash] Cached vulnerability data
64
+ def load_cached_data
65
+ return { "output" => { "results" => [] } } unless File.exist?(CACHE_FILE)
66
+
67
+ JSON.parse(File.read(CACHE_FILE))
68
+ rescue JSON::ParserError => e
69
+ Rails.logger.error("Error parsing bundler audit cache: #{e.message}")
70
+ { "output" => { "results" => [] } }
71
+ end
72
+
73
+ # Run bundler audit and collect vulnerability data
74
+ # @return [Hash] Fresh vulnerability data
75
+ def collect_bundler_audit_data
76
+ # Run bundler audit with JSON format
77
+ result = `bundle audit check --update --format json 2>&1`
78
+
79
+ # Check if bundler-audit is installed
80
+ if $?.exitstatus == 127 || result.include?("command not found")
81
+ raise "bundler-audit gem is not installed. Please run: gem install bundler-audit"
82
+ end
83
+
84
+ # Extract JSON part from output (bundler-audit may include extra text)
85
+ json_match = result.match(/(\{.*\})/m)
86
+ if json_match
87
+ parsed_data = JSON.parse(json_match[1])
88
+
89
+ # Add metadata
90
+ {
91
+ "output" => {
92
+ "version" => parsed_data.dig("version") || "unknown",
93
+ "created_at" => Time.current.strftime("%Y-%m-%d %H:%M:%S %z"),
94
+ "results" => parsed_data.dig("results") || []
95
+ }
96
+ }
97
+ else
98
+ # If no JSON found, create empty structure
99
+ {
100
+ "output" => {
101
+ "version" => "unknown",
102
+ "created_at" => Time.current.strftime("%Y-%m-%d %H:%M:%S %z"),
103
+ "results" => []
104
+ }
105
+ }
106
+ end
107
+ rescue JSON::ParserError => e
108
+ raise "Failed to parse bundler audit JSON output: #{e.message}"
109
+ end
110
+
111
+ # Save vulnerability data to cache file
112
+ # @param data [Hash] Vulnerability data to cache
113
+ def save_to_cache(data)
114
+ FileUtils.mkdir_p(File.dirname(CACHE_FILE))
115
+ File.write(CACHE_FILE, JSON.pretty_generate(data))
116
+ Rails.logger.info("Bundler audit data cached to #{CACHE_FILE}")
117
+ end
118
+
119
+ # Update the main summary.json file with security vulnerabilities
120
+ # @param data [Hash] Vulnerability data
121
+ def update_summary_json(data)
122
+ summary_file = Rails.root.join("solidstats", "summary.json")
123
+
124
+ # Ensure directory exists
125
+ FileUtils.mkdir_p(File.dirname(summary_file))
126
+
127
+ # Load existing summary or create new
128
+ existing_summary = if File.exist?(summary_file)
129
+ JSON.parse(File.read(summary_file))
130
+ else
131
+ {}
132
+ end
133
+
134
+ # Get vulnerability count and status
135
+ results = data.dig("output", "results") || []
136
+ vuln_count = results.count
137
+ status = determine_status(vuln_count)
138
+
139
+ # Calculate severity distribution
140
+ severity_counts = results.group_by { |r| r.dig("advisory", "criticality") || "unknown" }
141
+ .transform_values(&:count)
142
+
143
+ # Create badges for severity levels
144
+ badges = []
145
+ %w[critical high medium low].each do |severity|
146
+ count = severity_counts[severity] || 0
147
+ if count > 0
148
+ badges << {
149
+ "text" => "#{severity.capitalize}: #{count}",
150
+ "color" => severity_badge_color(severity)
151
+ }
152
+ end
153
+ end
154
+
155
+ # Update summary
156
+ existing_summary["Security Vulnerabilities"] = {
157
+ "icon" => "shield-alert",
158
+ "status" => status,
159
+ "value" => generate_message(vuln_count),
160
+ "last_updated" => data.dig("output", "created_at") || Time.current.iso8601,
161
+ "url" => "/solidstats/securities/bundler_audit",
162
+ "badges" => badges
163
+ }
164
+
165
+ # Save updated summary
166
+ File.write(summary_file, JSON.pretty_generate(existing_summary))
167
+ Rails.logger.info("Updated summary.json with security vulnerabilities")
168
+ end
169
+
170
+ # Determine status color based on vulnerability count
171
+ # @param count [Integer] Number of vulnerabilities
172
+ # @return [String] Status indicator
173
+ def determine_status(count)
174
+ case count
175
+ when 0 then "success"
176
+ when 1..2 then "warning"
177
+ else "danger"
178
+ end
179
+ end
180
+
181
+ # Generate status message based on vulnerability count
182
+ # @param count [Integer] Number of vulnerabilities
183
+ # @return [String] Human-readable status message
184
+ def generate_message(count)
185
+ case count
186
+ when 0 then "No vulnerabilities"
187
+ when 1 then "1 vulnerability"
188
+ else "#{count} vulnerabilities"
189
+ end
190
+ end
191
+
192
+ # Get badge color for severity level
193
+ # @param severity [String] Severity level
194
+ # @return [String] Badge color
195
+ def severity_badge_color(severity)
196
+ case severity.downcase
197
+ when "critical" then "red"
198
+ when "high" then "orange"
199
+ when "medium" then "yellow"
200
+ when "low" then "blue"
201
+ else "gray"
202
+ end
203
+ end
204
+ end
205
+ end
206
+ end