solidstats 2.0.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 +12 -63
- data/README.md +27 -33
- data/Rakefile +3 -3
- data/app/assets/stylesheets/solidstats/application.css +1 -6
- data/app/assets/stylesheets/solidstats/dashboard.css +48 -0
- data/app/controllers/solidstats/dashboard_controller.rb +81 -62
- 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 +124 -11
- 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 +207 -76
- 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 +8 -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 -7
- data/lib/generators/solidstats/install/install_generator.rb +28 -2
- data/lib/generators/solidstats/install/templates/README +7 -0
- data/lib/solidstats/engine.rb +9 -114
- data/lib/solidstats/version.rb +1 -1
- data/lib/solidstats.rb +2 -299
- data/lib/tasks/solidstats_install.rake +2 -122
- data/lib/tasks/solidstats_performance.rake +84 -0
- metadata +32 -103
- data/app/assets/javascripts/solidstats/application.js +0 -257
- data/app/assets/javascripts/solidstats/dashboard.js +0 -225
- data/app/assets/javascripts/solidstats/gem_metadata.js +0 -554
- data/app/assets/stylesheets/solidstats/components/action_button.css +0 -99
- data/app/assets/stylesheets/solidstats/components/dashboard.css +0 -151
- data/app/assets/stylesheets/solidstats/components/dashboard_header.css +0 -93
- data/app/assets/stylesheets/solidstats/components/dashboard_layout.css +0 -97
- data/app/assets/stylesheets/solidstats/components/gem_metadata.css +0 -1403
- data/app/assets/stylesheets/solidstats/components/navigation.css +0 -80
- data/app/assets/stylesheets/solidstats/components/quick_navigation.css +0 -54
- data/app/assets/stylesheets/solidstats/components/security.css +0 -332
- data/app/assets/stylesheets/solidstats/components/status_badge.css +0 -58
- data/app/assets/stylesheets/solidstats/components/summary_card.css +0 -66
- data/app/assets/stylesheets/solidstats/components/tab_navigation.css +0 -95
- data/app/components/solidstats/base_component.rb +0 -88
- data/app/components/solidstats/code_quality/code_quality_section_component.html.erb +0 -0
- data/app/components/solidstats/code_quality/code_quality_section_component.rb +0 -0
- data/app/components/solidstats/code_quality/section_component.html.erb +0 -45
- data/app/components/solidstats/code_quality/section_component.rb +0 -34
- data/app/components/solidstats/dashboard_header_component.html.erb +0 -39
- data/app/components/solidstats/dashboard_header_component.rb +0 -33
- data/app/components/solidstats/previews/action_button_component_preview/button_vs_link.html.erb +0 -6
- data/app/components/solidstats/previews/action_button_component_preview/sizes.html.erb +0 -6
- data/app/components/solidstats/previews/action_button_component_preview/variants.html.erb +0 -6
- data/app/components/solidstats/previews/action_button_component_preview/with_icons.html.erb +0 -6
- data/app/components/solidstats/previews/action_button_component_preview.rb +0 -64
- data/app/components/solidstats/previews/navigation_component_preview.rb +0 -74
- data/app/components/solidstats/previews/stats_overview_component_preview.rb +0 -100
- data/app/components/solidstats/previews/status_badge_component_preview/sizes.html.erb +0 -6
- data/app/components/solidstats/previews/status_badge_component_preview/statuses.html.erb +0 -6
- data/app/components/solidstats/previews/status_badge_component_preview/with_icons.html.erb +0 -6
- data/app/components/solidstats/previews/status_badge_component_preview.rb +0 -49
- data/app/components/solidstats/previews/summary_card_component_preview/clickable.html.erb +0 -9
- data/app/components/solidstats/previews/summary_card_component_preview/dashboard_layout.html.erb +0 -9
- data/app/components/solidstats/previews/summary_card_component_preview/statuses.html.erb +0 -6
- data/app/components/solidstats/previews/summary_card_component_preview/value_formats.html.erb +0 -6
- data/app/components/solidstats/previews/summary_card_component_preview.rb +0 -67
- data/app/components/solidstats/quick_navigation_component.html.erb +0 -8
- data/app/components/solidstats/quick_navigation_component.rb +0 -21
- data/app/components/solidstats/security/gem_impact_analysis_component.html.erb +0 -44
- data/app/components/solidstats/security/gem_impact_analysis_component.rb +0 -45
- data/app/components/solidstats/security/overview_component.html.erb +0 -21
- data/app/components/solidstats/security/overview_component.rb +0 -104
- data/app/components/solidstats/security/section_component.html.erb +0 -26
- data/app/components/solidstats/security/section_component.rb +0 -52
- data/app/components/solidstats/security/timeline_component.html.erb +0 -39
- data/app/components/solidstats/security/timeline_component.rb +0 -43
- data/app/components/solidstats/tasks_section_component.html.erb +0 -17
- data/app/components/solidstats/tasks_section_component.rb +0 -22
- data/app/components/solidstats/ui/action_button_component.html.erb +0 -6
- data/app/components/solidstats/ui/action_button_component.rb +0 -71
- data/app/components/solidstats/ui/dashboard_layout_component.html.erb +0 -19
- data/app/components/solidstats/ui/dashboard_layout_component.rb +0 -85
- data/app/components/solidstats/ui/navigation_component.html.erb +0 -34
- data/app/components/solidstats/ui/navigation_component.rb +0 -72
- data/app/components/solidstats/ui/stats_overview_component.html.erb +0 -14
- data/app/components/solidstats/ui/stats_overview_component.rb +0 -78
- data/app/components/solidstats/ui/status_badge_component.html.erb +0 -6
- data/app/components/solidstats/ui/status_badge_component.rb +0 -42
- data/app/components/solidstats/ui/summary_card_component.html.erb +0 -12
- data/app/components/solidstats/ui/summary_card_component.rb +0 -63
- data/app/components/solidstats/ui/tab_navigation_component.html.erb +0 -22
- data/app/components/solidstats/ui/tab_navigation_component.rb +0 -79
- data/app/controllers/solidstats/gem_metadata_controller.rb +0 -12
- data/app/services/solidstats/audit_service.rb +0 -56
- data/app/services/solidstats/data_collector_service.rb +0 -83
- data/app/services/solidstats/gem_metadata/fetcher_service.rb +0 -136
- 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 -81
- data/app/views/solidstats/gem_metadata/_panel.html.erb +0 -419
- data/lib/generators/solidstats/feature/feature_generator.rb +0 -170
- data/lib/generators/solidstats/feature/templates/component.html.erb +0 -84
- data/lib/generators/solidstats/feature/templates/component.rb.erb +0 -103
- data/lib/generators/solidstats/feature/templates/component.scss +0 -243
- data/lib/generators/solidstats/feature/templates/component_test.rb.erb +0 -183
- data/lib/generators/solidstats/feature/templates/controller.rb.erb +0 -44
- data/lib/generators/solidstats/feature/templates/controller_test.rb.erb +0 -111
- data/lib/generators/solidstats/feature/templates/detail_view.html.erb +0 -755
- data/lib/generators/solidstats/feature/templates/preview.rb.erb +0 -107
- data/lib/generators/solidstats/feature/templates/service.rb.erb +0 -132
- data/lib/generators/solidstats/feature/templates/service_test.rb.erb +0 -109
- data/lib/generators/solidstats/install_generator.rb +0 -109
- data/lib/generators/solidstats/templates/initializer.rb +0 -112
- data/lib/solidstats/asset_compatibility.rb +0 -238
- data/lib/solidstats/asset_manifest.rb +0 -205
- data/lib/tasks/solidstats_tasks.rake +0 -4
@@ -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
|
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
|
-
|
13
|
-
|
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
|
-
|
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
|
29
72
|
|
30
|
-
|
31
|
-
|
32
|
-
# Remove Sprockets directives since we're inlining
|
33
|
-
cleaned_js = javascript_content.gsub(/^\/\/=.*$/, "").strip
|
73
|
+
"/* === #{relative_path} === */\n#{cleaned_js}"
|
74
|
+
end.join("\n\n")
|
34
75
|
|
35
|
-
|
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
|
36
108
|
else
|
37
|
-
|
38
|
-
|
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
|