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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +27 -0
- data/Rakefile +3 -3
- data/app/assets/stylesheets/solidstats/dashboard.css +48 -0
- data/app/controllers/solidstats/dashboard_controller.rb +82 -60
- data/app/controllers/solidstats/logs_controller.rb +72 -0
- data/app/controllers/solidstats/performance_controller.rb +25 -0
- data/app/controllers/solidstats/productivity_controller.rb +39 -0
- data/app/controllers/solidstats/quality_controller.rb +152 -0
- data/app/controllers/solidstats/securities_controller.rb +30 -0
- data/app/helpers/solidstats/application_helper.rb +155 -0
- data/app/helpers/solidstats/performance_helper.rb +87 -0
- data/app/helpers/solidstats/productivity_helper.rb +38 -0
- data/app/services/solidstats/bundler_audit_service.rb +206 -0
- data/app/services/solidstats/coverage_compass_service.rb +335 -0
- data/app/services/solidstats/load_lens_service.rb +454 -0
- data/app/services/solidstats/log_size_monitor_service.rb +205 -74
- data/app/services/solidstats/my_todo_service.rb +242 -0
- data/app/services/solidstats/style_patrol_service.rb +319 -0
- data/app/views/layouts/solidstats/application.html.erb +9 -2
- data/app/views/layouts/solidstats/dashboard.html.erb +84 -0
- data/app/views/solidstats/dashboard/dashboard.html.erb +39 -0
- data/app/views/solidstats/logs/logs_size.html.erb +409 -0
- data/app/views/solidstats/performance/load_lens.html.erb +158 -0
- data/app/views/solidstats/productivity/_todo_list.html.erb +49 -0
- data/app/views/solidstats/productivity/my_todos.html.erb +84 -0
- data/app/views/solidstats/quality/coverage_compass.html.erb +420 -0
- data/app/views/solidstats/quality/style_patrol.html.erb +463 -0
- data/app/views/solidstats/securities/bundler_audit.html.erb +345 -0
- data/app/views/solidstats/shared/_dashboard_card.html.erb +160 -0
- data/app/views/solidstats/shared/_quick_actions.html.erb +26 -0
- data/config/routes.rb +32 -4
- data/lib/generators/solidstats/install/install_generator.rb +28 -2
- data/lib/generators/solidstats/install/templates/README +7 -0
- data/lib/solidstats/version.rb +1 -1
- data/lib/tasks/solidstats_performance.rake +84 -0
- metadata +43 -19
- data/app/services/solidstats/audit_service.rb +0 -56
- data/app/services/solidstats/data_collector_service.rb +0 -83
- data/app/services/solidstats/todo_service.rb +0 -114
- data/app/views/solidstats/dashboard/_log_monitor.html.erb +0 -759
- data/app/views/solidstats/dashboard/_todos.html.erb +0 -151
- data/app/views/solidstats/dashboard/audit/_additional_styles.css +0 -22
- data/app/views/solidstats/dashboard/audit/_audit_badge.html.erb +0 -5
- data/app/views/solidstats/dashboard/audit/_audit_details.html.erb +0 -495
- data/app/views/solidstats/dashboard/audit/_audit_summary.html.erb +0 -26
- data/app/views/solidstats/dashboard/audit/_no_vulnerabilities.html.erb +0 -3
- data/app/views/solidstats/dashboard/audit/_security_audit.html.erb +0 -14
- data/app/views/solidstats/dashboard/audit/_vulnerabilities_table.html.erb +0 -1120
- data/app/views/solidstats/dashboard/audit/_vulnerability_details.html.erb +0 -63
- data/app/views/solidstats/dashboard/index.html.erb +0 -1351
- data/lib/tasks/solidstats_tasks.rake +0 -4
@@ -1,94 +1,225 @@
|
|
1
1
|
module Solidstats
|
2
2
|
class LogSizeMonitorService
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
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
|
25
|
-
|
26
|
-
|
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
|
-
|
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
|
51
|
-
|
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
|
55
|
-
|
56
|
-
|
57
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
#
|
75
|
-
|
119
|
+
# Remove duplicates and filter readable files
|
120
|
+
log_paths.uniq.select { |path| File.readable?(path) }
|
76
121
|
end
|
77
|
-
|
78
|
-
def
|
79
|
-
|
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
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
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
|