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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +24 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/CONTRIBUTING.md +186 -0
- data/Dockerfile +39 -0
- data/LICENSE.txt +201 -0
- data/README.md +170 -0
- data/SECURITY.md +27 -0
- data/exe/archsight +9 -0
- data/lib/archsight/annotations/aggregators.rb +109 -0
- data/lib/archsight/annotations/annotation.rb +168 -0
- data/lib/archsight/annotations/architecture_annotations.rb +59 -0
- data/lib/archsight/annotations/backup_annotations.rb +21 -0
- data/lib/archsight/annotations/computed.rb +264 -0
- data/lib/archsight/annotations/email_recipient.rb +35 -0
- data/lib/archsight/annotations/generated_annotations.rb +17 -0
- data/lib/archsight/annotations/git_annotations.rb +21 -0
- data/lib/archsight/annotations/relation_resolver.rb +160 -0
- data/lib/archsight/cli.rb +120 -0
- data/lib/archsight/configuration.rb +36 -0
- data/lib/archsight/database.rb +183 -0
- data/lib/archsight/documentation.rb +171 -0
- data/lib/archsight/graph.rb +113 -0
- data/lib/archsight/helpers.rb +210 -0
- data/lib/archsight/linter.rb +77 -0
- data/lib/archsight/mcp/analyze_resource_tool.rb +222 -0
- data/lib/archsight/mcp/base.rb +48 -0
- data/lib/archsight/mcp/query_tool.rb +113 -0
- data/lib/archsight/mcp/resource_doc_tool.rb +87 -0
- data/lib/archsight/mcp.rb +6 -0
- data/lib/archsight/query/ast.rb +279 -0
- data/lib/archsight/query/errors.rb +39 -0
- data/lib/archsight/query/evaluator.rb +707 -0
- data/lib/archsight/query/lexer.rb +289 -0
- data/lib/archsight/query/parser.rb +506 -0
- data/lib/archsight/query.rb +68 -0
- data/lib/archsight/renderer.rb +134 -0
- data/lib/archsight/resources/application_component.rb +346 -0
- data/lib/archsight/resources/application_interface.rb +54 -0
- data/lib/archsight/resources/application_service.rb +222 -0
- data/lib/archsight/resources/base.rb +300 -0
- data/lib/archsight/resources/business_actor.rb +195 -0
- data/lib/archsight/resources/business_constraint.rb +32 -0
- data/lib/archsight/resources/business_process.rb +37 -0
- data/lib/archsight/resources/business_product.rb +206 -0
- data/lib/archsight/resources/business_requirement.rb +56 -0
- data/lib/archsight/resources/compliance_evidence.rb +42 -0
- data/lib/archsight/resources/data_object.rb +49 -0
- data/lib/archsight/resources/motivation_goal.rb +37 -0
- data/lib/archsight/resources/motivation_outcome.rb +33 -0
- data/lib/archsight/resources/motivation_stakeholder.rb +38 -0
- data/lib/archsight/resources/strategy_capability.rb +38 -0
- data/lib/archsight/resources/technology_artifact.rb +154 -0
- data/lib/archsight/resources/technology_interface.rb +34 -0
- data/lib/archsight/resources/technology_node.rb +42 -0
- data/lib/archsight/resources/technology_service.rb +35 -0
- data/lib/archsight/resources/technology_system_software.rb +37 -0
- data/lib/archsight/resources/view.rb +51 -0
- data/lib/archsight/resources.rb +49 -0
- data/lib/archsight/template.rb +49 -0
- data/lib/archsight/version.rb +5 -0
- data/lib/archsight/web/application.rb +290 -0
- data/lib/archsight/web/doc/archimate.md +215 -0
- data/lib/archsight/web/doc/computed_annotations.md +316 -0
- data/lib/archsight/web/doc/icons.md +303 -0
- data/lib/archsight/web/doc/index.md.erb +74 -0
- data/lib/archsight/web/doc/modeling.md +200 -0
- data/lib/archsight/web/doc/search.md +227 -0
- data/lib/archsight/web/doc/togaf.md +255 -0
- data/lib/archsight/web/doc/tool.md +90 -0
- data/lib/archsight/web/public/css/artifact.css +985 -0
- data/lib/archsight/web/public/css/base.css +201 -0
- data/lib/archsight/web/public/css/graph.css +106 -0
- data/lib/archsight/web/public/css/highlight.min.css +10 -0
- data/lib/archsight/web/public/css/iconoir.css +22 -0
- data/lib/archsight/web/public/css/instance.css +329 -0
- data/lib/archsight/web/public/css/layout.css +421 -0
- data/lib/archsight/web/public/css/mermaid-layers.css +188 -0
- data/lib/archsight/web/public/css/pico.min.css +4 -0
- data/lib/archsight/web/public/favicon.ico +0 -0
- data/lib/archsight/web/public/img/archimate.png +0 -0
- data/lib/archsight/web/public/img/togaf-high-level.png +0 -0
- data/lib/archsight/web/public/js/graph-zoom.js +18 -0
- data/lib/archsight/web/public/js/highlight.min.js +3899 -0
- data/lib/archsight/web/public/js/htmx.min.js +1 -0
- data/lib/archsight/web/public/js/mermaid-init.js +88 -0
- data/lib/archsight/web/public/js/mermaid.min.js +2811 -0
- data/lib/archsight/web/public/js/sparkline.js +42 -0
- data/lib/archsight/web/public/js/svg-pan-zoom.min.js +3 -0
- data/lib/archsight/web/public/js/svg-zoom-controls.js +93 -0
- data/lib/archsight/web/views/index.haml +12 -0
- data/lib/archsight/web/views/partials/artifact/_activity.haml +55 -0
- data/lib/archsight/web/views/partials/artifact/_agentic.haml +25 -0
- data/lib/archsight/web/views/partials/artifact/_deployment.haml +29 -0
- data/lib/archsight/web/views/partials/artifact/_git_info.haml +16 -0
- data/lib/archsight/web/views/partials/artifact/_language_stats.haml +53 -0
- data/lib/archsight/web/views/partials/artifact/_links.haml +24 -0
- data/lib/archsight/web/views/partials/artifact/_project_estimate.haml +26 -0
- data/lib/archsight/web/views/partials/artifact/_repositories.haml +55 -0
- data/lib/archsight/web/views/partials/artifact/_team.haml +83 -0
- data/lib/archsight/web/views/partials/artifact/_workflow.haml +69 -0
- data/lib/archsight/web/views/partials/components/_activity.haml +37 -0
- data/lib/archsight/web/views/partials/components/_git.haml +17 -0
- data/lib/archsight/web/views/partials/components/_jira.haml +18 -0
- data/lib/archsight/web/views/partials/components/_languages.haml +29 -0
- data/lib/archsight/web/views/partials/components/_owner.haml +15 -0
- data/lib/archsight/web/views/partials/components/_repositories.haml +37 -0
- data/lib/archsight/web/views/partials/components/_status.haml +23 -0
- data/lib/archsight/web/views/partials/instance/_detail.haml +99 -0
- data/lib/archsight/web/views/partials/instance/_graph.haml +6 -0
- data/lib/archsight/web/views/partials/instance/_list.haml +84 -0
- data/lib/archsight/web/views/partials/instance/_relations.haml +43 -0
- data/lib/archsight/web/views/partials/instance/_requirements.haml +41 -0
- data/lib/archsight/web/views/partials/instance/_view_detail.haml +57 -0
- data/lib/archsight/web/views/partials/layout/_content.haml +40 -0
- data/lib/archsight/web/views/partials/layout/_error.haml +22 -0
- data/lib/archsight/web/views/partials/layout/_head.haml +24 -0
- data/lib/archsight/web/views/partials/layout/_navigation.haml +20 -0
- data/lib/archsight/web/views/partials/layout/_sidebar.haml +27 -0
- data/lib/archsight/web/views/search.haml +53 -0
- data/lib/archsight.rb +17 -0
- 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,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
|