archsight 0.1.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.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +24 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/CONTRIBUTING.md +186 -0
  5. data/Dockerfile +39 -0
  6. data/LICENSE.txt +201 -0
  7. data/README.md +170 -0
  8. data/SECURITY.md +27 -0
  9. data/exe/archsight +9 -0
  10. data/lib/archsight/annotations/aggregators.rb +109 -0
  11. data/lib/archsight/annotations/annotation.rb +168 -0
  12. data/lib/archsight/annotations/architecture_annotations.rb +59 -0
  13. data/lib/archsight/annotations/backup_annotations.rb +21 -0
  14. data/lib/archsight/annotations/computed.rb +264 -0
  15. data/lib/archsight/annotations/email_recipient.rb +35 -0
  16. data/lib/archsight/annotations/generated_annotations.rb +17 -0
  17. data/lib/archsight/annotations/git_annotations.rb +21 -0
  18. data/lib/archsight/annotations/relation_resolver.rb +160 -0
  19. data/lib/archsight/cli.rb +120 -0
  20. data/lib/archsight/configuration.rb +36 -0
  21. data/lib/archsight/database.rb +183 -0
  22. data/lib/archsight/documentation.rb +171 -0
  23. data/lib/archsight/graph.rb +113 -0
  24. data/lib/archsight/helpers.rb +210 -0
  25. data/lib/archsight/linter.rb +77 -0
  26. data/lib/archsight/mcp/analyze_resource_tool.rb +222 -0
  27. data/lib/archsight/mcp/base.rb +48 -0
  28. data/lib/archsight/mcp/query_tool.rb +113 -0
  29. data/lib/archsight/mcp/resource_doc_tool.rb +87 -0
  30. data/lib/archsight/mcp.rb +6 -0
  31. data/lib/archsight/query/ast.rb +279 -0
  32. data/lib/archsight/query/errors.rb +39 -0
  33. data/lib/archsight/query/evaluator.rb +707 -0
  34. data/lib/archsight/query/lexer.rb +289 -0
  35. data/lib/archsight/query/parser.rb +506 -0
  36. data/lib/archsight/query.rb +68 -0
  37. data/lib/archsight/renderer.rb +134 -0
  38. data/lib/archsight/resources/application_component.rb +346 -0
  39. data/lib/archsight/resources/application_interface.rb +54 -0
  40. data/lib/archsight/resources/application_service.rb +222 -0
  41. data/lib/archsight/resources/base.rb +300 -0
  42. data/lib/archsight/resources/business_actor.rb +195 -0
  43. data/lib/archsight/resources/business_constraint.rb +32 -0
  44. data/lib/archsight/resources/business_process.rb +37 -0
  45. data/lib/archsight/resources/business_product.rb +206 -0
  46. data/lib/archsight/resources/business_requirement.rb +56 -0
  47. data/lib/archsight/resources/compliance_evidence.rb +42 -0
  48. data/lib/archsight/resources/data_object.rb +49 -0
  49. data/lib/archsight/resources/motivation_goal.rb +37 -0
  50. data/lib/archsight/resources/motivation_outcome.rb +33 -0
  51. data/lib/archsight/resources/motivation_stakeholder.rb +38 -0
  52. data/lib/archsight/resources/strategy_capability.rb +38 -0
  53. data/lib/archsight/resources/technology_artifact.rb +154 -0
  54. data/lib/archsight/resources/technology_interface.rb +34 -0
  55. data/lib/archsight/resources/technology_node.rb +42 -0
  56. data/lib/archsight/resources/technology_service.rb +35 -0
  57. data/lib/archsight/resources/technology_system_software.rb +37 -0
  58. data/lib/archsight/resources/view.rb +51 -0
  59. data/lib/archsight/resources.rb +49 -0
  60. data/lib/archsight/template.rb +49 -0
  61. data/lib/archsight/version.rb +5 -0
  62. data/lib/archsight/web/application.rb +290 -0
  63. data/lib/archsight/web/doc/archimate.md +215 -0
  64. data/lib/archsight/web/doc/computed_annotations.md +316 -0
  65. data/lib/archsight/web/doc/icons.md +303 -0
  66. data/lib/archsight/web/doc/index.md.erb +74 -0
  67. data/lib/archsight/web/doc/modeling.md +200 -0
  68. data/lib/archsight/web/doc/search.md +227 -0
  69. data/lib/archsight/web/doc/togaf.md +255 -0
  70. data/lib/archsight/web/doc/tool.md +90 -0
  71. data/lib/archsight/web/public/css/artifact.css +985 -0
  72. data/lib/archsight/web/public/css/base.css +201 -0
  73. data/lib/archsight/web/public/css/graph.css +106 -0
  74. data/lib/archsight/web/public/css/highlight.min.css +10 -0
  75. data/lib/archsight/web/public/css/iconoir.css +22 -0
  76. data/lib/archsight/web/public/css/instance.css +329 -0
  77. data/lib/archsight/web/public/css/layout.css +421 -0
  78. data/lib/archsight/web/public/css/mermaid-layers.css +188 -0
  79. data/lib/archsight/web/public/css/pico.min.css +4 -0
  80. data/lib/archsight/web/public/favicon.ico +0 -0
  81. data/lib/archsight/web/public/img/archimate.png +0 -0
  82. data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
  83. data/lib/archsight/web/public/js/graph-zoom.js +18 -0
  84. data/lib/archsight/web/public/js/highlight.min.js +3899 -0
  85. data/lib/archsight/web/public/js/htmx.min.js +1 -0
  86. data/lib/archsight/web/public/js/mermaid-init.js +88 -0
  87. data/lib/archsight/web/public/js/mermaid.min.js +2811 -0
  88. data/lib/archsight/web/public/js/sparkline.js +42 -0
  89. data/lib/archsight/web/public/js/svg-pan-zoom.min.js +3 -0
  90. data/lib/archsight/web/public/js/svg-zoom-controls.js +93 -0
  91. data/lib/archsight/web/views/index.haml +12 -0
  92. data/lib/archsight/web/views/partials/artifact/_activity.haml +55 -0
  93. data/lib/archsight/web/views/partials/artifact/_agentic.haml +25 -0
  94. data/lib/archsight/web/views/partials/artifact/_deployment.haml +29 -0
  95. data/lib/archsight/web/views/partials/artifact/_git_info.haml +16 -0
  96. data/lib/archsight/web/views/partials/artifact/_language_stats.haml +53 -0
  97. data/lib/archsight/web/views/partials/artifact/_links.haml +24 -0
  98. data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +26 -0
  99. data/lib/archsight/web/views/partials/artifact/_repositories.haml +55 -0
  100. data/lib/archsight/web/views/partials/artifact/_team.haml +83 -0
  101. data/lib/archsight/web/views/partials/artifact/_workflow.haml +69 -0
  102. data/lib/archsight/web/views/partials/components/_activity.haml +37 -0
  103. data/lib/archsight/web/views/partials/components/_git.haml +17 -0
  104. data/lib/archsight/web/views/partials/components/_jira.haml +18 -0
  105. data/lib/archsight/web/views/partials/components/_languages.haml +29 -0
  106. data/lib/archsight/web/views/partials/components/_owner.haml +15 -0
  107. data/lib/archsight/web/views/partials/components/_repositories.haml +37 -0
  108. data/lib/archsight/web/views/partials/components/_status.haml +23 -0
  109. data/lib/archsight/web/views/partials/instance/_detail.haml +99 -0
  110. data/lib/archsight/web/views/partials/instance/_graph.haml +6 -0
  111. data/lib/archsight/web/views/partials/instance/_list.haml +84 -0
  112. data/lib/archsight/web/views/partials/instance/_relations.haml +43 -0
  113. data/lib/archsight/web/views/partials/instance/_requirements.haml +41 -0
  114. data/lib/archsight/web/views/partials/instance/_view_detail.haml +57 -0
  115. data/lib/archsight/web/views/partials/layout/_content.haml +40 -0
  116. data/lib/archsight/web/views/partials/layout/_error.haml +22 -0
  117. data/lib/archsight/web/views/partials/layout/_head.haml +24 -0
  118. data/lib/archsight/web/views/partials/layout/_navigation.haml +20 -0
  119. data/lib/archsight/web/views/partials/layout/_sidebar.haml +27 -0
  120. data/lib/archsight/web/views/search.haml +53 -0
  121. data/lib/archsight.rb +17 -0
  122. metadata +311 -0
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TechnologyInterface is the backing of an applicationInterface
4
+ class Archsight::Resources::TechnologyInterface < Archsight::Resources::Base
5
+ include_annotations :git, :architecture
6
+
7
+ description <<~MD
8
+ Represents a point of access where technology services are made available.
9
+
10
+ ## ArchiMate Definition
11
+
12
+ **Layer:** Technology
13
+ **Aspect:** Active Structure (external)
14
+
15
+ A technology interface represents a point of access where technology services offered
16
+ by a node can be accessed. It provides the technical implementation backing for
17
+ application interfaces.
18
+
19
+ ## Usage
20
+
21
+ Use TechnologyInterface to represent:
22
+
23
+ - Network endpoints (IP:port combinations)
24
+ - Protocol bindings (HTTP, gRPC, AMQP)
25
+ - Load balancer virtual IPs
26
+ - Service mesh endpoints
27
+ - DNS entries
28
+ MD
29
+
30
+ icon "data-transfer-both"
31
+ layer "technology"
32
+
33
+ relation :suppliedBy, :technologyComponents, :TechnologyService
34
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TechnologyNode represents physical infrastructure (VMs, servers, Kubernetes nodes)
4
+ class Archsight::Resources::TechnologyNode < Archsight::Resources::Base
5
+ include_annotations :git, :architecture
6
+
7
+ description <<~MD
8
+ Represents physical infrastructure hosting application components.
9
+
10
+ ## ArchiMate Definition
11
+
12
+ **Layer:** Technology
13
+ **Aspect:** Active Structure
14
+
15
+ A node represents a computational or physical resource that hosts, manipulates, or
16
+ interacts with other computational or physical resources. In cloud contexts, this
17
+ includes compute instances, storage systems, and network equipment.
18
+
19
+ ## Usage
20
+
21
+ Use TechnologyNode to represent:
22
+
23
+ - Virtual machines
24
+ - Bare metal servers
25
+ - Kubernetes nodes
26
+ - Network appliances
27
+ - Storage arrays
28
+ MD
29
+
30
+ icon "server-connection"
31
+ layer "technology"
32
+
33
+ annotation "infrastructure/type",
34
+ description: "Type of infrastructure node",
35
+ title: "Infrastructure Type",
36
+ enum: %w[vm bare-metal kubernetes-node network-appliance storage-array],
37
+ list: true
38
+
39
+ relation :realizes, :businessConstraints, :BusinessConstraint
40
+ relation :servedBy, :technologyServices, :TechnologyService
41
+ relation :servedBy, :businessActors, :BusinessActor
42
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TechnologyService supports the deployment of the application on a high level
4
+ class Archsight::Resources::TechnologyService < Archsight::Resources::Base
5
+ include_annotations :git, :architecture, :generated
6
+
7
+ description <<~MD
8
+ Represents an explicitly defined piece of technology functionality.
9
+
10
+ ## ArchiMate Definition
11
+
12
+ **Layer:** Technology
13
+ **Aspect:** Behavior
14
+
15
+ A technology service represents an explicitly defined piece of functionality exposed
16
+ by technology nodes. It provides platform-level capabilities that support the deployment
17
+ and operation of application components.
18
+
19
+ ## Usage
20
+
21
+ Use TechnologyService to represent:
22
+
23
+ - Cloud platform services (AWS EC2, Azure VMs)
24
+ - Container orchestration services
25
+ - Managed database services
26
+ - CI/CD pipeline services
27
+ - Monitoring and logging platforms
28
+ MD
29
+
30
+ icon "cloud"
31
+ layer "technology"
32
+
33
+ relation :suppliedBy, :technologyComponents, :TechnologySystemSoftware
34
+ relation :servedBy, :businessActors, :BusinessActor
35
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # TechnologySystemSoftware serves the TechnologySystemSoftware
4
+ class Archsight::Resources::TechnologySystemSoftware < Archsight::Resources::Base
5
+ include_annotations :git, :architecture
6
+
7
+ description <<~MD
8
+ Represents a logical infrastructure component that serves application components.
9
+
10
+ ## ArchiMate Definition
11
+
12
+ **Layer:** Technology
13
+ **Aspect:** Active Structure
14
+
15
+ System software represents software that provides or contributes to an environment
16
+ for storing, executing, and using software or data deployed within it. Logical
17
+ technology components represent abstract infrastructure units.
18
+
19
+ ## Usage
20
+
21
+ Use TechnologySystemSoftware to represent:
22
+
23
+ - Database clusters
24
+ - Message broker clusters
25
+ - Cache clusters
26
+ - Load balancers
27
+ - Service meshes
28
+ MD
29
+
30
+ icon "terminal-tag"
31
+ layer "technology"
32
+
33
+ relation :realizedBy, :technologyComponents, :TechnologyNode
34
+ relation :realizedThrough, :technologyArtifacts, :TechnologyArtifact
35
+ relation :exposes, :applicationInterfaces, :ApplicationInterface
36
+ relation :dependsOn, :applicationInterfaces, :ApplicationInterface
37
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # View represents a saved query with custom table display options
4
+ class Archsight::Resources::View < Archsight::Resources::Base
5
+ include_annotations :architecture
6
+
7
+ description <<~MD
8
+ Represents a saved query with customizable display options.
9
+
10
+ ## Definition
11
+
12
+ A View is a tool-specific resource type that saves a query and its display configuration.
13
+ Views allow users to create reusable perspectives on the architecture data, with custom
14
+ column selections and sorting options.
15
+
16
+ ## Usage
17
+
18
+ Use View to create:
19
+
20
+ - Compliance dashboards
21
+ - Team-specific resource lists
22
+ - Audit views
23
+ - Custom reports
24
+ - Filtered resource tables
25
+ MD
26
+
27
+ icon "view-grid"
28
+ layer "other"
29
+
30
+ annotation "view/query",
31
+ description: 'Query string to execute (e.g., "ApplicationService: backup/mode == \"none\"")',
32
+ title: "Query",
33
+ sidebar: false
34
+
35
+ annotation "view/fields",
36
+ description: "Comma-separated list of annotation fields or @components to display as columns. " \
37
+ "Components: @activity, @git, @jira, @languages, @owner, @repositories, @status",
38
+ title: "Display Fields",
39
+ sidebar: false
40
+
41
+ annotation "view/type",
42
+ description: "Display type for results",
43
+ title: "Display Type",
44
+ enum: %w[list:name list:name+kind],
45
+ sidebar: false
46
+
47
+ annotation "view/sort",
48
+ description: 'Comma-separated list of fields to sort by. Prefix with - for descending (e.g., "-scc/language/Go/loc,name"). Special fields: name, kind',
49
+ title: "Sort Fields",
50
+ sidebar: false
51
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight
4
+ # Resources contains all resources to reflect the architecture assets
5
+ module Resources
6
+ # Store the class mapping
7
+ @resource_classes = {}
8
+
9
+ # Register a resource class
10
+ def self.register(klass)
11
+ # Skip anonymous classes (used in tests)
12
+ return if klass.name.nil?
13
+
14
+ name = klass.name.split("::").last
15
+ @resource_classes[name] = klass
16
+ end
17
+
18
+ # Returns all registered resource classes
19
+ def self.resource_classes
20
+ @resource_classes
21
+ end
22
+
23
+ # Returns the class by name
24
+ def self.[](klass_name)
25
+ @resource_classes[klass_name.to_s]
26
+ end
27
+
28
+ # Iterate over all resource class names (sorted)
29
+ def self.each(&)
30
+ @resource_classes.keys.sort.each(&)
31
+ end
32
+
33
+ # Get the constant by name (for backward compatibility with const_get)
34
+ def self.const_get(name)
35
+ @resource_classes[name.to_s] || super
36
+ end
37
+ end
38
+ end
39
+
40
+ # Load dependencies after module is defined
41
+ require_relative "helpers"
42
+
43
+ # Define the Annotations namespace before loading annotation files
44
+ # (required for compact class definitions like Archsight::Annotations::Annotation)
45
+ module Archsight::Annotations; end
46
+
47
+ Dir[File.join(__dir__, "annotations", "*.rb")].each { |file| require_relative file }
48
+ require_relative "resources/base"
49
+ Dir[File.join(__dir__, "resources", "*.rb")].each { |file| require_relative file }
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require_relative "resources"
5
+
6
+ module Archsight
7
+ # Template generates YAML templates for architecture resources
8
+ class Template
9
+ def self.generate(kind_name)
10
+ klass = Archsight::Resources[kind_name.to_s]
11
+ raise "Unknown resource kind '#{kind_name}'" unless klass
12
+
13
+ yaml = {}
14
+ yaml["apiVersion"] = "architecture/v1alpha1"
15
+ yaml["kind"] = kind_name
16
+ yaml["metadata"] = {
17
+ "name" => "TODO"
18
+ }
19
+
20
+ add_annotations(yaml, klass)
21
+ add_relations(yaml, klass)
22
+
23
+ yaml.to_yaml
24
+ end
25
+
26
+ class << self
27
+ private
28
+
29
+ def add_annotations(yaml, klass)
30
+ non_pattern_annotations = klass.annotations.reject(&:pattern?)
31
+ return if non_pattern_annotations.empty?
32
+
33
+ annotations = non_pattern_annotations.to_h { |a| [a.key, a.example_value] }
34
+ yaml["metadata"]["annotations"] = annotations unless annotations.empty?
35
+ end
36
+
37
+ def add_relations(yaml, klass)
38
+ return if klass.relations.empty?
39
+
40
+ yaml["spec"] = {} if yaml["spec"].nil?
41
+ klass.relations.each do |verb, relation_kind, _relation_klass|
42
+ relation_verb = verb.to_s.delete_prefix(":")
43
+ yaml["spec"][relation_verb] ||= {}
44
+ yaml["spec"][relation_verb][relation_kind.to_s] = []
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Archsight
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,290 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sinatra/base"
4
+ require "kramdown"
5
+ require "haml"
6
+ require "erb"
7
+ require "fast_mcp"
8
+
9
+ require_relative "../database"
10
+ require_relative "../graph"
11
+ require_relative "../renderer"
12
+ require_relative "../helpers"
13
+ require_relative "../documentation"
14
+ require_relative "../query"
15
+ require_relative "../resources"
16
+ require_relative "../mcp"
17
+
18
+ # Define the Web namespace before the class definition
19
+ module Archsight::Web; end
20
+
21
+ class Archsight::Web::Application < Sinatra::Base
22
+ class << self
23
+ attr_accessor :db
24
+
25
+ def database
26
+ @database ||= Archsight::Database.new(Archsight.resources_dir).tap(&:reload!)
27
+ end
28
+
29
+ def reload!
30
+ start = Time.new
31
+ puts "== Reloading ..." if database.verbose
32
+ database.reload!
33
+ dur = (Time.new - start) * 1000
34
+ puts format("== done %0.2f ms", dur) if database.verbose
35
+ end
36
+ end
37
+
38
+ configure do
39
+ set :views, File.join(__dir__, "views")
40
+ set :public_folder, File.join(__dir__, "public")
41
+ set :haml, format: :html5
42
+ set :server, :puma
43
+ end
44
+
45
+ # MCP Server setup
46
+ def self.setup_mcp!
47
+ mcp_server = FastMcp::Server.new(
48
+ name: "Archsight MCP",
49
+ version: Archsight::VERSION
50
+ )
51
+
52
+ # Configure MCP tools with database
53
+ Archsight::MCP.db = database
54
+
55
+ mcp_server.register_tool(Archsight::MCP::QueryTool)
56
+ mcp_server.register_tool(Archsight::MCP::AnalyzeResourceTool)
57
+ mcp_server.register_tool(Archsight::MCP::ResourceDocTool)
58
+
59
+ use FastMcp::Transports::RackTransport, mcp_server, path_prefix: "/mcp"
60
+ end
61
+
62
+ helpers Archsight::GraphvisHelper, Archsight::GraphvisRenderer, Archsight::Helpers
63
+
64
+ helpers do
65
+ def db
66
+ Archsight::Web::Application.database
67
+ end
68
+
69
+ # Render markdown to HTML with optional URL resolution for repository content
70
+ # @param data [String] Markdown content
71
+ # @param git_url [String, nil] Git URL for resolving relative paths (e.g., for README images)
72
+ def markdown(data, git_url: nil)
73
+ html = Kramdown::Document.new(data, input: "GFM").to_html
74
+
75
+ # Resolve relative URLs if we have a git URL (for repository READMEs)
76
+ if git_url && (base_url = github_raw_base_url(git_url))
77
+ html = resolve_relative_urls(html, base_url)
78
+ end
79
+
80
+ # Auto-link bare URLs that aren't already inside HTML attributes or anchor tags
81
+ html = html.gsub(%r{(?<!=["'])(?<!">)(https?://[^\s<>"]+)}) do |match|
82
+ # Strip trailing punctuation that's likely sentence-ending, not part of URL
83
+ url = match.sub(/[.,;:!)]+$/, "")
84
+ trailing = match[url.length..]
85
+ %(<a href="#{url}">#{url}</a>#{trailing})
86
+ end
87
+ # Convert [[ResourceName]] wiki-style links to resource links
88
+ html.gsub(/\[\[([^\]]+)\]\]/) do |_match|
89
+ name = ::Regexp.last_match(1)
90
+ resource = db.query("name =~ \"#{name}\"").first
91
+ if resource
92
+ %(<a href="/kinds/#{resource.kind}/instances/#{resource.name}">#{name}</a>)
93
+ else
94
+ %(<span class="broken-link" title="Resource not found">#{name}</span>)
95
+ end
96
+ end
97
+ end
98
+
99
+ def to_dollar(num)
100
+ # Round to 2 decimals first (important for floating‑point edge cases)
101
+ rounded = (num * 100).round / 100.0
102
+ # Insert commas every three digits left of the decimal point
103
+ parts = format("%.2f", rounded).split(".")
104
+ parts[0] = parts[0].reverse.scan(/\d{1,3}/).join(",").reverse
105
+ "$#{parts.join(".")}"
106
+ end
107
+
108
+ def http_git(repo_url)
109
+ repo_url.gsub(/.git$/, "")
110
+ .gsub(":", "/")
111
+ .gsub("git@", "https://")
112
+ end
113
+
114
+ def number_with_delimiter(num)
115
+ num.to_s.reverse.scan(/\d{1,3}/).join(",").reverse
116
+ end
117
+
118
+ # Generate asset path with cache-busting query string based on file mtime
119
+ def asset_path(path)
120
+ file_path = File.join(settings.public_folder, path)
121
+ if File.exist?(file_path)
122
+ mtime = File.mtime(file_path).to_i
123
+ "#{path}?v=#{mtime}"
124
+ else
125
+ path
126
+ end
127
+ end
128
+
129
+ # Convert timestamp to human-readable relative time
130
+ def time_ago(timestamp)
131
+ return nil unless timestamp
132
+
133
+ time = timestamp.is_a?(Time) ? timestamp : Time.parse(timestamp.to_s)
134
+ seconds = (Time.now - time).to_i
135
+
136
+ units = [
137
+ [60, "second"],
138
+ [60, "minute"],
139
+ [24, "hour"],
140
+ [7, "day"],
141
+ [4, "week"],
142
+ [12, "month"],
143
+ [Float::INFINITY, "year"]
144
+ ]
145
+
146
+ value = seconds
147
+ units.each do |divisor, unit|
148
+ return "just now" if unit == "second" && value < 10
149
+ return "#{value} #{unit}#{"s" if value != 1} ago" if value < divisor
150
+
151
+ value /= divisor
152
+ end
153
+ end
154
+ end
155
+
156
+ get "/" do
157
+ haml :index
158
+ end
159
+
160
+ get "/reload" do
161
+ Archsight::Web::Application.reload!
162
+ if params["redirect"]&.start_with?("/")
163
+ redirect params["redirect"]
164
+ else
165
+ redirect "/"
166
+ end
167
+ rescue Archsight::ResourceError => e
168
+ @error = e
169
+ haml :index
170
+ end
171
+
172
+ get "/doc/resources/:filename" do
173
+ filename = params["filename"].gsub(/[^a-zA-Z0-9_-]/, "") # sanitize
174
+ # Convert snake_case to PascalCase for resource kind
175
+ kind_name = filename.split("_").map(&:capitalize).join
176
+
177
+ begin
178
+ content = Archsight::Documentation.generate(kind_name)
179
+ @doc_content = markdown(content)
180
+ rescue StandardError
181
+ halt 404, "Documentation not found"
182
+ end
183
+
184
+ if request.env["HTTP_HX_REQUEST"]
185
+ "<article>#{@doc_content}</article>"
186
+ else
187
+ haml :index
188
+ end
189
+ end
190
+
191
+ get "/doc/:filename" do
192
+ filename = params["filename"].gsub(/[^a-zA-Z0-9_-]/, "") # sanitize
193
+
194
+ # Check for ERB template first, then plain markdown
195
+ erb_path = File.join(settings.views, "..", "doc", "#{filename}.md.erb")
196
+ md_path = File.join(settings.views, "..", "doc", "#{filename}.md")
197
+
198
+ content = if File.exist?(erb_path)
199
+ template = ERB.new(File.read(erb_path))
200
+ template.result(binding)
201
+ elsif File.exist?(md_path)
202
+ File.read(md_path)
203
+ else
204
+ halt 404, "Documentation not found"
205
+ end
206
+
207
+ @doc_content = markdown(content)
208
+
209
+ if request.env["HTTP_HX_REQUEST"]
210
+ "<article>#{@doc_content}</article>"
211
+ else
212
+ haml :index
213
+ end
214
+ end
215
+
216
+ # Shared search logic for both GET and POST
217
+ def perform_search
218
+ start_time = Time.now
219
+ if (@q = params["q"])
220
+ @instances = db.query(@q)
221
+ elsif (@tag = params["tag"]) && (@value = params["value"])
222
+ @method = params["method"] || "=="
223
+ # Build query string - quote value for string operators, leave unquoted for numeric
224
+ quoted_value = if %w[> < >= <=].include?(@method)
225
+ @value # Numeric comparison, no quotes
226
+ else
227
+ "\"#{@value.gsub('"', '\\"')}\"" # String comparison, quote it
228
+ end
229
+ @q = "#{@tag} #{@method} #{quoted_value}"
230
+ @instances = db.query(@q)
231
+ else
232
+ @instances = []
233
+ end
234
+ if (@kind = params["kind"])
235
+ @instances = @instances.select { |i| i.kind == @kind } if @kind
236
+ end
237
+ @search_time_ms = ((Time.now - start_time) * 1000).round(2)
238
+ rescue Archsight::Query::QueryError => e
239
+ @query_error = e
240
+ @search_time_ms = ((Time.now - start_time) * 1000).round(2)
241
+ @q = params["q"] || "#{params["tag"]} #{params["method"] || "=="} \"#{params["value"]}\""
242
+ end
243
+
244
+ # GET /search - for direct URL access, bookmarks, and browser history
245
+ get "/search" do
246
+ perform_search
247
+ haml :index
248
+ end
249
+
250
+ # POST /search - for HTMX requests
251
+ post "/search" do
252
+ perform_search
253
+ haml :search
254
+ end
255
+
256
+ get "/svg" do
257
+ content_type :svg
258
+ create_graph_all(db, :draw_svg)
259
+ end
260
+
261
+ get "/dot" do
262
+ content_type "text/plain"
263
+ create_graph_all(db, :draw_dot)
264
+ end
265
+
266
+ get "/kinds/:kind" do
267
+ @kind = params["kind"]
268
+ haml :index
269
+ end
270
+
271
+ get "/kinds/:kind/instances/:instance" do
272
+ @kind = params["kind"]
273
+ @instance = params["instance"]
274
+ haml :index
275
+ end
276
+
277
+ get "/kinds/:kind/instances/:instance/svg" do
278
+ @kind = params["kind"]
279
+ @instance = params["instance"]
280
+ content_type :svg
281
+ create_graph_one(db, @kind, @instance, :draw_svg)
282
+ end
283
+
284
+ get "/kinds/:kind/instances/:instance/dot" do
285
+ @kind = params["kind"]
286
+ @instance = params["instance"]
287
+ content_type "text/plain"
288
+ create_graph_one(db, @kind, @instance, :draw_dot)
289
+ end
290
+ end