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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1e3efc1790ad2beb55d19f731457a39034c744524da80c85674bc0d02fdb1d83
4
- data.tar.gz: 50a12e44bf70c1b297d86f8645267f4648b1476df611cba0e72fffb26e40c638
3
+ metadata.gz: a77d4bb173582d123384ccdaad313743f1305432d26b03d9f9489512fd5a07fa
4
+ data.tar.gz: 7ff358c8b80b43483c308956b408928fa2ef9d4e729b06c7446d37d730a86ecf
5
5
  SHA512:
6
- metadata.gz: a5d7b257090f0869fc20c6c1144f7a120d7e8bdc12d8474df5db603d6918f417d7b59a24a38f0e89cf0e89c0d2507de6a7984f8f293a3a1d3b3f5e368e29d2de
7
- data.tar.gz: c7dccdf4109e89e33ccad5a58af97e9cb4ea7e317243a6de9eafc0e24a5001abef0650b9a133bc8af6b5c52bdbf187c4818a28a519e55df42731611d89fe74da
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
- - Improved TODO dashboard partial:
32
- - Ensured correct handling of nil keys in the hotspots hash.
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
- - Bundler Audit scan
6
- - Rubocop offense count
7
- - TODO/FIXME tracker
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
- @audit_output = fetch_audit_output
9
- @rubocop_output = "JSON.parse(`rubocop --format json`)"
10
- @todo_items = fetch_todo_items
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
- todos = []
62
- # Updated grep pattern to match only all-uppercase or all-lowercase variants
63
- raw_output = `grep -r -n -E "(TODO|FIXME|HACK|todo|fixme|hack)" app lib`.split("\n")
11
+ # Get full data for detailed views
12
+ @audit_output = audit_service.fetch
13
+ @todo_items = todo_service.fetch
64
14
 
65
- raw_output.each do |line|
66
- if line =~ /^(.+):(\d+):(.+)$/
67
- file = $1
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
- # Match only exact lowercase or uppercase variants
72
- type_match = content.match(/(TODO|FIXME|HACK|todo|fixme|hack)/)
73
- if type_match
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
- def calculate_todo_stats(todos)
103
- return {} if todos.nil? || todos.empty?
104
-
105
- stats = {
106
- total: todos.count,
107
- by_type: {
108
- "TODO" => todos.count { |t| t[:type] == "TODO" },
109
- "FIXME" => todos.count { |t| t[:type] == "FIXME" },
110
- "HACK" => todos.count { |t| t[:type] == "HACK" }
111
- },
112
- by_file: {}
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
- # Group by file path
116
- todos.each do |todo|
117
- file_path = todo[:file]
118
- stats[:by_file][file_path] ||= 0
119
- stats[:by_file][file_path] += 1
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? && @todo_stats.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"><%= todo_count %></span>
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"><%= fixme_count %></span>
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"><%= hack_count %></span>
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? && @todo_stats.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
+ }