solidstats 1.1.0 → 3.0.0.beta.1

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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +27 -0
  4. data/Rakefile +3 -3
  5. data/app/assets/stylesheets/solidstats/dashboard.css +48 -0
  6. data/app/controllers/solidstats/dashboard_controller.rb +82 -60
  7. data/app/controllers/solidstats/logs_controller.rb +72 -0
  8. data/app/controllers/solidstats/performance_controller.rb +25 -0
  9. data/app/controllers/solidstats/productivity_controller.rb +39 -0
  10. data/app/controllers/solidstats/quality_controller.rb +152 -0
  11. data/app/controllers/solidstats/securities_controller.rb +30 -0
  12. data/app/helpers/solidstats/application_helper.rb +155 -0
  13. data/app/helpers/solidstats/performance_helper.rb +87 -0
  14. data/app/helpers/solidstats/productivity_helper.rb +38 -0
  15. data/app/services/solidstats/bundler_audit_service.rb +206 -0
  16. data/app/services/solidstats/coverage_compass_service.rb +335 -0
  17. data/app/services/solidstats/load_lens_service.rb +454 -0
  18. data/app/services/solidstats/log_size_monitor_service.rb +205 -74
  19. data/app/services/solidstats/my_todo_service.rb +242 -0
  20. data/app/services/solidstats/style_patrol_service.rb +319 -0
  21. data/app/views/layouts/solidstats/application.html.erb +9 -2
  22. data/app/views/layouts/solidstats/dashboard.html.erb +84 -0
  23. data/app/views/solidstats/dashboard/dashboard.html.erb +39 -0
  24. data/app/views/solidstats/logs/logs_size.html.erb +409 -0
  25. data/app/views/solidstats/performance/load_lens.html.erb +158 -0
  26. data/app/views/solidstats/productivity/_todo_list.html.erb +49 -0
  27. data/app/views/solidstats/productivity/my_todos.html.erb +84 -0
  28. data/app/views/solidstats/quality/coverage_compass.html.erb +420 -0
  29. data/app/views/solidstats/quality/style_patrol.html.erb +463 -0
  30. data/app/views/solidstats/securities/bundler_audit.html.erb +345 -0
  31. data/app/views/solidstats/shared/_dashboard_card.html.erb +160 -0
  32. data/app/views/solidstats/shared/_quick_actions.html.erb +26 -0
  33. data/config/routes.rb +32 -4
  34. data/lib/generators/solidstats/install/install_generator.rb +28 -2
  35. data/lib/generators/solidstats/install/templates/README +7 -0
  36. data/lib/solidstats/version.rb +1 -1
  37. data/lib/tasks/solidstats_performance.rake +84 -0
  38. metadata +43 -19
  39. data/app/services/solidstats/audit_service.rb +0 -56
  40. data/app/services/solidstats/data_collector_service.rb +0 -83
  41. data/app/services/solidstats/todo_service.rb +0 -114
  42. data/app/views/solidstats/dashboard/_log_monitor.html.erb +0 -759
  43. data/app/views/solidstats/dashboard/_todos.html.erb +0 -151
  44. data/app/views/solidstats/dashboard/audit/_additional_styles.css +0 -22
  45. data/app/views/solidstats/dashboard/audit/_audit_badge.html.erb +0 -5
  46. data/app/views/solidstats/dashboard/audit/_audit_details.html.erb +0 -495
  47. data/app/views/solidstats/dashboard/audit/_audit_summary.html.erb +0 -26
  48. data/app/views/solidstats/dashboard/audit/_no_vulnerabilities.html.erb +0 -3
  49. data/app/views/solidstats/dashboard/audit/_security_audit.html.erb +0 -14
  50. data/app/views/solidstats/dashboard/audit/_vulnerabilities_table.html.erb +0 -1120
  51. data/app/views/solidstats/dashboard/audit/_vulnerability_details.html.erb +0 -63
  52. data/app/views/solidstats/dashboard/index.html.erb +0 -1351
  53. data/lib/tasks/solidstats_tasks.rake +0 -4
@@ -1,4 +1,159 @@
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
+
34
+ # Inline CSS helper - reads all CSS files and returns them as a style tag
35
+ def solidstats_styles
36
+ return @solidstats_styles_cache if defined?(@solidstats_styles_cache)
37
+
38
+ begin
39
+ engine_root = Solidstats::Engine.root
40
+ css_files = Dir.glob("#{engine_root}/app/assets/stylesheets/solidstats/*.css")
41
+
42
+ combined_css = css_files.map do |file_path|
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")
50
+
51
+ @solidstats_styles_cache = content_tag(:style, combined_css.html_safe, type: "text/css")
52
+ rescue => e
53
+ Rails.logger.error "Solidstats CSS loading error: #{e.message}"
54
+ content_tag(:style, "/* Solidstats CSS loading failed: #{e.message} */", type: "text/css")
55
+ end
56
+ end
57
+
58
+ # Inline JavaScript helper - reads all JS files and returns them as a script tag
59
+ def solidstats_scripts
60
+ return @solidstats_scripts_cache if defined?(@solidstats_scripts_cache)
61
+
62
+ begin
63
+ engine_root = Solidstats::Engine.root
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);
93
+
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
106
+
107
+ final_js = combined_js + "\n\n" + dashboard_js
108
+ else
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
140
+ end
141
+
142
+ @solidstats_scripts_cache = content_tag(:script, final_js.html_safe, type: "text/javascript")
143
+ rescue => e
144
+ Rails.logger.error "Solidstats JavaScript loading error: #{e.message}"
145
+ content_tag(:script, "console.error('Solidstats JavaScript loading failed: #{e.message}');", type: "text/javascript")
146
+ end
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
3
158
  end
4
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