capydash 0.1.7 → 0.2.1

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.
data/lib/capydash/auth.rb DELETED
@@ -1,103 +0,0 @@
1
- require 'securerandom'
2
- require 'digest'
3
-
4
- module CapyDash
5
- class Auth
6
- class << self
7
- def authenticate(username, password)
8
- return false unless auth_enabled?
9
-
10
- # Simple hardcoded credentials for MVP
11
- # In production, this would connect to a proper user database
12
- valid_credentials = {
13
- 'admin' => 'capydash123',
14
- 'developer' => 'test123',
15
- 'viewer' => 'readonly123'
16
- }
17
-
18
- if valid_credentials[username] == password
19
- token = generate_token(username)
20
- Logger.info("User authenticated", {
21
- username: username,
22
- token: token[0..8] + "..."
23
- })
24
- token
25
- else
26
- Logger.warn("Authentication failed", {
27
- username: username,
28
- ip: current_ip
29
- })
30
- nil
31
- end
32
- end
33
-
34
- def validate_token(token)
35
- return false unless auth_enabled?
36
- return false unless token && token.length > 10
37
-
38
- # Simple token validation for MVP
39
- # In production, this would use JWT or similar
40
- begin
41
- decoded = Base64.decode64(token)
42
- parts = decoded.split(':')
43
- return false unless parts.length == 3
44
-
45
- username, timestamp, signature = parts
46
- expected_signature = generate_signature(username, timestamp)
47
-
48
- if signature == expected_signature
49
- # Check if token is not expired (24 hours)
50
- token_time = Time.at(timestamp.to_i)
51
- if Time.now - token_time < 24 * 60 * 60
52
- Logger.debug("Token validated", { username: username })
53
- username
54
- else
55
- Logger.warn("Token expired", { username: username })
56
- false
57
- end
58
- else
59
- Logger.warn("Invalid token signature", { token: token[0..8] + "..." })
60
- false
61
- end
62
- rescue => e
63
- ErrorHandler.handle_error(e, {
64
- error_type: 'authentication',
65
- operation: 'validate_token'
66
- })
67
- false
68
- end
69
- end
70
-
71
- def auth_enabled?
72
- CapyDash.config.auth_enabled?
73
- end
74
-
75
- def require_auth!
76
- return true unless auth_enabled?
77
- # This would be called by middleware or controllers
78
- # For now, just return true
79
- true
80
- end
81
-
82
- private
83
-
84
- def generate_token(username)
85
- timestamp = Time.now.to_i.to_s
86
- signature = generate_signature(username, timestamp)
87
- token_data = "#{username}:#{timestamp}:#{signature}"
88
- Base64.encode64(token_data).strip
89
- end
90
-
91
- def generate_signature(username, timestamp)
92
- secret = CapyDash.config.secret_key
93
- data = "#{username}:#{timestamp}:#{secret}"
94
- Digest::SHA256.hexdigest(data)
95
- end
96
-
97
- def current_ip
98
- # In a real app, this would get the actual IP
99
- "127.0.0.1"
100
- end
101
- end
102
- end
103
- end
@@ -1,186 +0,0 @@
1
- module CapyDash
2
- class Configuration
3
- attr_accessor :server, :dashboard, :tests, :logging, :security, :performance
4
-
5
- def initialize
6
- @server = {
7
- host: "localhost",
8
- port: 4000,
9
- websocket_path: "/websocket",
10
- max_connections: 100,
11
- message_history_limit: 1000
12
- }
13
-
14
- @dashboard = {
15
- title: "CapyDash Test Monitor",
16
- refresh_interval: 1000,
17
- auto_scroll: true,
18
- show_timestamps: true,
19
- screenshot_quality: 0.8,
20
- max_screenshot_width: 1200
21
- }
22
-
23
- @tests = {
24
- default_directory: "test",
25
- system_tests_dir: "test/system",
26
- feature_tests_dir: "test/features",
27
- controller_tests_dir: "test/controllers",
28
- model_tests_dir: "test/models",
29
- screenshot_dir: "tmp/capydash_screenshots",
30
- timeout: 300
31
- }
32
-
33
- @logging = {
34
- level: "info",
35
- file: "log/capydash.log",
36
- max_file_size: "10MB",
37
- max_files: 5
38
- }
39
-
40
- @security = {
41
- enable_auth: false,
42
- secret_key: "your-secret-key-here",
43
- session_timeout: 3600
44
- }
45
-
46
- @performance = {
47
- enable_compression: true,
48
- cleanup_interval: 300,
49
- max_memory_usage: "512MB"
50
- }
51
- end
52
-
53
- def self.load_from_file(config_path = nil)
54
- config_path ||= File.join(Dir.pwd, "config", "capydash.yml")
55
-
56
- if File.exist?(config_path)
57
- require 'yaml'
58
- yaml_config = YAML.load_file(config_path)
59
-
60
- config = new
61
- config.load_from_hash(yaml_config)
62
- config
63
- else
64
- # Return default configuration if file doesn't exist
65
- new
66
- end
67
- rescue => e
68
- puts "Warning: Could not load configuration from #{config_path}: #{e.message}"
69
- puts "Using default configuration."
70
- new
71
- end
72
-
73
- def load_from_hash(hash)
74
- @server.merge!(hash['server']) if hash['server']
75
- @dashboard.merge!(hash['dashboard']) if hash['dashboard']
76
- @tests.merge!(hash['tests']) if hash['tests']
77
- @logging.merge!(hash['logging']) if hash['logging']
78
- @security.merge!(hash['security']) if hash['security']
79
- @performance.merge!(hash['performance']) if hash['performance']
80
- end
81
-
82
- def server_host
83
- @server[:host]
84
- end
85
-
86
- def server_port
87
- @server[:port]
88
- end
89
-
90
- def websocket_path
91
- @server[:websocket_path]
92
- end
93
-
94
- def max_connections
95
- @server[:max_connections]
96
- end
97
-
98
- def message_history_limit
99
- @server[:message_history_limit]
100
- end
101
-
102
- def dashboard_title
103
- @dashboard[:title]
104
- end
105
-
106
- def auto_scroll?
107
- @dashboard[:auto_scroll]
108
- end
109
-
110
- def show_timestamps?
111
- @dashboard[:show_timestamps]
112
- end
113
-
114
- def screenshot_quality
115
- @dashboard[:screenshot_quality]
116
- end
117
-
118
- def max_screenshot_width
119
- @dashboard[:max_screenshot_width]
120
- end
121
-
122
- def system_tests_dir
123
- @tests[:system_tests_dir]
124
- end
125
-
126
- def feature_tests_dir
127
- @tests[:feature_tests_dir]
128
- end
129
-
130
- def controller_tests_dir
131
- @tests[:controller_tests_dir]
132
- end
133
-
134
- def model_tests_dir
135
- @tests[:model_tests_dir]
136
- end
137
-
138
- def screenshot_dir
139
- @tests[:screenshot_dir]
140
- end
141
-
142
- def test_timeout
143
- @tests[:timeout]
144
- end
145
-
146
- def log_level
147
- @logging[:level]
148
- end
149
-
150
- def log_file
151
- @logging[:file]
152
- end
153
-
154
- def max_files
155
- @logging[:max_files]
156
- end
157
-
158
- def max_file_size
159
- @logging[:max_file_size]
160
- end
161
-
162
- def auth_enabled?
163
- @security[:enable_auth]
164
- end
165
-
166
- def secret_key
167
- @security[:secret_key]
168
- end
169
-
170
- def session_timeout
171
- @security[:session_timeout]
172
- end
173
-
174
- def compression_enabled?
175
- @performance[:enable_compression]
176
- end
177
-
178
- def cleanup_interval
179
- @performance[:cleanup_interval]
180
- end
181
-
182
- def max_memory_usage
183
- @performance[:max_memory_usage]
184
- end
185
- end
186
- end
@@ -1,167 +0,0 @@
1
- require 'eventmachine'
2
- require 'em-websocket'
3
-
4
- module CapyDash
5
- class DashboardServer
6
- attr_reader :port, :clients
7
-
8
- # Provide a single shared instance for the whole process
9
- def self.instance(port: nil)
10
- configured_port = port || CapyDash.config.server_port
11
- @instance ||= new(port: configured_port)
12
- end
13
-
14
- def initialize(port: nil)
15
- @port = port || CapyDash.config.server_port
16
- @clients = []
17
- @history = [] # recent events to replay on reconnect
18
- @history_limit = CapyDash.config.message_history_limit
19
- @max_connections = CapyDash.config.max_connections
20
- end
21
-
22
- # Start the WebSocket server in a background thread
23
- def start
24
- return if defined?(@started) && @started
25
- @started = true
26
- Thread.new do
27
- EM.run do
28
- EM::WebSocket.run(host: "0.0.0.0", port: @port) do |ws|
29
-
30
- ws.onopen do
31
- begin
32
- # Check connection limit
33
- if @clients.length >= @max_connections
34
- CapyDash::Logger.warn("Connection limit reached", {
35
- current_connections: @clients.length,
36
- max_connections: @max_connections
37
- })
38
- ws.close
39
- return
40
- end
41
-
42
- @clients << ws
43
- CapyDash::Logger.info("Client connected", {
44
- total_clients: @clients.length,
45
- client_id: ws.object_id
46
- })
47
- puts "Client connected"
48
-
49
- # Replay recent history so refreshed clients see last events
50
- @history.each do |msg|
51
- begin
52
- ws.send(msg)
53
- rescue => e
54
- CapyDash::ErrorHandler.handle_websocket_error(e, ws)
55
- end
56
- end
57
- rescue => e
58
- CapyDash::ErrorHandler.handle_websocket_error(e, ws)
59
- end
60
- end
61
-
62
- ws.onclose do
63
- begin
64
- @clients.delete(ws)
65
- CapyDash::Logger.info("Client disconnected", {
66
- remaining_clients: @clients.length,
67
- client_id: ws.object_id
68
- })
69
- puts "Client disconnected"
70
- rescue => e
71
- CapyDash::ErrorHandler.handle_websocket_error(e, ws)
72
- end
73
- end
74
-
75
- ws.onmessage do |msg|
76
- begin
77
- CapyDash::Logger.debug("Received WebSocket message", {
78
- message_length: msg.length,
79
- client_id: ws.object_id
80
- })
81
- puts "Received message: #{msg}"
82
-
83
- data = JSON.parse(msg) rescue nil
84
- if data && data["command"] == "run_tests"
85
- args = data["args"] || ["bundle", "exec", "rails", "test", "test/system"]
86
- CapyDash::Logger.info("Executing test command", {
87
- command: args.join(' '),
88
- client_id: ws.object_id
89
- })
90
-
91
- Thread.new do
92
- begin
93
- # Change to the dummy app directory and run tests there
94
- current_dir = File.dirname(__FILE__)
95
- gem_root = File.expand_path(File.join(current_dir, "..", ".."))
96
- dummy_app_path = File.join(gem_root, "spec", "dummy_app")
97
-
98
- CapyDash::Logger.info("Running tests in directory", {
99
- directory: dummy_app_path,
100
- exists: Dir.exist?(dummy_app_path)
101
- })
102
-
103
- Dir.chdir(dummy_app_path) do
104
- # Set Rails environment and ensure CapyDash is loaded
105
- ENV["RAILS_ENV"] = "test"
106
- ENV["CAPYDASH_EXTERNAL_WS"] = "1" # Use external WebSocket mode
107
-
108
-
109
- # Run the command and capture both stdout and stderr
110
- IO.popen(args, err: [:child, :out]) do |io|
111
- io.each_line do |line|
112
- event = { type: "runner", line: line.strip, status: "running", ts: Time.now.to_i }
113
- broadcast(event.to_json)
114
- end
115
- end
116
- end
117
- rescue => e
118
- CapyDash::ErrorHandler.handle_test_execution_error(e, args.join(' '))
119
- broadcast({ type: "runner", line: "Error: #{e.message}", status: "failed", ts: Time.now.to_i }.to_json)
120
- end
121
- broadcast({ type: "runner", line: "Finished", status: "passed", ts: Time.now.to_i }.to_json)
122
- end
123
- else
124
- # Optionally broadcast to all clients
125
- broadcast(msg)
126
- end
127
- rescue => e
128
- CapyDash::ErrorHandler.handle_websocket_error(e, ws)
129
- end
130
- end
131
-
132
- end
133
- end
134
- end
135
- end
136
-
137
- # Send a message to all connected clients
138
- def broadcast(message)
139
- begin
140
- # store in history buffer
141
- @history << message
142
- @history.shift if @history.length > @history_limit
143
-
144
- # fan out to all connected clients
145
- @clients.each do |client|
146
- begin
147
- client.send(message)
148
- rescue => e
149
- CapyDash::ErrorHandler.handle_websocket_error(e, client)
150
- # Remove dead connections
151
- @clients.delete(client)
152
- end
153
- end
154
- rescue => e
155
- CapyDash::ErrorHandler.handle_error(e, {
156
- error_type: 'broadcast',
157
- message_length: message&.length
158
- })
159
- end
160
- end
161
-
162
- # Stop the WebSocket server gracefully
163
- def stop
164
- EM.stop_event_loop if EM.reactor_running?
165
- end
166
- end
167
- end
@@ -1,52 +0,0 @@
1
- module CapyDash
2
- class Engine < ::Rails::Engine
3
- isolate_namespace CapyDash
4
-
5
- rake_tasks do
6
- load File.expand_path("../tasks/capydash.rake", __dir__)
7
- end
8
-
9
- initializer "capydash.configuration", before: :load_config_initializers do
10
- # Load configuration
11
- CapyDash.config = CapyDash::Configuration.load_from_file
12
- CapyDash::Logger.setup(CapyDash.config)
13
-
14
- CapyDash::Logger.info("CapyDash configuration loaded", {
15
- server_port: CapyDash.config.server_port,
16
- log_level: CapyDash.config.log_level,
17
- auth_enabled: CapyDash.config.auth_enabled?
18
- })
19
- end
20
-
21
- initializer "capydash.instrumentation", after: :load_config_initializers do
22
- begin
23
- # Hook into Capybara sessions automatically
24
- ActiveSupport.on_load(:action_dispatch_integration_test) do
25
- Capybara::Session.include(CapyDash::Instrumentation)
26
- end
27
-
28
- # Start WebSocket server automatically in dev/test unless using external WS
29
- if (Rails.env.development? || Rails.env.test?) && ENV["CAPYDASH_EXTERNAL_WS"] != "1"
30
- port = CapyDash.config.server_port
31
- CapyDash::DashboardServer.instance(port: port).start
32
-
33
- CapyDash::Logger.info("WebSocket server started", {
34
- port: port,
35
- environment: Rails.env
36
- })
37
- else
38
- CapyDash::Logger.info("Skipping in-process WebSocket server", {
39
- reason: ENV["CAPYDASH_EXTERNAL_WS"] == "1" ? "external_mode" : "production_environment",
40
- environment: Rails.env
41
- })
42
- end
43
- rescue => e
44
- CapyDash::ErrorHandler.handle_error(e, {
45
- error_type: 'initialization',
46
- component: 'engine'
47
- })
48
- raise
49
- end
50
- end
51
- end
52
- end
@@ -1,101 +0,0 @@
1
- module CapyDash
2
- class ErrorHandler
3
- class << self
4
- def handle_error(error, context = {})
5
- log_error(error, context)
6
- notify_error(error, context) if should_notify?(error)
7
- recover_from_error(error, context)
8
- end
9
-
10
- def handle_websocket_error(error, connection = nil)
11
- context = {
12
- error_type: 'websocket',
13
- connection_id: connection&.id,
14
- error_class: error.class.name
15
- }
16
- handle_error(error, context)
17
- end
18
-
19
- def handle_test_execution_error(error, test_path = nil)
20
- context = {
21
- error_type: 'test_execution',
22
- test_path: test_path,
23
- error_class: error.class.name
24
- }
25
- handle_error(error, context)
26
- end
27
-
28
- def handle_instrumentation_error(error, method_name = nil)
29
- context = {
30
- error_type: 'instrumentation',
31
- method_name: method_name,
32
- error_class: error.class.name
33
- }
34
- handle_error(error, context)
35
- end
36
-
37
- private
38
-
39
- def log_error(error, context)
40
- Logger.error(
41
- "Error occurred: #{error.message}",
42
- context.merge(
43
- backtrace: error.backtrace&.first(5),
44
- error_class: error.class.name
45
- )
46
- )
47
- end
48
-
49
- def notify_error(error, context)
50
- # In a production app, this would send notifications to monitoring services
51
- # like Sentry, Bugsnag, or custom webhooks
52
- puts "🚨 CRITICAL ERROR: #{error.message}"
53
- puts "Context: #{context}"
54
- end
55
-
56
- def should_notify?(error)
57
- # Only notify for critical errors
58
- error.is_a?(StandardError) &&
59
- !error.is_a?(ArgumentError) &&
60
- !error.is_a?(NoMethodError)
61
- end
62
-
63
- def recover_from_error(error, context)
64
- case context[:error_type]
65
- when 'websocket'
66
- recover_websocket_connection
67
- when 'test_execution'
68
- recover_test_execution
69
- when 'instrumentation'
70
- recover_instrumentation
71
- else
72
- # Generic recovery
73
- Logger.warn("No specific recovery strategy for error type: #{context[:error_type]}")
74
- end
75
- end
76
-
77
- def recover_websocket_connection
78
- Logger.info("Attempting to recover WebSocket connection")
79
- # In a real implementation, this would attempt to reconnect
80
- # or notify the dashboard to refresh the connection
81
- end
82
-
83
- def recover_test_execution
84
- Logger.info("Test execution error recovered")
85
- # Could implement retry logic or cleanup
86
- end
87
-
88
- def recover_instrumentation
89
- Logger.info("Instrumentation error recovered")
90
- # Could disable problematic instrumentation or use fallbacks
91
- end
92
- end
93
- end
94
-
95
- # Custom error classes for better error handling
96
- class CapyDashError < StandardError; end
97
- class ConfigurationError < CapyDashError; end
98
- class WebSocketError < CapyDashError; end
99
- class TestExecutionError < CapyDashError; end
100
- class InstrumentationError < CapyDashError; end
101
- end
@@ -1,29 +0,0 @@
1
- require 'capydash/dashboard_server'
2
- require 'capydash/forwarder'
3
-
4
- module CapyDash
5
- module EventEmitter
6
- @clients = []
7
-
8
- class << self
9
- attr_accessor :clients
10
-
11
- def subscribe(&block)
12
- @on_event = block
13
- end
14
-
15
- def broadcast(event)
16
- @on_event&.call(event)
17
-
18
- port = CapyDash.configuration&.port
19
- if ENV["CAPYDASH_EXTERNAL_WS"] == "1"
20
- forwarder = CapyDash::Forwarder.instance(port: port)
21
- forwarder.send_message(event)
22
- else
23
- server = CapyDash::DashboardServer.instance(port: port)
24
- server.broadcast(event.to_json)
25
- end
26
- end
27
- end
28
- end
29
- end