archsight 0.1.2 → 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/README.md +26 -5
- 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 +163 -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 +1 -0
- 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
data/lib/archsight/cli.rb
CHANGED
|
@@ -4,6 +4,10 @@ require "thor"
|
|
|
4
4
|
|
|
5
5
|
module Archsight
|
|
6
6
|
class CLI < Thor
|
|
7
|
+
def self.exit_on_failure?
|
|
8
|
+
true
|
|
9
|
+
end
|
|
10
|
+
|
|
7
11
|
class_option :resources,
|
|
8
12
|
aliases: "-r",
|
|
9
13
|
type: :string,
|
|
@@ -12,11 +16,21 @@ module Archsight
|
|
|
12
16
|
desc "web", "Start the web server"
|
|
13
17
|
option :port, aliases: "-p", type: :numeric, default: 4567, desc: "Port to listen on"
|
|
14
18
|
option :host, aliases: "-H", type: :string, default: "localhost", desc: "Host to bind to"
|
|
19
|
+
option :production, type: :boolean, default: false, desc: "Run in production mode"
|
|
20
|
+
option :disable_reload, type: :boolean, default: false, desc: "Disable the reload button in the UI"
|
|
21
|
+
option :enable_logging, type: :boolean, default: nil, desc: "Enable request logging (default: false in dev, true in prod)"
|
|
15
22
|
def web
|
|
16
23
|
configure_resources
|
|
17
24
|
require "archsight/web/application"
|
|
25
|
+
|
|
26
|
+
env = options[:production] ? :production : :development
|
|
27
|
+
Archsight::Web::Application.configure_environment!(env, logging: options[:enable_logging])
|
|
28
|
+
Archsight::Web::Application.set :reload_enabled, !options[:disable_reload]
|
|
18
29
|
Archsight::Web::Application.setup_mcp!
|
|
19
30
|
Archsight::Web::Application.run!(port: options[:port], bind: options[:host])
|
|
31
|
+
rescue Archsight::ResourceError => e
|
|
32
|
+
display_error_with_context(e.to_s)
|
|
33
|
+
exit 1
|
|
20
34
|
end
|
|
21
35
|
|
|
22
36
|
desc "lint", "Validate architecture resources"
|
|
@@ -71,6 +85,80 @@ module Archsight
|
|
|
71
85
|
binding.irb
|
|
72
86
|
end
|
|
73
87
|
|
|
88
|
+
desc "import", "Execute pending imports"
|
|
89
|
+
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Verbose output"
|
|
90
|
+
option :dry_run, aliases: "-n", type: :boolean, default: false, desc: "Show execution plan without running"
|
|
91
|
+
option :filter, aliases: "-f", type: :string, desc: "Filter imports by name (regex pattern)"
|
|
92
|
+
option :force, aliases: "-F", type: :boolean, default: false, desc: "Ignore cache and re-run all imports"
|
|
93
|
+
def import
|
|
94
|
+
configure_resources
|
|
95
|
+
require "archsight/database"
|
|
96
|
+
require "archsight/import/executor"
|
|
97
|
+
|
|
98
|
+
resources_dir = Archsight.resources_dir
|
|
99
|
+
|
|
100
|
+
# Load all handlers
|
|
101
|
+
require_import_handlers
|
|
102
|
+
|
|
103
|
+
# Create database that loads from resources directory
|
|
104
|
+
# Only load Import resources to avoid validation errors on incomplete resources
|
|
105
|
+
db = Archsight::Database.new(resources_dir, verbose: options[:verbose], only_kinds: ["Import"], verify: false)
|
|
106
|
+
|
|
107
|
+
if options[:dry_run]
|
|
108
|
+
puts "Execution Plan:"
|
|
109
|
+
executor = Archsight::Import::Executor.new(
|
|
110
|
+
database: db,
|
|
111
|
+
resources_dir: resources_dir,
|
|
112
|
+
verbose: true,
|
|
113
|
+
filter: options[:filter],
|
|
114
|
+
force: options[:force]
|
|
115
|
+
)
|
|
116
|
+
executor.execution_plan
|
|
117
|
+
else
|
|
118
|
+
executor = Archsight::Import::Executor.new(
|
|
119
|
+
database: db,
|
|
120
|
+
resources_dir: resources_dir,
|
|
121
|
+
verbose: options[:verbose],
|
|
122
|
+
filter: options[:filter],
|
|
123
|
+
force: options[:force]
|
|
124
|
+
)
|
|
125
|
+
executor.run!
|
|
126
|
+
puts "All imports completed successfully."
|
|
127
|
+
end
|
|
128
|
+
rescue Archsight::Import::InterruptedError
|
|
129
|
+
# Graceful shutdown already handled by executor
|
|
130
|
+
exit 130
|
|
131
|
+
rescue Archsight::Import::DeadlockError => e
|
|
132
|
+
puts "Error: #{e.message}"
|
|
133
|
+
exit 1
|
|
134
|
+
rescue Archsight::Import::ImportError => e
|
|
135
|
+
puts "Error: #{e.message}"
|
|
136
|
+
exit 1
|
|
137
|
+
rescue Archsight::ResourceError => e
|
|
138
|
+
display_error_with_context(e.to_s)
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
desc "analyze", "Execute analysis scripts"
|
|
143
|
+
option :verbose, aliases: "-v", type: :boolean, default: false, desc: "Verbose output"
|
|
144
|
+
option :dry_run, aliases: "-n", type: :boolean, default: false, desc: "List analyses without running"
|
|
145
|
+
option :filter, aliases: "-f", type: :string, desc: "Filter analyses by name (regex pattern)"
|
|
146
|
+
def analyze
|
|
147
|
+
configure_resources
|
|
148
|
+
require "archsight/database"
|
|
149
|
+
require "archsight/analysis"
|
|
150
|
+
|
|
151
|
+
db = load_database_for_analysis
|
|
152
|
+
analyses = filter_analyses(db)
|
|
153
|
+
|
|
154
|
+
return puts("No analyses found#{" matching '#{options[:filter]}'" if options[:filter]}.") if analyses.empty?
|
|
155
|
+
return print_analysis_dry_run(analyses) if options[:dry_run]
|
|
156
|
+
|
|
157
|
+
results = execute_analyses(db, analyses)
|
|
158
|
+
print_analysis_results(results)
|
|
159
|
+
exit 1 if results.any?(&:failed?)
|
|
160
|
+
end
|
|
161
|
+
|
|
74
162
|
desc "version", "Show version"
|
|
75
163
|
def version
|
|
76
164
|
puts "archsight #{Archsight::VERSION}"
|
|
@@ -84,6 +172,81 @@ module Archsight
|
|
|
84
172
|
Archsight.resources_dir = options[:resources] if options[:resources]
|
|
85
173
|
end
|
|
86
174
|
|
|
175
|
+
def require_import_handlers
|
|
176
|
+
handlers_dir = File.expand_path("import/handlers", __dir__)
|
|
177
|
+
Dir.glob(File.join(handlers_dir, "*.rb")).each do |handler_file|
|
|
178
|
+
require handler_file
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def load_database_for_analysis
|
|
183
|
+
db = Archsight::Database.new(Archsight.resources_dir, verbose: options[:verbose])
|
|
184
|
+
db.reload!
|
|
185
|
+
db
|
|
186
|
+
rescue Archsight::ResourceError => e
|
|
187
|
+
display_error_with_context(e.to_s)
|
|
188
|
+
exit 1
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def filter_analyses(db)
|
|
192
|
+
analyses = db.instances_by_kind("Analysis").values
|
|
193
|
+
analyses = analyses.select { |a| Regexp.new(options[:filter], Regexp::IGNORECASE).match?(a.name) } if options[:filter]
|
|
194
|
+
analyses.reject { |a| a.annotations["analysis/enabled"] == "false" }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def print_analysis_dry_run(analyses)
|
|
198
|
+
puts "Analyses to run#{" (filter: #{options[:filter]})" if options[:filter]}:"
|
|
199
|
+
analyses.sort_by(&:name).each_with_index do |analysis, idx|
|
|
200
|
+
timeout = analysis.annotations["analysis/timeout"] || "30s"
|
|
201
|
+
desc = analysis.annotations["analysis/description"] || "(no description)"
|
|
202
|
+
puts " #{idx + 1}. #{analysis.name} [#{timeout}]"
|
|
203
|
+
puts " #{desc}"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def execute_analyses(db, analyses)
|
|
208
|
+
executor = Archsight::Analysis::Executor.new(db)
|
|
209
|
+
analyses.map { |analysis| executor.execute(analysis) }
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def print_analysis_results(results)
|
|
213
|
+
require "tty-markdown"
|
|
214
|
+
|
|
215
|
+
results.each do |result|
|
|
216
|
+
print_single_result(result)
|
|
217
|
+
puts ""
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
summary_md = build_analysis_summary_markdown(results)
|
|
221
|
+
puts TTY::Markdown.parse(summary_md)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def print_single_result(result)
|
|
225
|
+
# Print status header
|
|
226
|
+
header = "# #{result.status_emoji} #{result.name}"
|
|
227
|
+
header += " (#{result.duration_str})" unless result.duration_str.empty?
|
|
228
|
+
puts TTY::Markdown.parse(header)
|
|
229
|
+
|
|
230
|
+
# Print error if failed
|
|
231
|
+
puts TTY::Markdown.parse(result.error_markdown(verbose: options[:verbose])) if result.failed?
|
|
232
|
+
|
|
233
|
+
# Print script output
|
|
234
|
+
output = result.to_s(verbose: options[:verbose])
|
|
235
|
+
puts output unless output.empty?
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def build_analysis_summary_markdown(results)
|
|
239
|
+
passed = results.count(&:success?)
|
|
240
|
+
failed = results.count(&:failed?)
|
|
241
|
+
with_findings = results.count(&:has_findings?)
|
|
242
|
+
|
|
243
|
+
lines = ["---", "", "# Summary", ""]
|
|
244
|
+
lines << "- ✅ **#{passed}** passed"
|
|
245
|
+
lines << "- ❌ **#{failed}** failed" if failed.positive?
|
|
246
|
+
lines << "- ⚠️ **#{with_findings}** with findings" if with_findings.positive?
|
|
247
|
+
lines.join("\n")
|
|
248
|
+
end
|
|
249
|
+
|
|
87
250
|
def list_kinds
|
|
88
251
|
puts "Available resource kinds:\n\n"
|
|
89
252
|
Archsight::Resources.resource_classes.each_key { |kind| puts " - #{kind}" }
|
data/lib/archsight/database.rb
CHANGED
|
@@ -43,13 +43,14 @@ module Archsight
|
|
|
43
43
|
# of the structure. The loading and parsing of files will raise errors
|
|
44
44
|
# if invalid data is passed.
|
|
45
45
|
class Database
|
|
46
|
-
attr_accessor :instances, :verbose, :verify, :compute_annotations
|
|
46
|
+
attr_accessor :instances, :verbose, :verify, :compute_annotations, :only_kinds
|
|
47
47
|
|
|
48
|
-
def initialize(path, verbose: false, verify: true, compute_annotations: true)
|
|
48
|
+
def initialize(path, verbose: false, verify: true, compute_annotations: true, only_kinds: nil)
|
|
49
49
|
@path = path
|
|
50
50
|
@verbose = verbose
|
|
51
51
|
@verify = verify
|
|
52
52
|
@compute_annotations = compute_annotations
|
|
53
|
+
@only_kinds = only_kinds
|
|
53
54
|
@instances = {}
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -127,6 +128,9 @@ module Archsight
|
|
|
127
128
|
obj = node.to_ruby
|
|
128
129
|
next unless obj # skip empty / unknown documents
|
|
129
130
|
|
|
131
|
+
# Skip resources that don't match only_kinds filter
|
|
132
|
+
next if @only_kinds && !@only_kinds.include?(obj["kind"])
|
|
133
|
+
|
|
130
134
|
self << create_valid_instance(obj)
|
|
131
135
|
end
|
|
132
136
|
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack/utils"
|
|
4
|
+
|
|
5
|
+
module Archsight
|
|
6
|
+
module Helpers
|
|
7
|
+
# AnalysisRenderer provides HTML rendering for analysis result sections
|
|
8
|
+
module AnalysisRenderer
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
# HTML escape helper
|
|
12
|
+
def escape_html(text)
|
|
13
|
+
Rack::Utils.escape_html(text.to_s)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Render a single analysis section to HTML
|
|
17
|
+
# @param section [Hash] Section data with :type and content
|
|
18
|
+
# @param markdown_renderer [Proc, nil] Optional proc to render markdown
|
|
19
|
+
def render_analysis_section(section, markdown_renderer: nil)
|
|
20
|
+
case section[:type]
|
|
21
|
+
when :heading
|
|
22
|
+
render_heading(section)
|
|
23
|
+
when :text
|
|
24
|
+
render_text(section, markdown_renderer)
|
|
25
|
+
when :message
|
|
26
|
+
render_message(section)
|
|
27
|
+
when :table
|
|
28
|
+
render_table(section)
|
|
29
|
+
when :list
|
|
30
|
+
render_list(section)
|
|
31
|
+
when :code
|
|
32
|
+
render_code(section)
|
|
33
|
+
else
|
|
34
|
+
""
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Render analysis table section to HTML
|
|
39
|
+
def render_analysis_table(section)
|
|
40
|
+
render_table(section)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Private rendering methods
|
|
44
|
+
|
|
45
|
+
def render_heading(section)
|
|
46
|
+
%(<div class="analysis-heading level-#{section[:level]}">#{escape_html(section[:text])}</div>)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def render_text(section, markdown_renderer)
|
|
50
|
+
content = markdown_renderer ? markdown_renderer.call(section[:content]) : escape_html(section[:content])
|
|
51
|
+
%(<div class="analysis-text">#{content}</div>)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def render_message(section)
|
|
55
|
+
icon = case section[:level]
|
|
56
|
+
when :error then "xmark-circle"
|
|
57
|
+
when :warning then "warning-triangle"
|
|
58
|
+
else "info-circle"
|
|
59
|
+
end
|
|
60
|
+
%(<div class="analysis-message message-#{section[:level]}"><i class="iconoir-#{icon}"></i> #{escape_html(section[:message])}</div>)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_table(section)
|
|
64
|
+
headers = section[:headers].map { |h| "<th>#{escape_html(h)}</th>" }.join
|
|
65
|
+
rows = section[:rows].map do |row|
|
|
66
|
+
cells = row.map { |cell| "<td>#{escape_html(cell.to_s)}</td>" }.join
|
|
67
|
+
"<tr>#{cells}</tr>"
|
|
68
|
+
end.join
|
|
69
|
+
%(<div class="analysis-table-wrapper"><table><thead><tr>#{headers}</tr></thead><tbody>#{rows}</tbody></table></div>)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def render_list(section)
|
|
73
|
+
items = section[:items].map { |item| "<li>#{escape_html(item)}</li>" }.join
|
|
74
|
+
%(<ul class="analysis-list">#{items}</ul>)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def render_code(section)
|
|
78
|
+
lang_class = section[:lang] ? " class=\"language-#{section[:lang]}\"" : ""
|
|
79
|
+
%(<pre class="code"><code#{lang_class}>#{escape_html(section[:content])}</code></pre>)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -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
|