solidstats 0.0.4 → 1.0.0
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 +61 -5
- data/README.md +52 -5
- data/app/controllers/solidstats/dashboard_controller.rb +43 -123
- data/app/services/solidstats/audit_service.rb +56 -0
- data/app/services/solidstats/data_collector_service.rb +83 -0
- data/app/services/solidstats/todo_service.rb +114 -0
- data/app/views/solidstats/dashboard/_todos.html.erb +6 -27
- data/app/views/solidstats/dashboard/audit/_additional_styles.css +22 -0
- data/app/views/solidstats/dashboard/audit/_audit_details.html.erb +47 -58
- data/app/views/solidstats/dashboard/audit/_audit_summary.html.erb +1 -1
- data/app/views/solidstats/dashboard/audit/_security_audit.html.erb +0 -23
- data/app/views/solidstats/dashboard/audit/_vulnerabilities_table.html.erb +1092 -38
- data/app/views/solidstats/dashboard/audit/_vulnerability_details.html.erb +4 -3
- data/app/views/solidstats/dashboard/index.html.erb +1152 -162
- data/config/routes.rb +1 -0
- data/lib/solidstats/version.rb +1 -1
- data/lib/tasks/solidstats_release.rake +69 -0
- metadata +10 -5
- data/app/views/solidstats/dashboard/audit/_audit_filters.html.erb +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a77d4bb173582d123384ccdaad313743f1305432d26b03d9f9489512fd5a07fa
|
4
|
+
data.tar.gz: 7ff358c8b80b43483c308956b408928fa2ef9d4e729b06c7446d37d730a86ecf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 21c7e3383dd6e05ba382df3179a4935797b96872731e9e7cc4f9b3a7bed991a1cc93c44cc9b517a4333b97387475b73642ece0b69aba0e18df4a2a55508dcdb6
|
7
|
+
data.tar.gz: 6ddc2a6c605f4975b6fd7e614cb01e9830b1616cf40597a2a779ec81c0673403517d39f961603f8ac12fda134461af0bd9b13823c3e5552e7d6ae3f36008c66e
|
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## [1.0.0] - 2025-05-22
|
9
|
+
|
10
|
+
### Added
|
11
|
+
- Fully functional refresh button with AJAX updates
|
12
|
+
- Dynamic dashboard refreshing for all components:
|
13
|
+
- Security metrics and score update in real-time
|
14
|
+
- Vulnerability table regeneration with fresh data
|
15
|
+
- Donut chart visualization updates
|
16
|
+
- Gem impact analysis section updates
|
17
|
+
- Vulnerability details section updates
|
18
|
+
- Enhanced disabled button tooltips with inline display
|
19
|
+
- Comprehensive security dashboard features:
|
20
|
+
- Security score rating (A+, B, C) based on vulnerabilities
|
21
|
+
- Detailed vulnerability table with filtering and searching
|
22
|
+
- Visual severity breakdown with interactive donut chart
|
23
|
+
- Gem impact analysis with affected gem grouping
|
24
|
+
- Detailed vulnerability information with remediation options
|
25
|
+
|
26
|
+
### Fixed
|
27
|
+
- Vulnerability details no longer hidden under sticky header when clicking "More details"
|
28
|
+
- Improved scroll offset calculations for better navigation
|
29
|
+
- Added visual feedback with animations when viewing details
|
30
|
+
- Fixed "Back to vulnerabilities table" button positioning
|
31
|
+
|
32
|
+
## [0.0.4] - 2025-05-20
|
33
|
+
|
34
|
+
### Added
|
35
|
+
- Service-based architecture for data collection with caching
|
36
|
+
- Base DataCollectorService class for shared functionality
|
37
|
+
- Specialized services: AuditService and TodoService
|
38
|
+
- Comprehensive UI/UX redesign with modern dashboard layout:
|
39
|
+
- Sticky navigation menu with intuitive section links
|
40
|
+
- Organized dashboard sections (Overview, Security, Code Quality, Tasks)
|
41
|
+
- Tabbed interfaces for better content organization
|
42
|
+
- Interactive summary cards with click-to-navigate functionality
|
43
|
+
- Enhanced security visualization:
|
44
|
+
- Visual security score indicator (A+, B, C ratings) with color coding
|
45
|
+
- Improved vulnerability metrics display with severity breakdown
|
46
|
+
- Detailed vulnerability table with filtering and search capabilities
|
47
|
+
- New "Affected Gems" tab with visual cards for vulnerable dependencies
|
48
|
+
- Timeline visualization for security history
|
49
|
+
- Improved code quality section:
|
50
|
+
- Dedicated tabs for Metrics, Test Coverage, and Code Health
|
51
|
+
- Visual progress bars for test coverage
|
52
|
+
- Task management improvements:
|
53
|
+
- Organized tabs for TODOs, FIXMEs, and HACKs
|
54
|
+
- Better categorization and display of code tasks
|
55
|
+
- Responsive layout adjustments for various screen sizes
|
56
|
+
- Quick floating navigation menu for easy section access
|
57
|
+
- Toast notification system for user feedback
|
58
|
+
|
59
|
+
### Fixed
|
60
|
+
- Ruby version compatibility issues with dependency constraints
|
61
|
+
- Support for multiple Ruby versions (2.7+, 3.0+, 3.2+)
|
62
|
+
- Automatic Rails version selection based on Ruby version (6.1 through 8.x)
|
63
|
+
- Documentation of compatibility matrix in README
|
64
|
+
- Variable scope issues in nested template partials
|
65
|
+
- Consistent passing of data between dashboard partials
|
66
|
+
|
8
67
|
## [0.0.3] - 2025-05-20
|
9
68
|
|
10
69
|
### Added
|
@@ -28,8 +87,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
28
87
|
|
29
88
|
## [Unreleased]
|
30
89
|
|
31
|
-
|
32
|
-
|
33
|
-
- Displayed TODO, FIXME, and HACK counts with color-coded badges.
|
34
|
-
- Added expandable details for files with most TODOs and all TODO items.
|
35
|
-
- Enhanced UI with better grouping and error handling for TODO data.
|
90
|
+
### Added
|
91
|
+
- Future enhancements will be listed here
|
data/README.md
CHANGED
@@ -1,12 +1,28 @@
|
|
1
|
-
|
2
|
-
Solidstats is a local-only Rails engine that shows your project's health at `/solidstats`.
|
1
|
+
Solidstats is a local-only Rails engine that shows your project's health at `/solidstats`. The dashboard provides real-time insights into your application's security, code quality, and development tasks.
|
3
2
|
|
4
3
|
## Features
|
5
|
-
-
|
6
|
-
-
|
7
|
-
-
|
4
|
+
- Interactive security dashboard with real-time refresh capability
|
5
|
+
- Comprehensive gem vulnerability analysis with severity breakdown
|
6
|
+
- Visual security score rating (A+, B, C) and metrics
|
7
|
+
- Bundler Audit scan with detailed remediation suggestions
|
8
|
+
- Interactive vulnerability details with patched version information
|
9
|
+
- Gem impact analysis showing affected gems by severity
|
10
|
+
- Rubocop offense count and quality metrics
|
11
|
+
- TODO/FIXME tracker with file hotspots
|
8
12
|
- Test coverage summary
|
9
13
|
|
14
|
+
## Compatibility
|
15
|
+
|
16
|
+
- Ruby 2.7+: Compatible with Rails 6.1 through Rails 7.0
|
17
|
+
- Ruby 3.0-3.1: Compatible with Rails 6.1 through Rails 7.x
|
18
|
+
- Ruby 3.2+: Compatible with all Rails 6.1+ versions
|
19
|
+
|
20
|
+
Solidstats automatically detects your Ruby version and selects a compatible Rails version.
|
21
|
+
|
22
|
+
### CI/Testing
|
23
|
+
|
24
|
+
This gem is automatically tested across multiple Ruby versions (2.7, 3.0, 3.1, and 3.2) to ensure compatibility. If you're contributing to this gem, make sure your changes work across all supported Ruby versions.
|
25
|
+
|
10
26
|
## Installation
|
11
27
|
|
12
28
|
```ruby
|
@@ -35,5 +51,36 @@ The installer will automatically mount the engine in your routes:
|
|
35
51
|
mount Solidstats::Engine => '/solidstats' if Rails.env.development?
|
36
52
|
```
|
37
53
|
|
54
|
+
## Usage
|
55
|
+
|
56
|
+
Visit `/solidstats` in your development environment to access the dashboard. The dashboard provides an overview of your application's health and is organized into the following sections:
|
57
|
+
|
58
|
+
### Overview
|
59
|
+
Shows summary cards for security issues, TODO items, and code quality metrics.
|
60
|
+
|
61
|
+
### Security
|
62
|
+
Provides a comprehensive security dashboard with:
|
63
|
+
- Security score rating based on vulnerability severity
|
64
|
+
- Vulnerability metrics showing critical, high, medium and low issues
|
65
|
+
- Interactive vulnerability table with filtering and searching
|
66
|
+
- Gem impact analysis showing which gems are affected
|
67
|
+
- Detailed vulnerability information with remediation suggestions
|
68
|
+
|
69
|
+
You can refresh the dashboard data at any time by clicking the "Refresh" button in the top navigation bar. This will:
|
70
|
+
1. Trigger a fresh security audit of your application
|
71
|
+
2. Update all metrics and visualizations with current data
|
72
|
+
3. Show real-time feedback during the refresh process
|
73
|
+
4. Update the "Last Updated" timestamp
|
74
|
+
|
75
|
+
### Code Quality
|
76
|
+
Displays code quality metrics, test coverage, and code health indicators.
|
77
|
+
|
78
|
+
### Tasks
|
79
|
+
Shows a breakdown of TODO, FIXME, and HACK annotations found in your codebase, with file hotspots and expandable details.
|
80
|
+
|
81
|
+
## Contributing
|
82
|
+
|
83
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/infolily/solidstats.
|
84
|
+
|
38
85
|
## License
|
39
86
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
@@ -1,138 +1,58 @@
|
|
1
1
|
module Solidstats
|
2
2
|
class DashboardController < ApplicationController
|
3
|
-
AUDIT_CACHE_FILE = Rails.root.join("tmp", "solidstats_audit.json")
|
4
3
|
TODO_CACHE_FILE = Rails.root.join("tmp", "solidstats_todos.json")
|
5
4
|
AUDIT_CACHE_HOURS = 12 # Configure how many hours before refreshing
|
6
5
|
|
7
6
|
def index
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@todo_stats = calculate_todo_stats(@todo_items)
|
12
|
-
@coverage = "read_coverage_percent"
|
13
|
-
end
|
14
|
-
|
15
|
-
private
|
16
|
-
|
17
|
-
def fetch_audit_output
|
18
|
-
# Check if cache file exists and is recent enough
|
19
|
-
if File.exist?(AUDIT_CACHE_FILE)
|
20
|
-
cached_data = JSON.parse(File.read(AUDIT_CACHE_FILE))
|
21
|
-
last_run_time = Time.parse(cached_data["timestamp"])
|
22
|
-
|
23
|
-
# Use cached data if it's less than AUDIT_CACHE_HOURS old
|
24
|
-
if Time.now - last_run_time < AUDIT_CACHE_HOURS.hours
|
25
|
-
return cached_data["output"]
|
26
|
-
end
|
27
|
-
end
|
28
|
-
|
29
|
-
# Cache expired or doesn't exist, run the audit
|
30
|
-
raw_output = `bundle audit check --update --format json`
|
31
|
-
json_part = raw_output[/\{.*\}/m] # extract JSON starting from first '{'
|
32
|
-
audit_output = JSON.parse(json_part) rescue { error: "Failed to parse JSON" }
|
33
|
-
|
34
|
-
# Save to cache file with timestamp
|
35
|
-
cache_data = {
|
36
|
-
"output" => audit_output,
|
37
|
-
"timestamp" => Time.now.iso8601
|
38
|
-
}
|
39
|
-
|
40
|
-
# Ensure the tmp directory exists
|
41
|
-
FileUtils.mkdir_p(File.dirname(AUDIT_CACHE_FILE))
|
42
|
-
|
43
|
-
# Write the cache file
|
44
|
-
File.write(AUDIT_CACHE_FILE, JSON.generate(cache_data))
|
45
|
-
|
46
|
-
audit_output
|
47
|
-
end
|
48
|
-
|
49
|
-
def fetch_todo_items
|
50
|
-
# Check if cache file exists and is recent enough
|
51
|
-
if File.exist?(TODO_CACHE_FILE)
|
52
|
-
cached_data = JSON.parse(File.read(TODO_CACHE_FILE))
|
53
|
-
last_run_time = Time.parse(cached_data["timestamp"])
|
54
|
-
|
55
|
-
# Use cached data if it's less than AUDIT_CACHE_HOURS old
|
56
|
-
if Time.now - last_run_time < AUDIT_CACHE_HOURS.hours
|
57
|
-
return cached_data["output"]
|
58
|
-
end
|
59
|
-
end
|
7
|
+
# Use new services for data collection
|
8
|
+
audit_service = AuditService.new
|
9
|
+
todo_service = TodoService.new
|
60
10
|
|
61
|
-
|
62
|
-
|
63
|
-
|
11
|
+
# Get full data for detailed views
|
12
|
+
@audit_output = audit_service.fetch
|
13
|
+
@todo_items = todo_service.fetch
|
64
14
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
line_num = $2
|
69
|
-
content = $3
|
15
|
+
# Get summary data for dashboard cards
|
16
|
+
@audit_summary = audit_service.summary
|
17
|
+
@todo_summary = todo_service.summary
|
70
18
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
# Convert to uppercase for consistency
|
75
|
-
type = type_match.to_s.upcase
|
76
|
-
|
77
|
-
todos << {
|
78
|
-
file: file,
|
79
|
-
line: line_num.to_i,
|
80
|
-
content: content.strip,
|
81
|
-
type: type
|
82
|
-
}
|
83
|
-
end
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
# Save to cache file with timestamp
|
88
|
-
cache_data = {
|
89
|
-
"output" => todos,
|
90
|
-
"timestamp" => Time.now.iso8601
|
91
|
-
}
|
92
|
-
|
93
|
-
# Ensure the tmp directory exists
|
94
|
-
FileUtils.mkdir_p(File.dirname(TODO_CACHE_FILE))
|
95
|
-
|
96
|
-
# Write the cache file
|
97
|
-
File.write(TODO_CACHE_FILE, JSON.generate(cache_data))
|
98
|
-
|
99
|
-
todos
|
19
|
+
# TODO: Refactor these to use services as well
|
20
|
+
@rubocop_output = "JSON.parse(`rubocop --format json`)"
|
21
|
+
@coverage = "100"
|
100
22
|
end
|
101
23
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
24
|
+
# Force refresh all dashboard data
|
25
|
+
def refresh
|
26
|
+
# Create services
|
27
|
+
audit_service = AuditService.new
|
28
|
+
todo_service = TodoService.new
|
29
|
+
|
30
|
+
# Force refresh of data
|
31
|
+
audit_output = audit_service.fetch(true) # Force refresh
|
32
|
+
todo_items = todo_service.fetch(true) # Force refresh
|
33
|
+
|
34
|
+
# Get updated summaries
|
35
|
+
audit_summary = audit_service.summary
|
36
|
+
todo_summary = todo_service.summary
|
37
|
+
|
38
|
+
# Get current time for last updated display
|
39
|
+
last_updated = Time.now.strftime("%B %d, %Y at %H:%M")
|
40
|
+
|
41
|
+
# Return JSON response with refreshed data
|
42
|
+
render json: {
|
43
|
+
audit_output: audit_output,
|
44
|
+
todo_items: todo_items,
|
45
|
+
audit_summary: audit_summary,
|
46
|
+
todo_summary: todo_summary,
|
47
|
+
last_updated: last_updated,
|
48
|
+
status: "success"
|
113
49
|
}
|
114
|
-
|
115
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
end
|
121
|
-
|
122
|
-
# Find files with most TODOs (top 5)
|
123
|
-
stats[:hotspots] = stats[:by_file].sort_by { |_, count| -count }
|
124
|
-
.first(5)
|
125
|
-
.to_h
|
126
|
-
|
127
|
-
stats
|
128
|
-
end
|
129
|
-
|
130
|
-
def read_coverage_percent
|
131
|
-
file = Rails.root.join("coverage", ".last_run.json")
|
132
|
-
return 0 unless File.exist?(file)
|
133
|
-
|
134
|
-
data = JSON.parse(File.read(file))
|
135
|
-
data.dig("result", "covered_percent").to_f.round(2)
|
50
|
+
rescue StandardError => e
|
51
|
+
# Return error information
|
52
|
+
render json: {
|
53
|
+
status: "error",
|
54
|
+
message: "Failed to refresh data: #{e.message}"
|
55
|
+
}, status: :internal_server_error
|
136
56
|
end
|
137
57
|
end
|
138
58
|
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solidstats
|
4
|
+
# Service to collect and process security audit data
|
5
|
+
class AuditService < DataCollectorService
|
6
|
+
# Initialize with default cache settings for audits
|
7
|
+
def initialize
|
8
|
+
super(Rails.root.join("tmp", "solidstats_audit.json"))
|
9
|
+
end
|
10
|
+
|
11
|
+
# Generate a summary for the dashboard display
|
12
|
+
# @return [Hash] Summary information with status, count, and message
|
13
|
+
def summary
|
14
|
+
data = fetch
|
15
|
+
vuln_count = data["vulnerabilities"]&.count || 0
|
16
|
+
|
17
|
+
{
|
18
|
+
count: vuln_count,
|
19
|
+
status: determine_status(vuln_count),
|
20
|
+
message: generate_message(vuln_count)
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
# Determine the status indicator based on vulnerability count
|
27
|
+
# @param count [Integer] Number of vulnerabilities
|
28
|
+
# @return [String] Status indicator (success, warning, or danger)
|
29
|
+
def determine_status(count)
|
30
|
+
case count
|
31
|
+
when 0 then "success"
|
32
|
+
when 1..2 then "warning"
|
33
|
+
else "danger"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Generate a status message based on vulnerability count
|
38
|
+
# @param count [Integer] Number of vulnerabilities
|
39
|
+
# @return [String] Human-readable status message
|
40
|
+
def generate_message(count)
|
41
|
+
case count
|
42
|
+
when 0 then "No vulnerabilities found"
|
43
|
+
when 1 then "1 vulnerability found"
|
44
|
+
else "#{count} vulnerabilities found"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Collect fresh audit data by running bundle-audit
|
49
|
+
# @return [Hash] Parsed audit data
|
50
|
+
def collect_data
|
51
|
+
raw_output = `bundle audit check --update --format json`
|
52
|
+
json_part = raw_output[/\{.*\}/m] # extract JSON starting from first '{'
|
53
|
+
JSON.parse(json_part) rescue { "error" => "Failed to parse JSON", "vulnerabilities" => [] }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solidstats
|
4
|
+
# Base service class for all data collectors
|
5
|
+
# This class handles caching logic and provides a common interface
|
6
|
+
# for all data collection services in SolidStats
|
7
|
+
class DataCollectorService
|
8
|
+
attr_reader :cache_file, :cache_duration
|
9
|
+
|
10
|
+
# Initialize the data collector with cache configuration
|
11
|
+
# @param cache_file [String] Path to the cache file
|
12
|
+
# @param cache_duration [ActiveSupport::Duration] Duration to keep the cache valid
|
13
|
+
def initialize(cache_file, cache_duration = 12.hours)
|
14
|
+
@cache_file = cache_file
|
15
|
+
@cache_duration = cache_duration
|
16
|
+
end
|
17
|
+
|
18
|
+
# Fetch data with caching
|
19
|
+
# Returns cached data if available and not expired,
|
20
|
+
# otherwise collects fresh data
|
21
|
+
# @param force_refresh [Boolean] Whether to force a refresh regardless of cache state
|
22
|
+
# @return [Hash] The collected data
|
23
|
+
def fetch(force_refresh = false)
|
24
|
+
return cached_data if fresh_cache? && !force_refresh
|
25
|
+
|
26
|
+
data = collect_data
|
27
|
+
save_to_cache(data)
|
28
|
+
data
|
29
|
+
end
|
30
|
+
|
31
|
+
# Get a summary of the data for dashboard display
|
32
|
+
# @return [Hash] Summary information
|
33
|
+
def summary
|
34
|
+
raise NotImplementedError, "Subclasses must implement summary"
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Get data from the cache file
|
40
|
+
# @return [Hash, nil] Cached data or nil if cache invalid
|
41
|
+
def cached_data
|
42
|
+
JSON.parse(File.read(cache_file))["output"]
|
43
|
+
rescue StandardError => e
|
44
|
+
Rails.logger.error "Error reading cache: #{e.message}"
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
|
48
|
+
# Check if the cache is fresh (exists and not expired)
|
49
|
+
# @return [Boolean] True if cache is valid and not expired
|
50
|
+
def fresh_cache?
|
51
|
+
return false unless File.exist?(cache_file)
|
52
|
+
|
53
|
+
begin
|
54
|
+
cached_data = JSON.parse(File.read(cache_file))
|
55
|
+
last_run_time = Time.parse(cached_data["timestamp"])
|
56
|
+
Time.now - last_run_time < cache_duration
|
57
|
+
rescue StandardError => e
|
58
|
+
Rails.logger.error "Error checking cache freshness: #{e.message}"
|
59
|
+
false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
# Save data to the cache file
|
64
|
+
# @param data [Hash] Data to save to cache
|
65
|
+
def save_to_cache(data)
|
66
|
+
cache_data = {
|
67
|
+
"output" => data,
|
68
|
+
"timestamp" => Time.now.iso8601
|
69
|
+
}
|
70
|
+
|
71
|
+
FileUtils.mkdir_p(File.dirname(cache_file))
|
72
|
+
File.write(cache_file, JSON.generate(cache_data))
|
73
|
+
rescue StandardError => e
|
74
|
+
Rails.logger.error "Error saving to cache: #{e.message}"
|
75
|
+
end
|
76
|
+
|
77
|
+
# Collect fresh data - to be implemented by subclasses
|
78
|
+
# @return [Hash] The collected data
|
79
|
+
def collect_data
|
80
|
+
raise NotImplementedError, "Subclasses must implement collect_data"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Solidstats
|
4
|
+
# Service to collect and process TODO items from codebase
|
5
|
+
class TodoService < DataCollectorService
|
6
|
+
# Initialize with default cache settings for TODO items
|
7
|
+
def initialize
|
8
|
+
super(Rails.root.join("tmp", "solidstats_todos.json"))
|
9
|
+
end
|
10
|
+
|
11
|
+
# Generate a summary for the dashboard display
|
12
|
+
# @return [Hash] Summary information with counts and status
|
13
|
+
def summary
|
14
|
+
todos = fetch
|
15
|
+
return { count: 0, status: "success", message: "No TODO items found" } if todos.nil? || todos.empty?
|
16
|
+
|
17
|
+
stats = calculate_stats(todos)
|
18
|
+
|
19
|
+
{
|
20
|
+
count: todos.count,
|
21
|
+
status: determine_status(todos),
|
22
|
+
message: generate_message(todos.count),
|
23
|
+
by_type: stats[:by_type],
|
24
|
+
hotspots: stats[:hotspots]
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# Calculate statistics for the TODO items
|
31
|
+
# @param todos [Array] List of TODO items
|
32
|
+
# @return [Hash] Statistics about the TODO items
|
33
|
+
def calculate_stats(todos)
|
34
|
+
stats = {
|
35
|
+
by_type: {
|
36
|
+
"TODO" => todos.count { |t| (t[:type] || t["type"]) == "TODO" },
|
37
|
+
"FIXME" => todos.count { |t| (t[:type] || t["type"]) == "FIXME" },
|
38
|
+
"HACK" => todos.count { |t| (t[:type] || t["type"]) == "HACK" }
|
39
|
+
},
|
40
|
+
by_file: {}
|
41
|
+
}
|
42
|
+
|
43
|
+
# Group by file path
|
44
|
+
todos.each do |todo|
|
45
|
+
file_path = todo[:file] || todo["file"]
|
46
|
+
stats[:by_file][file_path] ||= 0
|
47
|
+
stats[:by_file][file_path] += 1
|
48
|
+
end
|
49
|
+
|
50
|
+
# Find files with most TODOs (top 5)
|
51
|
+
stats[:hotspots] = stats[:by_file].sort_by { |_, count| -count }
|
52
|
+
.first(5)
|
53
|
+
.to_h
|
54
|
+
|
55
|
+
stats
|
56
|
+
end
|
57
|
+
|
58
|
+
# Determine the status indicator based on TODO items
|
59
|
+
# @param todos [Array] List of TODO items
|
60
|
+
# @return [String] Status indicator (success, warning, or danger)
|
61
|
+
def determine_status(todos)
|
62
|
+
fixme_count = todos.count { |t| t[:type] == "FIXME" }
|
63
|
+
hack_count = todos.count { |t| t[:type] == "HACK" }
|
64
|
+
|
65
|
+
if hack_count > 0
|
66
|
+
"danger"
|
67
|
+
elsif fixme_count > 0
|
68
|
+
"warning"
|
69
|
+
elsif todos.count > 10
|
70
|
+
"warning"
|
71
|
+
else
|
72
|
+
"success"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Generate a status message based on TODO count
|
77
|
+
# @param count [Integer] Number of TODOs
|
78
|
+
# @return [String] Human-readable status message
|
79
|
+
def generate_message(count)
|
80
|
+
"#{count} #{count == 1 ? 'item' : 'items'} found"
|
81
|
+
end
|
82
|
+
|
83
|
+
# Collect fresh TODO data by scanning the codebase
|
84
|
+
# @return [Array] List of TODO items found
|
85
|
+
def collect_data
|
86
|
+
todos = []
|
87
|
+
raw_output = `grep -r -n -E "(TODO|FIXME|HACK|todo|fixme|hack)" app lib`.split("\n")
|
88
|
+
|
89
|
+
raw_output.each do |line|
|
90
|
+
if line =~ /^(.+):(\d+):(.+)$/
|
91
|
+
file = $1
|
92
|
+
line_num = $2
|
93
|
+
content = $3
|
94
|
+
|
95
|
+
# Match only exact lowercase or uppercase variants
|
96
|
+
type_match = content.match(/(TODO|FIXME|HACK|todo|fixme|hack)/)
|
97
|
+
if type_match
|
98
|
+
# Convert to uppercase for consistency
|
99
|
+
type = type_match.to_s.upcase
|
100
|
+
|
101
|
+
todos << {
|
102
|
+
file: file,
|
103
|
+
line: line_num.to_i,
|
104
|
+
content: content.strip,
|
105
|
+
type: type
|
106
|
+
}
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
todos
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -9,25 +9,19 @@
|
|
9
9
|
<div class="status-badge warning"><%= @todo_items.count %> <%= "Item".pluralize(@todo_items.count) %> Found</div>
|
10
10
|
<% end %>
|
11
11
|
|
12
|
-
<% if @todo_items.present? && @
|
13
|
-
<%
|
14
|
-
# Ensure we have proper counts by type - recalculate if needed
|
15
|
-
todo_count = @todo_items.count { |item| (item[:type] || item["type"])&.upcase == "TODO" }
|
16
|
-
fixme_count = @todo_items.count { |item| (item[:type] || item["type"])&.upcase == "FIXME" }
|
17
|
-
hack_count = @todo_items.count { |item| (item[:type] || item["type"])&.upcase == "HACK" }
|
18
|
-
%>
|
12
|
+
<% if @todo_items.present? && @todo_summary.present? %>
|
19
13
|
<div class="metrics-group">
|
20
14
|
<div class="metric">
|
21
15
|
<span class="metric-label">TODO:</span>
|
22
|
-
<span class="metric-value"><%=
|
16
|
+
<span class="metric-value"><%= @todo_summary[:by_type]["TODO"] %></span>
|
23
17
|
</div>
|
24
18
|
<div class="metric">
|
25
19
|
<span class="metric-label">FIXME:</span>
|
26
|
-
<span class="metric-value text-danger"><%=
|
20
|
+
<span class="metric-value text-danger"><%= @todo_summary[:by_type]["FIXME"] %></span>
|
27
21
|
</div>
|
28
22
|
<div class="metric">
|
29
23
|
<span class="metric-label">HACK:</span>
|
30
|
-
<span class="metric-value text-warning"><%=
|
24
|
+
<span class="metric-value text-warning"><%= @todo_summary[:by_type]["HACK"] %></span>
|
31
25
|
</div>
|
32
26
|
</div>
|
33
27
|
|
@@ -35,7 +29,7 @@
|
|
35
29
|
<% end %>
|
36
30
|
</div>
|
37
31
|
|
38
|
-
<% if @todo_items.present? && @
|
32
|
+
<% if @todo_items.present? && @todo_summary.present? %>
|
39
33
|
<div id="todo-details" class="details-panel hidden">
|
40
34
|
<h3 class="details-section-title">Files with Most TODOs</h3>
|
41
35
|
<div class="table-responsive mb-4">
|
@@ -47,22 +41,7 @@
|
|
47
41
|
</tr>
|
48
42
|
</thead>
|
49
43
|
<tbody>
|
50
|
-
<%
|
51
|
-
# If hotspots has nil keys, reconstruct it from @todo_items
|
52
|
-
hotspots = @todo_stats[:hotspots]
|
53
|
-
if hotspots.is_a?(Hash) && hotspots.keys.any?(&:nil?)
|
54
|
-
# Rebuild hotspots from todo_items
|
55
|
-
hotspots = {}
|
56
|
-
@todo_items.each do |item|
|
57
|
-
item_hash = item.respond_to?(:symbolize_keys) ? item.symbolize_keys : item
|
58
|
-
file = item_hash[:file] || item["file"]
|
59
|
-
next unless file.present?
|
60
|
-
hotspots[file] ||= 0
|
61
|
-
hotspots[file] += 1
|
62
|
-
end
|
63
|
-
end
|
64
|
-
%>
|
65
|
-
|
44
|
+
<% hotspots = @todo_summary[:hotspots] %>
|
66
45
|
<% if hotspots.present? %>
|
67
46
|
<% hotspots.each do |file, count| %>
|
68
47
|
<tr>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
.details-link-btn {
|
2
|
+
color: #0366d6;
|
3
|
+
font-size: 0.85rem;
|
4
|
+
text-decoration: none;
|
5
|
+
display: inline-flex;
|
6
|
+
align-items: center;
|
7
|
+
gap: 0.25rem;
|
8
|
+
}
|
9
|
+
|
10
|
+
.details-link-btn:hover {
|
11
|
+
text-decoration: underline;
|
12
|
+
}
|
13
|
+
|
14
|
+
.external-link-icon-sm {
|
15
|
+
font-size: 0.75rem;
|
16
|
+
}
|
17
|
+
|
18
|
+
.details-preview {
|
19
|
+
font-size: 0.85rem;
|
20
|
+
color: #666;
|
21
|
+
margin-top: 0.25rem;
|
22
|
+
}
|