archsight 0.1.1 → 0.1.3
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/Dockerfile +21 -22
- data/README.md +38 -12
- data/exe/archsight +3 -1
- data/lib/archsight/analysis/executor.rb +112 -0
- data/lib/archsight/analysis/result.rb +174 -0
- data/lib/archsight/analysis/sandbox.rb +319 -0
- data/lib/archsight/analysis.rb +11 -0
- data/lib/archsight/annotations/architecture_annotations.rb +2 -2
- data/lib/archsight/cli.rb +164 -0
- data/lib/archsight/database.rb +6 -2
- data/lib/archsight/helpers/analysis_renderer.rb +83 -0
- data/lib/archsight/helpers/formatting.rb +95 -0
- data/lib/archsight/helpers.rb +20 -4
- data/lib/archsight/import/concurrent_progress.rb +341 -0
- data/lib/archsight/import/executor.rb +466 -0
- data/lib/archsight/import/git_analytics.rb +626 -0
- data/lib/archsight/import/handler.rb +263 -0
- data/lib/archsight/import/handlers/github.rb +161 -0
- data/lib/archsight/import/handlers/gitlab.rb +202 -0
- data/lib/archsight/import/handlers/jira_base.rb +189 -0
- data/lib/archsight/import/handlers/jira_discover.rb +161 -0
- data/lib/archsight/import/handlers/jira_metrics.rb +179 -0
- data/lib/archsight/import/handlers/openapi_schema_parser.rb +279 -0
- data/lib/archsight/import/handlers/repository.rb +439 -0
- data/lib/archsight/import/handlers/rest_api.rb +293 -0
- data/lib/archsight/import/handlers/rest_api_index.rb +183 -0
- data/lib/archsight/import/progress.rb +91 -0
- data/lib/archsight/import/registry.rb +54 -0
- data/lib/archsight/import/shared_file_writer.rb +67 -0
- data/lib/archsight/import/team_matcher.rb +195 -0
- data/lib/archsight/import.rb +14 -0
- data/lib/archsight/resources/analysis.rb +91 -0
- data/lib/archsight/resources/application_component.rb +2 -2
- data/lib/archsight/resources/application_service.rb +12 -12
- data/lib/archsight/resources/business_product.rb +12 -12
- data/lib/archsight/resources/data_object.rb +1 -1
- data/lib/archsight/resources/import.rb +79 -0
- data/lib/archsight/resources/technology_artifact.rb +23 -2
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/api/docs.rb +17 -0
- data/lib/archsight/web/api/json_helpers.rb +164 -0
- data/lib/archsight/web/api/openapi/spec.yaml +500 -0
- data/lib/archsight/web/api/routes.rb +101 -0
- data/lib/archsight/web/application.rb +66 -43
- data/lib/archsight/web/doc/import.md +458 -0
- data/lib/archsight/web/doc/index.md.erb +2 -1
- data/lib/archsight/web/public/css/artifact.css +10 -0
- data/lib/archsight/web/public/css/graph.css +14 -0
- data/lib/archsight/web/public/css/instance.css +489 -0
- data/lib/archsight/web/views/api_docs.erb +19 -0
- data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +14 -8
- data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +74 -0
- data/lib/archsight/web/views/partials/instance/_analysis_result.haml +64 -0
- data/lib/archsight/web/views/partials/instance/_detail.haml +7 -3
- data/lib/archsight/web/views/partials/instance/_import_detail.haml +87 -0
- data/lib/archsight/web/views/partials/instance/_relations.haml +4 -4
- data/lib/archsight/web/views/partials/layout/_content.haml +4 -0
- data/lib/archsight/web/views/partials/layout/_navigation.haml +6 -5
- metadata +78 -1
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Archsight
|
|
4
|
+
module Helpers
|
|
5
|
+
# Formatting provides string and number formatting utilities
|
|
6
|
+
module Formatting
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
# Convert string to class name format
|
|
10
|
+
def classify(val)
|
|
11
|
+
val.to_s.split("-").map(&:capitalize).join
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Format number as euro currency
|
|
15
|
+
def to_euro(num)
|
|
16
|
+
rounded = (num * 100).round / 100.0
|
|
17
|
+
parts = format("%.2f", rounded).split(".")
|
|
18
|
+
parts[0] = parts[0].reverse.scan(/\d{1,3}/).join(",").reverse
|
|
19
|
+
"€#{parts.join(".")}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# AI-adjusted project estimate configuration
|
|
23
|
+
# Source values stored separately for easy adjustment
|
|
24
|
+
AI_ESTIMATE_CONFIG = {
|
|
25
|
+
cocomo_salary: 150_000, # COCOMO assumes US salary in USD
|
|
26
|
+
target_salary: 80_000, # Target salary in EUR
|
|
27
|
+
ai_cost_multiplier: 3.0, # AI productivity boost for cost
|
|
28
|
+
ai_schedule_multiplier: 2.5, # AI productivity boost for schedule
|
|
29
|
+
ai_team_multiplier: 3.0 # AI productivity boost for team size
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
# Apply AI adjustment factors to project estimates
|
|
33
|
+
# @param type [Symbol] :cost, :schedule, or :team
|
|
34
|
+
# @param value [Numeric, nil] Raw estimate value
|
|
35
|
+
# @return [Numeric, nil] Adjusted value
|
|
36
|
+
def ai_adjusted_estimate(type, value)
|
|
37
|
+
return nil if value.nil?
|
|
38
|
+
|
|
39
|
+
cfg = AI_ESTIMATE_CONFIG
|
|
40
|
+
salary_ratio = cfg[:target_salary].to_f / cfg[:cocomo_salary]
|
|
41
|
+
|
|
42
|
+
adjusted = case type
|
|
43
|
+
when :cost
|
|
44
|
+
value.to_f * salary_ratio / cfg[:ai_cost_multiplier]
|
|
45
|
+
when :schedule
|
|
46
|
+
value.to_f / cfg[:ai_schedule_multiplier]
|
|
47
|
+
when :team
|
|
48
|
+
(value.to_f / cfg[:ai_team_multiplier]).ceil
|
|
49
|
+
else
|
|
50
|
+
raise ArgumentError, "Unknown estimate type: #{type}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
type == :team ? adjusted.to_i : adjusted
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Convert git URL to HTTPS URL
|
|
57
|
+
def http_git(repo_url)
|
|
58
|
+
repo_url.gsub(/.git$/, "")
|
|
59
|
+
.gsub(":", "/")
|
|
60
|
+
.gsub("git@", "https://")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Format number with thousands delimiter
|
|
64
|
+
def number_with_delimiter(num)
|
|
65
|
+
num.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Convert timestamp to human-readable relative time
|
|
69
|
+
def time_ago(timestamp)
|
|
70
|
+
return nil unless timestamp
|
|
71
|
+
|
|
72
|
+
time = timestamp.is_a?(Time) ? timestamp : Time.parse(timestamp.to_s)
|
|
73
|
+
seconds = (Time.now - time).to_i
|
|
74
|
+
|
|
75
|
+
units = [
|
|
76
|
+
[60, "second"],
|
|
77
|
+
[60, "minute"],
|
|
78
|
+
[24, "hour"],
|
|
79
|
+
[7, "day"],
|
|
80
|
+
[4, "week"],
|
|
81
|
+
[12, "month"],
|
|
82
|
+
[Float::INFINITY, "year"]
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
value = seconds
|
|
86
|
+
units.each do |divisor, unit|
|
|
87
|
+
return "just now" if unit == "second" && value < 10
|
|
88
|
+
return "#{value} #{unit}#{"s" if value != 1} ago" if value < divisor
|
|
89
|
+
|
|
90
|
+
value /= divisor
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/archsight/helpers.rb
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "helpers/formatting"
|
|
4
|
+
require_relative "helpers/analysis_renderer"
|
|
5
|
+
|
|
3
6
|
module Archsight
|
|
4
7
|
# Helpers provides utility functions for the architecture tool
|
|
5
8
|
module Helpers
|
|
9
|
+
# Include sub-modules for direct access
|
|
10
|
+
extend Formatting
|
|
11
|
+
extend AnalysisRenderer
|
|
12
|
+
|
|
6
13
|
module_function
|
|
7
14
|
|
|
8
15
|
# Make path relative to resources directory
|
|
@@ -45,10 +52,6 @@ module Archsight
|
|
|
45
52
|
end
|
|
46
53
|
end
|
|
47
54
|
|
|
48
|
-
def classify(val)
|
|
49
|
-
val.to_s.split("-").map(&:capitalize).join
|
|
50
|
-
end
|
|
51
|
-
|
|
52
55
|
def deep_merge(hash1, hash2)
|
|
53
56
|
hash1.dup.merge(hash2) do |_, old_value, new_value|
|
|
54
57
|
if old_value.is_a?(Hash) && new_value.is_a?(Hash)
|
|
@@ -206,5 +209,18 @@ module Archsight
|
|
|
206
209
|
(val_a || "").to_s.downcase <=> (val_b || "").to_s.downcase
|
|
207
210
|
end
|
|
208
211
|
end
|
|
212
|
+
|
|
213
|
+
# Delegate formatting methods
|
|
214
|
+
def classify(val) = Formatting.classify(val)
|
|
215
|
+
def to_euro(num) = Formatting.to_euro(num)
|
|
216
|
+
def ai_adjusted_estimate(type, value) = Formatting.ai_adjusted_estimate(type, value)
|
|
217
|
+
def http_git(repo_url) = Formatting.http_git(repo_url)
|
|
218
|
+
def number_with_delimiter(num) = Formatting.number_with_delimiter(num)
|
|
219
|
+
def time_ago(timestamp) = Formatting.time_ago(timestamp)
|
|
220
|
+
|
|
221
|
+
# Delegate analysis renderer methods
|
|
222
|
+
def escape_html(text) = AnalysisRenderer.escape_html(text)
|
|
223
|
+
def render_analysis_section(section, **) = AnalysisRenderer.render_analysis_section(section, **)
|
|
224
|
+
def render_analysis_table(section) = AnalysisRenderer.render_analysis_table(section)
|
|
209
225
|
end
|
|
210
226
|
end
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Manages concurrent progress output with slot-based display
|
|
4
|
+
#
|
|
5
|
+
# In TTY mode: Each slot gets its own line, updated in place using ANSI codes with colors
|
|
6
|
+
# In non-TTY mode: Each update prints on its own line with context prefix (no colors)
|
|
7
|
+
class Archsight::Import::ConcurrentProgress
|
|
8
|
+
# ANSI color codes
|
|
9
|
+
COLORS = {
|
|
10
|
+
reset: "\e[0m",
|
|
11
|
+
bold: "\e[1m",
|
|
12
|
+
dim: "\e[2m",
|
|
13
|
+
green: "\e[32m",
|
|
14
|
+
yellow: "\e[33m",
|
|
15
|
+
blue: "\e[34m",
|
|
16
|
+
magenta: "\e[35m",
|
|
17
|
+
cyan: "\e[36m",
|
|
18
|
+
red: "\e[31m"
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
# ANSI cursor control
|
|
22
|
+
CURSOR_HIDE = "\e[?25l"
|
|
23
|
+
CURSOR_SHOW = "\e[?25h"
|
|
24
|
+
CURSOR_SAVE = "\e[s"
|
|
25
|
+
CURSOR_RESTORE = "\e[u"
|
|
26
|
+
|
|
27
|
+
def initialize(max_slots:, output: $stdout)
|
|
28
|
+
@output = output
|
|
29
|
+
@tty = output.respond_to?(:tty?) && output.tty?
|
|
30
|
+
@max_slots = max_slots
|
|
31
|
+
@mutex = Mutex.new
|
|
32
|
+
@slots = {}
|
|
33
|
+
@slot_queue = Queue.new
|
|
34
|
+
@lines_printed = 0
|
|
35
|
+
|
|
36
|
+
# Overall progress tracking
|
|
37
|
+
@total_imports = 0
|
|
38
|
+
@completed_imports = 0
|
|
39
|
+
@has_overall_line = false
|
|
40
|
+
@start_time = nil
|
|
41
|
+
|
|
42
|
+
# Initialize slot queue
|
|
43
|
+
max_slots.times { |i| @slot_queue << i }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def tty?
|
|
47
|
+
@tty
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Initialize total number of imports for overall progress
|
|
51
|
+
def total=(total)
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
@total_imports = total
|
|
54
|
+
@completed_imports = 0
|
|
55
|
+
@start_time = Time.now
|
|
56
|
+
if @tty && !@has_overall_line
|
|
57
|
+
# Save cursor position and hide cursor for clean display
|
|
58
|
+
@output.print CURSOR_SAVE
|
|
59
|
+
@output.print CURSOR_HIDE
|
|
60
|
+
@output.puts build_overall_line
|
|
61
|
+
@has_overall_line = true
|
|
62
|
+
@lines_printed += 1
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Update total without resetting completed count (for multi-stage imports)
|
|
68
|
+
def update_total(total)
|
|
69
|
+
@mutex.synchronize do
|
|
70
|
+
@total_imports = total
|
|
71
|
+
update_overall_line if @tty && @has_overall_line
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Increment completed count and update overall progress
|
|
76
|
+
def increment_completed
|
|
77
|
+
@mutex.synchronize do
|
|
78
|
+
@completed_imports += 1
|
|
79
|
+
update_overall_line if @tty && @has_overall_line
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Acquire a slot for a new task
|
|
84
|
+
# @return [SlotProgress] A progress reporter for this slot
|
|
85
|
+
def acquire_slot(context)
|
|
86
|
+
slot_id = @slot_queue.pop
|
|
87
|
+
slot = SlotProgress.new(self, slot_id, context)
|
|
88
|
+
|
|
89
|
+
@mutex.synchronize do
|
|
90
|
+
@slots[slot_id] = slot
|
|
91
|
+
# Slot lines start after the overall progress line (if present)
|
|
92
|
+
effective_line = @has_overall_line ? slot_id + 1 : slot_id
|
|
93
|
+
if @tty && effective_line >= @lines_printed
|
|
94
|
+
# Print empty lines to reserve space
|
|
95
|
+
(@lines_printed..effective_line).each { @output.puts }
|
|
96
|
+
@lines_printed = effective_line + 1
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
slot
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Release a slot back to the pool
|
|
104
|
+
def release_slot(slot_id)
|
|
105
|
+
@mutex.synchronize do
|
|
106
|
+
@slots.delete(slot_id)
|
|
107
|
+
end
|
|
108
|
+
@slot_queue << slot_id
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Update a specific slot's display
|
|
112
|
+
def update_slot(slot_id, context, message, current: nil, total: nil, color: nil)
|
|
113
|
+
line = build_line(context, message, current, total, color: color)
|
|
114
|
+
|
|
115
|
+
@mutex.synchronize do
|
|
116
|
+
if @tty
|
|
117
|
+
# Move cursor to slot line and update (account for overall progress line)
|
|
118
|
+
effective_line = @has_overall_line ? slot_id + 1 : slot_id
|
|
119
|
+
lines_up = @lines_printed - effective_line
|
|
120
|
+
@output.print "\e[#{lines_up}A" # Move up
|
|
121
|
+
@output.print "\e[2K" # Clear line
|
|
122
|
+
@output.print line
|
|
123
|
+
@output.print "\e[#{lines_up}B" # Move back down
|
|
124
|
+
@output.print "\r" # Return to start of line
|
|
125
|
+
@output.flush
|
|
126
|
+
else
|
|
127
|
+
@output.puts line
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Mark a slot as complete
|
|
133
|
+
def complete_slot(slot_id, context, message = nil)
|
|
134
|
+
@mutex.synchronize do
|
|
135
|
+
if @tty
|
|
136
|
+
effective_line = @has_overall_line ? slot_id + 1 : slot_id
|
|
137
|
+
lines_up = @lines_printed - effective_line
|
|
138
|
+
@output.print "\e[#{lines_up}A"
|
|
139
|
+
@output.print "\e[2K"
|
|
140
|
+
msg = message || "Done"
|
|
141
|
+
@output.print "#{COLORS[:bold]}#{context}#{COLORS[:reset]} - #{COLORS[:green]}#{msg}#{COLORS[:reset]}"
|
|
142
|
+
@output.print "\e[#{lines_up}B"
|
|
143
|
+
@output.print "\r"
|
|
144
|
+
@output.flush
|
|
145
|
+
elsif message
|
|
146
|
+
@output.puts "#{context} - #{message}"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Report an error for a slot
|
|
152
|
+
def error_slot(slot_id, context, message)
|
|
153
|
+
safe_message = sanitize_message(message)
|
|
154
|
+
@mutex.synchronize do
|
|
155
|
+
if @tty
|
|
156
|
+
effective_line = @has_overall_line ? slot_id + 1 : slot_id
|
|
157
|
+
lines_up = @lines_printed - effective_line
|
|
158
|
+
@output.print "\e[#{lines_up}A"
|
|
159
|
+
@output.print "\e[2K"
|
|
160
|
+
@output.print "#{COLORS[:bold]}#{context}#{COLORS[:reset]} - #{COLORS[:red]}Error: #{safe_message}#{COLORS[:reset]}"
|
|
161
|
+
@output.print "\e[#{lines_up}B"
|
|
162
|
+
@output.print "\r"
|
|
163
|
+
@output.flush
|
|
164
|
+
else
|
|
165
|
+
@output.puts "#{context} - Error: #{safe_message}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Sanitize message to prevent breaking TTY display (remove newlines, truncate)
|
|
171
|
+
def sanitize_message(message)
|
|
172
|
+
return "" if message.nil?
|
|
173
|
+
|
|
174
|
+
# Replace newlines with spaces and collapse multiple spaces
|
|
175
|
+
clean = message.to_s.gsub(/[\r\n]+/, " ").gsub(/\s+/, " ").strip
|
|
176
|
+
# Truncate if too long
|
|
177
|
+
clean.length > 80 ? "#{clean[0, 77]}..." : clean
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Print a final summary (restores cursor and shows it)
|
|
181
|
+
# Note: Use finish_from_trap when called from a signal handler
|
|
182
|
+
def finish(message)
|
|
183
|
+
@mutex.synchronize do
|
|
184
|
+
finish_unsafe(message)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Trap-safe version of finish (no mutex, safe to call from signal handlers)
|
|
189
|
+
def finish_from_trap(message)
|
|
190
|
+
finish_unsafe(message)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Show interrupt message without clearing progress (called on first Ctrl-C)
|
|
194
|
+
# Note: Use interrupt_from_trap when called from a signal handler
|
|
195
|
+
def interrupt(message)
|
|
196
|
+
@mutex.synchronize do
|
|
197
|
+
interrupt_unsafe(message)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Trap-safe version of interrupt (no mutex, safe to call from signal handlers)
|
|
202
|
+
def interrupt_from_trap(message)
|
|
203
|
+
interrupt_unsafe(message)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def finish_unsafe(message)
|
|
209
|
+
if @tty
|
|
210
|
+
# Restore cursor position and show cursor
|
|
211
|
+
@output.print CURSOR_RESTORE
|
|
212
|
+
# Clear from cursor to end of screen to remove progress lines
|
|
213
|
+
@output.print "\e[J"
|
|
214
|
+
@output.print CURSOR_SHOW
|
|
215
|
+
end
|
|
216
|
+
@output.puts message if message
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def interrupt_unsafe(message)
|
|
220
|
+
if @tty
|
|
221
|
+
# Move to the line below progress and print message
|
|
222
|
+
@output.print "\n"
|
|
223
|
+
@output.print "\e[2K"
|
|
224
|
+
@output.print "#{COLORS[:yellow]}#{message}#{COLORS[:reset]}"
|
|
225
|
+
@output.print "\n"
|
|
226
|
+
@output.flush
|
|
227
|
+
# Increase lines printed to account for the interrupt message
|
|
228
|
+
@lines_printed += 2
|
|
229
|
+
else
|
|
230
|
+
@output.puts message
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def build_overall_line
|
|
235
|
+
percentage = @total_imports.positive? ? ((@completed_imports.to_f / @total_imports) * 100).round : 0
|
|
236
|
+
progress_bar = build_progress_bar(percentage)
|
|
237
|
+
eta_str = calculate_eta_string
|
|
238
|
+
if @tty
|
|
239
|
+
"#{COLORS[:bold]}#{COLORS[:magenta]}Overall#{COLORS[:reset]} #{progress_bar} " \
|
|
240
|
+
"#{COLORS[:cyan]}#{percentage}%#{COLORS[:reset]} " \
|
|
241
|
+
"[#{@completed_imports}/#{@total_imports}] " \
|
|
242
|
+
"#{COLORS[:dim]}#{eta_str}#{COLORS[:reset]}"
|
|
243
|
+
else
|
|
244
|
+
"Overall: [#{@completed_imports}/#{@total_imports}] #{percentage}% #{eta_str}"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
def calculate_eta_string
|
|
249
|
+
return "ETA: --:--" if @start_time.nil? || @completed_imports.zero?
|
|
250
|
+
|
|
251
|
+
elapsed = Time.now - @start_time
|
|
252
|
+
remaining = @total_imports - @completed_imports
|
|
253
|
+
rate = @completed_imports.to_f / elapsed
|
|
254
|
+
eta_seconds = (remaining / rate).round
|
|
255
|
+
|
|
256
|
+
if eta_seconds < 60
|
|
257
|
+
"ETA: #{eta_seconds}s"
|
|
258
|
+
elsif eta_seconds < 3600
|
|
259
|
+
minutes = eta_seconds / 60
|
|
260
|
+
seconds = eta_seconds % 60
|
|
261
|
+
format("ETA: %d:%02d", minutes, seconds)
|
|
262
|
+
else
|
|
263
|
+
hours = eta_seconds / 3600
|
|
264
|
+
minutes = (eta_seconds % 3600) / 60
|
|
265
|
+
format("ETA: %d:%02d:%02d", hours, minutes, eta_seconds % 60)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def build_progress_bar(percentage)
|
|
270
|
+
width = 20
|
|
271
|
+
filled = [(percentage / 5.0).round, width].min
|
|
272
|
+
filled = [filled, 0].max
|
|
273
|
+
empty = width - filled
|
|
274
|
+
bar = "#{"█" * filled}#{"░" * empty}"
|
|
275
|
+
"#{COLORS[:green]}#{bar}#{COLORS[:reset]}"
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def update_overall_line
|
|
279
|
+
# Move to first line (overall progress line), update, move back
|
|
280
|
+
lines_up = @lines_printed
|
|
281
|
+
@output.print "\e[#{lines_up}A"
|
|
282
|
+
@output.print "\e[2K"
|
|
283
|
+
@output.print build_overall_line
|
|
284
|
+
@output.print "\e[#{lines_up}B"
|
|
285
|
+
@output.print "\r"
|
|
286
|
+
@output.flush
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
def build_line(context, message, current, total, color: nil)
|
|
290
|
+
parts = []
|
|
291
|
+
if @tty
|
|
292
|
+
parts << "#{COLORS[:bold]}#{context}#{COLORS[:reset]}"
|
|
293
|
+
parts << "#{COLORS[:cyan]}#{progress_indicator(current, total)}#{COLORS[:reset]}" if current && total
|
|
294
|
+
msg_color = color || COLORS[:reset]
|
|
295
|
+
parts << "#{msg_color}#{message}#{COLORS[:reset]}"
|
|
296
|
+
else
|
|
297
|
+
parts << context
|
|
298
|
+
parts << progress_indicator(current, total) if current && total
|
|
299
|
+
parts << message
|
|
300
|
+
end
|
|
301
|
+
parts.join(" - ")
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def progress_indicator(current, total)
|
|
305
|
+
percentage = ((current.to_f / total) * 100).round
|
|
306
|
+
"[#{current}/#{total} #{percentage}%]"
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Individual slot progress reporter
|
|
310
|
+
class SlotProgress
|
|
311
|
+
attr_reader :slot_id
|
|
312
|
+
attr_accessor :context
|
|
313
|
+
|
|
314
|
+
def initialize(parent, slot_id, context)
|
|
315
|
+
@parent = parent
|
|
316
|
+
@slot_id = slot_id
|
|
317
|
+
@context = context
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def update(message, current: nil, total: nil)
|
|
321
|
+
@parent.update_slot(@slot_id, @context, message, current: current, total: total)
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def complete(message = nil)
|
|
325
|
+
@parent.complete_slot(@slot_id, @context, message)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
def error(message)
|
|
329
|
+
@parent.error_slot(@slot_id, @context, message)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def warn(message)
|
|
333
|
+
# Warnings are shown inline with the current context (yellow in TTY mode)
|
|
334
|
+
@parent.update_slot(@slot_id, @context, "Warning: #{message}", color: COLORS[:yellow])
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def release
|
|
338
|
+
@parent.release_slot(@slot_id)
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
end
|