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
@@ -0,0 +1,319 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'json'
5
+
6
+ module Solidstats
7
+ class StylePatrolService
8
+ CACHE_KEY = "style_patrol_data"
9
+ CACHE_DURATION = 6.hours
10
+ STANDARD_JSON_FILE = Rails.root.join('solidstats', 'standard.json')
11
+
12
+ def self.collect_data(force_refresh: false)
13
+ return cached_data unless force_refresh || cache_expired?
14
+
15
+ analysis_data = analyze_code_quality
16
+ cache_data(analysis_data)
17
+ save_to_standard_json(analysis_data)
18
+ update_summary_json(analysis_data)
19
+ analysis_data
20
+ end
21
+
22
+ def self.get_summary
23
+ data = collect_data
24
+ {
25
+ status: data[:status],
26
+ total_files: data.dig(:summary, :total_files) || 0,
27
+ total_offenses: data.dig(:summary, :total_offenses) || 0,
28
+ correctable_count: data.dig(:summary, :correctable_count) || 0,
29
+ last_analyzed: data[:analyzed_at],
30
+ health_score: calculate_health_score(data)
31
+ }
32
+ end
33
+
34
+ def self.refresh_cache
35
+ Rails.cache.delete(CACHE_KEY)
36
+ collect_data(force_refresh: true)
37
+ end
38
+
39
+ private
40
+
41
+ def self.analyze_code_quality
42
+ result = `standardrb --format json 2>&1`
43
+
44
+ # Extract JSON from output (may contain debug info or other text)
45
+ json_content = extract_json_from_output(result)
46
+
47
+ if json_content.nil?
48
+ # No JSON found - could be clean result or error
49
+ if $?.success?
50
+ # Clean result - no issues found
51
+ {
52
+ status: "clean",
53
+ issues: [],
54
+ summary: {
55
+ total_files: 0,
56
+ total_offenses: 0,
57
+ correctable_count: 0,
58
+ target_file_count: 0,
59
+ inspected_file_count: 0
60
+ },
61
+ raw_data: nil,
62
+ analyzed_at: Time.current.iso8601
63
+ }
64
+ else
65
+ {
66
+ status: "error",
67
+ error_message: result,
68
+ analyzed_at: Time.current.iso8601
69
+ }
70
+ end
71
+ else
72
+ begin
73
+ json_output = JSON.parse(json_content)
74
+ process_standard_output(json_output)
75
+ rescue JSON::ParserError => e
76
+ {
77
+ status: "error",
78
+ error_message: "Failed to parse JSON: #{e.message}\nRaw output: #{result}",
79
+ analyzed_at: Time.current.iso8601
80
+ }
81
+ end
82
+ end
83
+ rescue StandardError => e
84
+ {
85
+ status: "error",
86
+ error_message: e.message,
87
+ analyzed_at: Time.current.iso8601
88
+ }
89
+ end
90
+
91
+ def self.process_standard_output(json_data)
92
+ issues = []
93
+
94
+ # Handle StandardRB JSON format
95
+ files = json_data["files"] || []
96
+
97
+ files.each do |file_data|
98
+ file_data["offenses"]&.each do |offense|
99
+ issues << {
100
+ file: file_data["path"],
101
+ line: offense["location"]["line"],
102
+ column: offense["location"]["column"],
103
+ severity: offense["severity"],
104
+ message: offense["message"],
105
+ cop_name: offense["cop_name"],
106
+ correctable: offense["correctable"] || false
107
+ }
108
+ end
109
+ end
110
+
111
+ # Get summary from StandardRB output
112
+ summary_data = json_data["summary"] || {}
113
+
114
+ {
115
+ status: issues.any? ? "issues_found" : "clean",
116
+ issues: issues,
117
+ summary: {
118
+ total_files: files.count,
119
+ total_offenses: summary_data["offense_count"] || issues.count,
120
+ correctable_count: issues.count { |i| i[:correctable] },
121
+ target_file_count: summary_data["target_file_count"] || 0,
122
+ inspected_file_count: summary_data["inspected_file_count"] || 0
123
+ },
124
+ raw_data: json_data,
125
+ analyzed_at: Time.current.iso8601
126
+ }
127
+ end
128
+
129
+ def self.cached_data
130
+ Rails.cache.fetch(CACHE_KEY, expires_in: CACHE_DURATION) do
131
+ analyze_code_quality
132
+ end
133
+ end
134
+
135
+ def self.cache_data(data)
136
+ Rails.cache.write(CACHE_KEY, data, expires_in: CACHE_DURATION)
137
+ data
138
+ end
139
+
140
+ def self.cache_expired?
141
+ cached_entry = Rails.cache.read(CACHE_KEY)
142
+ return true if cached_entry.nil?
143
+
144
+ cached_time = Time.parse(cached_entry[:analyzed_at])
145
+ Time.current > cached_time + CACHE_DURATION
146
+ rescue
147
+ true
148
+ end
149
+
150
+ def self.calculate_health_score(data)
151
+ return 100 if data[:status] == "clean"
152
+ return 0 if data[:status] == "error"
153
+
154
+ total_offenses = data.dig(:summary, :total_offenses) || 0
155
+ total_files = data.dig(:summary, :total_files) || 1
156
+
157
+ # Health score: 100 - (offenses per file * 10), minimum 0
158
+ score = 100 - ((total_offenses.to_f / total_files) * 10).round
159
+ [score, 0].max
160
+ end
161
+
162
+ def self.update_summary_json(data)
163
+ summary_file_path = Rails.root.join('solidstats', 'summary.json')
164
+
165
+ # Ensure directory exists
166
+ FileUtils.mkdir_p(File.dirname(summary_file_path))
167
+
168
+ # Read existing summary or create new one
169
+ begin
170
+ existing_summary = File.exist?(summary_file_path) ? JSON.parse(File.read(summary_file_path)) : {}
171
+ rescue JSON::ParserError
172
+ existing_summary = {}
173
+ end
174
+
175
+ # Calculate style patrol statistics
176
+ total_offenses = data.dig(:summary, :total_offenses) || 0
177
+ correctable_count = data.dig(:summary, :correctable_count) || 0
178
+ status = determine_dashboard_status(data[:status], total_offenses)
179
+ health_score = calculate_health_score(data)
180
+
181
+ # Create badges based on code quality metrics
182
+ badges = []
183
+ badges << { "text" => "Health: #{health_score}%", "color" => health_badge_color(health_score) }
184
+
185
+ if total_offenses > 0
186
+ badges << { "text" => "#{total_offenses} Issues", "color" => "warning" }
187
+
188
+ if correctable_count > 0
189
+ badges << { "text" => "#{correctable_count} Auto-fixable", "color" => "info" }
190
+ end
191
+ end
192
+
193
+ # Update the StylePatrol entry
194
+ existing_summary["StylePatrol"] = {
195
+ "icon" => "code",
196
+ "status" => status,
197
+ "value" => generate_dashboard_message(data[:status], total_offenses, health_score),
198
+ "last_updated" => data[:analyzed_at],
199
+ "url" => "/solidstats/quality/style_patrol",
200
+ "badges" => badges
201
+ }
202
+
203
+ # Write updated summary
204
+ File.write(summary_file_path, JSON.pretty_generate(existing_summary))
205
+ Rails.logger.info("Updated summary.json with StylePatrol data")
206
+ rescue => e
207
+ Rails.logger.error("Failed to update summary.json: #{e.message}")
208
+ end
209
+
210
+ def self.determine_dashboard_status(analysis_status, offense_count)
211
+ case analysis_status
212
+ when "clean" then "success"
213
+ when "error" then "danger"
214
+ when "issues_found"
215
+ case offense_count
216
+ when 0..5 then "info"
217
+ when 6..15 then "warning"
218
+ else "danger"
219
+ end
220
+ else "warning"
221
+ end
222
+ end
223
+
224
+ def self.health_badge_color(score)
225
+ case score
226
+ when 90..100 then "success"
227
+ when 70..89 then "info"
228
+ when 50..69 then "warning"
229
+ else "error"
230
+ end
231
+ end
232
+
233
+ def self.generate_dashboard_message(status, offense_count, health_score)
234
+ case status
235
+ when "clean"
236
+ "Code is clean! ✨"
237
+ when "error"
238
+ "Analysis failed ❌"
239
+ when "issues_found"
240
+ "#{offense_count} issues found"
241
+ else
242
+ "Status unknown"
243
+ end
244
+ end
245
+
246
+ def self.save_to_standard_json(data)
247
+ # Ensure directory exists
248
+ FileUtils.mkdir_p(File.dirname(STANDARD_JSON_FILE))
249
+
250
+ # Prepare data for standard.json file
251
+ standard_data = {
252
+ last_updated: data[:analyzed_at],
253
+ status: data[:status],
254
+ metadata: data.dig(:raw_data, "metadata") || {},
255
+ files: data.dig(:raw_data, "files") || [],
256
+ summary: data.dig(:raw_data, "summary") || {},
257
+ processed_summary: data[:summary] || {},
258
+ issues_count: data[:issues]&.count || 0
259
+ }
260
+
261
+ # Write to standard.json file
262
+ File.write(STANDARD_JSON_FILE, JSON.pretty_generate(standard_data))
263
+ Rails.logger.info("Saved StandardRB data to #{STANDARD_JSON_FILE}")
264
+ rescue => e
265
+ Rails.logger.error("Failed to save standard.json: #{e.message}")
266
+ end
267
+
268
+ def self.extract_json_from_output(output)
269
+ # Look for JSON content - should start with { and end with }
270
+ # Handle cases where debug info or other text appears before/after JSON
271
+
272
+ return nil if output.nil? || output.strip.empty?
273
+
274
+ # Split by lines and look for the line that starts with JSON
275
+ lines = output.split("\n")
276
+ json_lines = []
277
+ json_started = false
278
+ brace_count = 0
279
+
280
+ lines.each do |line|
281
+ # Skip debug lines that mention "Subprocess Debugger", "ruby-debug-ide", etc.
282
+ next if line.include?("Subprocess Debugger") ||
283
+ line.include?("ruby-debug-ide") ||
284
+ line.include?("debase") ||
285
+ line.include?("listens on")
286
+
287
+ # Look for the start of JSON (line starting with {)
288
+ if !json_started && line.strip.start_with?('{')
289
+ json_started = true
290
+ end
291
+
292
+ if json_started
293
+ json_lines << line
294
+
295
+ # Count braces to know when JSON ends
296
+ brace_count += line.count('{') - line.count('}')
297
+
298
+ # If we've closed all braces, we've reached the end of JSON
299
+ break if brace_count == 0
300
+ end
301
+ end
302
+
303
+ return nil if json_lines.empty?
304
+
305
+ json_content = json_lines.join("\n")
306
+
307
+ # Validate that it looks like StandardRB JSON by checking for expected structure
308
+ return nil unless json_content.include?('"metadata"') ||
309
+ json_content.include?('"files"') ||
310
+ json_content.include?('"summary"')
311
+
312
+ json_content
313
+ rescue => e
314
+ Rails.logger.error("Error extracting JSON from StandardRB output: #{e.message}")
315
+ Rails.logger.error("Raw output was: #{output}")
316
+ nil
317
+ end
318
+ end
319
+ end
@@ -4,14 +4,21 @@
4
4
  <title>Solidstats</title>
5
5
  <%= csrf_meta_tags %>
6
6
  <%= csp_meta_tag %>
7
+
8
+ <!-- Solidstats CDN Dependencies -->
9
+ <%= solidstats_cdn_dependencies %>
7
10
 
8
11
  <%= yield :head %>
9
12
 
10
- <%= stylesheet_link_tag "solidstats/application", media: "all" %>
13
+ <!-- Inline Solidstats Styles -->
14
+ <%= solidstats_styles %>
11
15
  </head>
12
- <body>
16
+ <body data-theme="light" class="min-h-screen bg-base-200">
13
17
 
14
18
  <%= yield %>
15
19
 
20
+ <!-- Inline Solidstats Scripts -->
21
+ <%= solidstats_scripts %>
22
+
16
23
  </body>
17
24
  </html>
@@ -0,0 +1,84 @@
1
+ <!DOCTYPE html>
2
+ <html data-theme="dark" class="transition-colors duration-300">
3
+ <head>
4
+ <title>Solidstats Dashboard</title>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <%= csrf_meta_tags %>
8
+ <%= csp_meta_tag %>
9
+
10
+ <%= yield :head %>
11
+
12
+ <!-- Solidstats CDN Dependencies -->
13
+ <%= solidstats_cdn_dependencies %>
14
+
15
+ <!-- Inline Solidstats Styles -->
16
+ <%= solidstats_styles %>
17
+ </head>
18
+ <body class="min-h-screen bg-base-100 transition-all duration-300">
19
+
20
+ <!-- DaisyUI Navbar -->
21
+ <div class="navbar bg-base-100 shadow-lg border-b border-base-300">
22
+ <div class="navbar-start">
23
+ <div class="flex items-center space-x-4">
24
+ <div class="avatar">
25
+ <div class="w-8 rounded-lg bg-gradient-to-r from-primary to-secondary flex items-center justify-center">
26
+ <i data-feather="activity" class="w-5 h-5 text-white"></i>
27
+ </div>
28
+ </div>
29
+ <%= link_to solidstats.solidstats_dashboard_path, class: "text-xl font-bold gradient-text hover:opacity-80 transition-opacity" do %>
30
+ Solidstats
31
+ <% end %>
32
+ </div>
33
+ </div>
34
+
35
+ <div class="navbar-end">
36
+ <!-- Simple Theme Toggle -->
37
+ <button class="btn btn-ghost btn-circle" onclick="toggleTheme()">
38
+ <i data-feather="sun" class="w-5 h-5 hidden dark:inline"></i>
39
+ <i data-feather="moon" class="w-5 h-5 dark:hidden"></i>
40
+ </button>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Main Content -->
45
+ <main class="container mx-auto px-4 py-8">
46
+ <%= yield %>
47
+ </main>
48
+
49
+ <!-- Simple Theme Toggle Script -->
50
+ <script>
51
+ // Initialize Feather icons
52
+ feather.replace();
53
+
54
+ // Simple theme toggle between dark and light
55
+ function toggleTheme() {
56
+ const currentTheme = localStorage.getItem('theme') || 'dark';
57
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
58
+ setTheme(newTheme);
59
+ }
60
+
61
+ function setTheme(theme) {
62
+ document.documentElement.setAttribute('data-theme', theme);
63
+ localStorage.setItem('theme', theme);
64
+
65
+ // Update dark class for custom CSS compatibility
66
+ if (theme === 'dark') {
67
+ document.documentElement.classList.add('dark');
68
+ } else {
69
+ document.documentElement.classList.remove('dark');
70
+ }
71
+
72
+ // Re-initialize icons after theme change
73
+ setTimeout(() => feather.replace(), 100);
74
+ }
75
+
76
+ // Check for saved theme preference or default to 'dark'
77
+ const currentTheme = localStorage.getItem('theme') || 'dark';
78
+ setTheme(currentTheme);
79
+ </script>
80
+
81
+ <!-- Inline Solidstats Scripts -->
82
+ <%= solidstats_scripts %>
83
+ </body>
84
+ </html>
@@ -0,0 +1,39 @@
1
+ <%# Dashboard Index View %>
2
+
3
+ <div class="dashboard-enter">
4
+ <!-- Welcome Section -->
5
+ <div class="hero mb-8">
6
+ <div class="hero-content text-center">
7
+ <div class="max-w-2xl">
8
+ <h2 class="text-3xl font-bold mb-2">
9
+ Welcome to Your Dashboard
10
+ </h2>
11
+ <p class="text-base-content/70">
12
+ Monitor your application's performance, security, and health metrics in real-time
13
+ </p>
14
+ </div>
15
+ </div>
16
+ </div>
17
+
18
+ <!-- Stats Grid -->
19
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mb-8">
20
+ <% @dashboard_cards.each do |card| %>
21
+ <%= render 'solidstats/shared/dashboard_card', card %>
22
+ <% end %>
23
+ </div>
24
+
25
+ <!-- Quick Actions -->
26
+ <%= render 'solidstats/shared/quick_actions' %>
27
+ </div>
28
+
29
+ <script>
30
+ feather.replace();
31
+
32
+ // Auto-refresh functionality with DaisyUI animations
33
+ setInterval(() => {
34
+ document.querySelectorAll('.card').forEach(card => {
35
+ card.style.transform = 'scale(1.02)';
36
+ setTimeout(() => card.style.transform = '', 200);
37
+ });
38
+ }, 30000);
39
+ </script>