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,94 +1,225 @@
1
1
  module Solidstats
2
2
  class LogSizeMonitorService
3
- WARNING_THRESHOLD = 25 # In megabytes
4
- DANGER_THRESHOLD = 50 # In megabytes
5
-
6
- def collect_data
7
- log_files = scan_log_directory
8
- total_size_bytes = log_files.sum { |file| file[:size_bytes] }
9
-
10
- # Create aggregated data
11
- {
12
- log_dir_path: log_directory,
13
- total_size_bytes: total_size_bytes,
14
- total_size_mb: bytes_to_mb(total_size_bytes),
15
- status: calculate_status(total_size_bytes),
16
- logs_count: log_files.size,
17
- log_files: log_files,
18
- created_at: Time.now.iso8601
19
- }
3
+ CACHE_FILE = "logs.json"
4
+ SUMMARY_FILE = "summary.json"
5
+
6
+ # Size thresholds in bytes
7
+ WARNING_SIZE = 500.megabytes
8
+ ERROR_SIZE = 1.gigabyte
9
+
10
+ def self.get_logs_data
11
+ cache_file_path = solidstats_cache_path(CACHE_FILE)
12
+
13
+ if File.exist?(cache_file_path) && cache_fresh?(cache_file_path)
14
+ JSON.parse(File.read(cache_file_path))
15
+ else
16
+ scan_and_cache
17
+ end
18
+ rescue JSON::ParserError, Errno::ENOENT
19
+ Rails.logger.error("Error reading logs cache, regenerating...")
20
+ scan_and_cache
20
21
  end
21
-
22
- def truncate_log(filename = nil)
22
+
23
+ def self.scan_and_cache
24
+ logs_data = scan_log_files
25
+
26
+ # Cache the logs data
27
+ cache_logs_data(logs_data)
28
+
29
+ # Update summary.json with log monitoring card
30
+ update_summary_json(logs_data)
31
+
32
+ logs_data
33
+ end
34
+
35
+ def self.truncate_log(filename)
36
+ return { status: "error", message: "Filename required" } if filename.blank?
37
+
38
+ log_file_path = find_log_file(filename)
39
+ return { status: "error", message: "Log file not found" } unless log_file_path
40
+
23
41
  begin
24
- if filename.present?
25
- # Truncate specific log file
26
- # Ensure filename has .log extension
27
- filename = "#{filename}.log" unless filename.end_with?('.log')
28
-
29
- file_path = File.join(log_directory, filename)
30
- if File.exist?(file_path)
31
- File.open(file_path, 'w') { |f| f.truncate(0) }
32
- { success: true, message: "Log file '#{filename}' truncated successfully" }
33
- else
34
- { success: false, message: "Log file '#{filename}' not found" }
35
- end
36
- else
37
- # Truncate all log files
38
- scan_log_directory.each do |log_file|
39
- File.open(log_file[:path], 'w') { |f| f.truncate(0) }
40
- end
41
- { success: true, message: "All log files truncated successfully" }
42
+ # Check if file is writable
43
+ unless File.writable?(log_file_path)
44
+ return { status: "error", message: "File is not writable" }
42
45
  end
46
+
47
+ # Get original size for reporting
48
+ original_size = File.size(log_file_path)
49
+
50
+ # Truncate the file
51
+ File.truncate(log_file_path, 0)
52
+
53
+ # Update cache after truncation
54
+ scan_and_cache
55
+
56
+ {
57
+ status: "success",
58
+ message: "Log file truncated successfully",
59
+ original_size: format_file_size(original_size),
60
+ filename: filename
61
+ }
43
62
  rescue => e
44
- { success: false, message: "Failed to truncate log file: #{e.message}" }
63
+ Rails.logger.error("Error truncating log file #{filename}: #{e.message}")
64
+ { status: "error", message: "Failed to truncate: #{e.message}" }
45
65
  end
46
66
  end
47
-
67
+
48
68
  private
49
-
50
- def log_directory
51
- Rails.root.join("log").to_s
69
+
70
+ def self.scan_log_files
71
+ log_files = discover_log_files
72
+ total_size = 0
73
+ files_data = []
74
+
75
+ log_files.each do |file_path|
76
+ next unless File.exist?(file_path) && File.readable?(file_path)
77
+
78
+ file_stat = File.stat(file_path)
79
+ file_size = file_stat.size
80
+ total_size += file_size
81
+
82
+ files_data << {
83
+ name: File.basename(file_path),
84
+ path: file_path,
85
+ size_bytes: file_size,
86
+ size_human: format_file_size(file_size),
87
+ last_modified: file_stat.mtime.iso8601,
88
+ status: determine_file_status(file_size),
89
+ can_truncate: File.writable?(file_path)
90
+ }
91
+ end
92
+
93
+ # Sort by size descending
94
+ files_data.sort_by! { |f| -f[:size_bytes] }
95
+
96
+ {
97
+ summary: {
98
+ total_files: files_data.length,
99
+ total_size: format_file_size(total_size),
100
+ total_size_bytes: total_size,
101
+ largest_file: files_data.first&.dig(:name) || "None",
102
+ largest_file_size: files_data.first&.dig(:size_human) || "0 B",
103
+ last_updated: Time.current.iso8601,
104
+ status: determine_overall_status(total_size, files_data)
105
+ },
106
+ files: files_data
107
+ }
52
108
  end
53
109
 
54
- def scan_log_directory
55
- log_files = []
56
-
57
- # Get all files in the log directory
58
- Dir.glob(File.join(log_directory, "*.log")).each do |file_path|
59
- if File.file?(file_path)
60
- size_bytes = File.size(file_path)
61
- filename = File.basename(file_path)
62
-
63
- log_files << {
64
- filename: filename,
65
- path: file_path,
66
- size_bytes: size_bytes,
67
- size_mb: bytes_to_mb(size_bytes),
68
- status: calculate_status(size_bytes),
69
- last_modified: File.mtime(file_path)
70
- }
71
- end
110
+ def self.discover_log_files
111
+ log_paths = []
112
+
113
+ # Rails log directory
114
+ if defined?(Rails) && Rails.root
115
+ rails_log_dir = Rails.root.join('log')
116
+ log_paths += Dir.glob(rails_log_dir.join('*.log')) if Dir.exist?(rails_log_dir)
72
117
  end
73
118
 
74
- # Sort by size (largest first)
75
- log_files.sort_by { |file| -file[:size_bytes] }
119
+ # Remove duplicates and filter readable files
120
+ log_paths.uniq.select { |path| File.readable?(path) }
76
121
  end
77
-
78
- def bytes_to_mb(bytes)
79
- (bytes.to_f / (1024 * 1024)).round(2)
122
+
123
+ def self.find_log_file(filename)
124
+ discovered_files = discover_log_files
125
+ discovered_files.find { |path| File.basename(path) == filename }
80
126
  end
81
-
82
- def calculate_status(size_bytes)
83
- size_mb = bytes_to_mb(size_bytes)
84
-
85
- if size_mb >= DANGER_THRESHOLD
86
- :danger
87
- elsif size_mb >= WARNING_THRESHOLD
88
- :warning
127
+
128
+ def self.determine_file_status(size_bytes)
129
+ if size_bytes >= ERROR_SIZE
130
+ 'error'
131
+ elsif size_bytes >= WARNING_SIZE
132
+ 'warning'
89
133
  else
90
- :ok
134
+ 'success'
91
135
  end
92
136
  end
137
+
138
+ def self.determine_overall_status(total_size, files_data)
139
+ error_files = files_data.count { |f| f[:status] == 'error' }
140
+ warning_files = files_data.count { |f| f[:status] == 'warning' }
141
+
142
+ if error_files > 0 || total_size >= ERROR_SIZE * 2
143
+ 'error'
144
+ elsif warning_files > 0 || total_size >= WARNING_SIZE * 3
145
+ 'warning'
146
+ else
147
+ 'success'
148
+ end
149
+ end
150
+
151
+ def self.format_file_size(size_bytes)
152
+ return "0 B" if size_bytes.zero?
153
+
154
+ units = ['B', 'KB', 'MB', 'GB', 'TB']
155
+ base = 1024.0
156
+ exp = (Math.log(size_bytes) / Math.log(base)).to_i
157
+ exp = [exp, units.length - 1].min
158
+
159
+ "%.1f %s" % [size_bytes / (base ** exp), units[exp]]
160
+ end
161
+
162
+ def self.cache_logs_data(logs_data)
163
+ cache_file_path = solidstats_cache_path(CACHE_FILE)
164
+ ensure_solidstats_directory
165
+
166
+ File.write(cache_file_path, JSON.pretty_generate(logs_data))
167
+ rescue => e
168
+ Rails.logger.error("Failed to cache logs data: #{e.message}")
169
+ end
170
+
171
+ def self.update_summary_json(logs_data)
172
+ summary_file_path = solidstats_cache_path(SUMMARY_FILE)
173
+
174
+ # Read existing summary or create new one
175
+ begin
176
+ existing_summary = File.exist?(summary_file_path) ? JSON.parse(File.read(summary_file_path)) : {}
177
+ rescue JSON::ParserError
178
+ existing_summary = {}
179
+ end
180
+
181
+ # Create badges based on status
182
+ badges = []
183
+ badges << { "text" => "#{logs_data[:summary][:total_files]} Files", "color" => "info" }
184
+
185
+ case logs_data[:summary][:status]
186
+ when 'error'
187
+ badges << { "text" => "Large Size", "color" => "error" }
188
+ when 'warning'
189
+ badges << { "text" => "Growing", "color" => "warning" }
190
+ else
191
+ badges << { "text" => "Healthy", "color" => "success" }
192
+ end
193
+
194
+ # Update the log monitoring entry
195
+ existing_summary["Log Files"] = {
196
+ "icon" => "file-text",
197
+ "status" => logs_data[:summary][:status],
198
+ "value" => logs_data[:summary][:total_size],
199
+ "last_updated" => logs_data[:summary][:last_updated],
200
+ "url" => "/solidstats/logs/size",
201
+ "badges" => badges
202
+ }
203
+
204
+ # Write updated summary
205
+ File.write(summary_file_path, JSON.pretty_generate(existing_summary))
206
+ rescue => e
207
+ Rails.logger.error("Failed to update summary.json: #{e.message}")
208
+ end
209
+
210
+ def self.cache_fresh?(cache_file_path, max_age = 30.minutes)
211
+ File.mtime(cache_file_path) > max_age.ago
212
+ rescue
213
+ false
214
+ end
215
+
216
+ def self.solidstats_cache_path(filename)
217
+ Rails.root.join( 'solidstats', filename)
218
+ end
219
+
220
+ def self.ensure_solidstats_directory
221
+ dir_path = File.dirname(solidstats_cache_path('dummy'))
222
+ FileUtils.mkdir_p(dir_path) unless Dir.exist?(dir_path)
223
+ end
93
224
  end
94
225
  end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'find'
4
+ require 'json'
5
+
6
+ module Solidstats
7
+ # Enhanced TODO service for comprehensive project scanning
8
+ class MyTodoService
9
+ CACHE_FILE = Rails.root.join('solidstats', 'todos.json')
10
+ CACHE_DURATION = 24.hours
11
+
12
+ TODO_PATTERNS = [
13
+ /TODO:?\s*(.+)/i,
14
+ /FIXME:?\s*(.+)/i,
15
+ /HACK:?\s*(.+)/i,
16
+ /NOTE:?\s*(.+)/i,
17
+ /BUG:?\s*(.+)/i
18
+ ].freeze
19
+
20
+ SCAN_EXTENSIONS = %w[.rb .js .html .erb .yml .yaml .json .css .scss .vue .jsx .tsx .ts].freeze
21
+ EXCLUDE_DIRS = %w[node_modules vendor tmp log public/assets .git coverage pkg app/assets/builds solidstats].freeze
22
+
23
+ def self.collect_todos(force_refresh: false)
24
+ return cached_todos unless force_refresh || cache_expired?
25
+
26
+ todos = scan_project_files
27
+ cache_todos(todos)
28
+ update_summary_json(todos)
29
+ todos
30
+ end
31
+
32
+ def self.get_summary
33
+ todos = collect_todos
34
+ {
35
+ total_count: todos.length,
36
+ by_type: todos.group_by { |t| t[:type] }.transform_values(&:count),
37
+ by_file: todos.group_by { |t| t[:file] }.transform_values(&:count),
38
+ top_files: todos.group_by { |t| t[:file] }
39
+ .transform_values(&:count)
40
+ .sort_by { |_, count| -count }
41
+ .first(5)
42
+ .to_h,
43
+ status: determine_status(todos),
44
+ last_updated: Time.current.iso8601
45
+ }
46
+ end
47
+
48
+ private
49
+
50
+ def self.scan_project_files
51
+ todos = []
52
+ exclude_patterns = load_gitignore_patterns
53
+
54
+ Find.find(Rails.root) do |path|
55
+ if File.directory?(path)
56
+ Find.prune if should_exclude_directory?(path, exclude_patterns)
57
+ next
58
+ end
59
+
60
+ next unless should_scan_file?(path)
61
+ next if excluded_by_gitignore?(path, exclude_patterns)
62
+
63
+ scan_file_for_todos(path, todos)
64
+ end
65
+
66
+ todos.sort_by { |todo| [todo[:file], todo[:line_number]] }
67
+ end
68
+
69
+ def self.scan_file_for_todos(file_path, todos)
70
+ relative_path = file_path.sub("#{Rails.root}/", '')
71
+
72
+ File.readlines(file_path, chomp: true).each_with_index do |line, index|
73
+ TODO_PATTERNS.each do |pattern|
74
+ if match = line.match(pattern)
75
+ todos << {
76
+ file: relative_path,
77
+ line_number: index + 1,
78
+ content: match[1]&.strip || line.strip,
79
+ type: extract_todo_type(line),
80
+ full_line: line.strip,
81
+ created_at: Time.current
82
+ }
83
+ break # Only match first pattern per line
84
+ end
85
+ end
86
+ end
87
+ rescue => e
88
+ Rails.logger.warn "Error scanning file #{file_path}: #{e.message}"
89
+ end
90
+
91
+ def self.extract_todo_type(line)
92
+ case line.upcase
93
+ when /TODO/ then 'todo'
94
+ when /FIXME/ then 'fixme'
95
+ when /HACK/ then 'hack'
96
+ when /NOTE/ then 'note'
97
+ when /BUG/ then 'bug'
98
+ else 'todo'
99
+ end
100
+ end
101
+
102
+ def self.should_exclude_directory?(path, exclude_patterns)
103
+ relative_path = path.sub("#{Rails.root}/", '')
104
+
105
+ # Check standard exclude directories
106
+ EXCLUDE_DIRS.each do |dir|
107
+ # Check if this directory or any parent directory matches
108
+ path_parts = relative_path.split('/')
109
+ return true if path_parts.include?(dir)
110
+ return true if relative_path == dir
111
+ return true if relative_path.start_with?("#{dir}/")
112
+ end
113
+
114
+ # Check gitignore patterns
115
+ return true if exclude_patterns.any? { |pattern| File.fnmatch(pattern, relative_path, File::FNM_PATHNAME) }
116
+
117
+ false
118
+ end
119
+
120
+ def self.should_scan_file?(path)
121
+ SCAN_EXTENSIONS.any? { |ext| path.end_with?(ext) }
122
+ end
123
+
124
+ def self.excluded_by_gitignore?(file_path, exclude_patterns)
125
+ relative_path = file_path.sub("#{Rails.root}/", '')
126
+ exclude_patterns.any? { |pattern| File.fnmatch(pattern, relative_path, File::FNM_PATHNAME) }
127
+ end
128
+
129
+ def self.load_gitignore_patterns
130
+ gitignore_path = Rails.root.join('.gitignore')
131
+ return [] unless File.exist?(gitignore_path)
132
+
133
+ patterns = []
134
+ File.readlines(gitignore_path, chomp: true).each do |line|
135
+ line = line.strip
136
+ next if line.empty? || line.start_with?('#')
137
+
138
+ # Convert gitignore patterns to fnmatch patterns
139
+ pattern = line.gsub(/\*\*/, '*')
140
+ pattern = pattern.chomp('/')
141
+ patterns << pattern
142
+ patterns << "#{pattern}/*" if !pattern.include?('*')
143
+ end
144
+
145
+ patterns
146
+ end
147
+
148
+ def self.cached_todos
149
+ return [] unless File.exist?(CACHE_FILE)
150
+
151
+ JSON.parse(File.read(CACHE_FILE), symbolize_names: true)
152
+ rescue JSON::ParserError
153
+ []
154
+ end
155
+
156
+ def self.cache_todos(todos)
157
+ FileUtils.mkdir_p(File.dirname(CACHE_FILE))
158
+ File.write(CACHE_FILE, todos.to_json)
159
+ end
160
+
161
+ def self.cache_expired?
162
+ return true unless File.exist?(CACHE_FILE)
163
+
164
+ File.mtime(CACHE_FILE) < CACHE_DURATION.ago
165
+ end
166
+
167
+ def self.update_summary_json(todos)
168
+ summary_file_path = Rails.root.join('solidstats', 'summary.json')
169
+
170
+ # Ensure directory exists
171
+ FileUtils.mkdir_p(File.dirname(summary_file_path))
172
+
173
+ # Read existing summary or create new one
174
+ begin
175
+ existing_summary = File.exist?(summary_file_path) ? JSON.parse(File.read(summary_file_path)) : {}
176
+ rescue JSON::ParserError
177
+ existing_summary = {}
178
+ end
179
+
180
+ # Calculate TODO statistics
181
+ todo_count = todos.length
182
+ status = determine_status(todos)
183
+ type_counts = todos.group_by { |t| t[:type] }.transform_values(&:count)
184
+
185
+ # Create badges based on TODO types and counts
186
+ badges = []
187
+ badges << { "text" => "#{todo_count} Items", "color" => "info" }
188
+
189
+ %w[todo fixme hack note bug].each do |type|
190
+ count = type_counts[type] || 0
191
+ if count > 0
192
+ color = case type
193
+ when 'fixme' then 'warning'
194
+ when 'hack', 'bug' then 'error'
195
+ else 'info'
196
+ end
197
+ badges << { "text" => "#{type.upcase}: #{count}", "color" => color }
198
+ end
199
+ end
200
+
201
+ # Update the TODO items entry
202
+ existing_summary["TODO Items"] = {
203
+ "icon" => "list-todo",
204
+ "status" => status,
205
+ "value" => generate_message(todo_count),
206
+ "last_updated" => Time.current.iso8601,
207
+ "url" => "/solidstats/productivity/my_todos",
208
+ "badges" => badges
209
+ }
210
+
211
+ # Write updated summary
212
+ File.write(summary_file_path, JSON.pretty_generate(existing_summary))
213
+ Rails.logger.info("Updated summary.json with TODO items")
214
+ rescue => e
215
+ Rails.logger.error("Failed to update summary.json: #{e.message}")
216
+ end
217
+
218
+ def self.determine_status(todos)
219
+ return "success" if todos.empty?
220
+
221
+ fixme_count = todos.count { |t| t[:type] == 'fixme' }
222
+ hack_count = todos.count { |t| t[:type] == 'hack' }
223
+ bug_count = todos.count { |t| t[:type] == 'bug' }
224
+
225
+ if bug_count > 0 || hack_count > 0
226
+ "error"
227
+ elsif fixme_count > 0 || todos.length > 20
228
+ "warning"
229
+ else
230
+ "success"
231
+ end
232
+ end
233
+
234
+ def self.generate_message(count)
235
+ case count
236
+ when 0 then "No items found"
237
+ when 1 then "1 item found"
238
+ else "#{count} items found"
239
+ end
240
+ end
241
+ end
242
+ end