ragdoll-rails 0.1.8 → 0.1.11

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +18 -21
  3. data/app/assets/javascripts/ragdoll/application.js +129 -0
  4. data/app/assets/javascripts/ragdoll/bulk_upload_status.js +454 -0
  5. data/app/assets/stylesheets/ragdoll/application.css +84 -0
  6. data/app/assets/stylesheets/ragdoll/bulk_upload_status.css +379 -0
  7. data/app/channels/application_cable/channel.rb +6 -0
  8. data/app/channels/application_cable/connection.rb +6 -0
  9. data/app/channels/ragdoll/bulk_upload_status_channel.rb +27 -0
  10. data/app/channels/ragdoll/file_processing_channel.rb +26 -0
  11. data/app/components/ragdoll/alert_component.html.erb +4 -0
  12. data/app/components/ragdoll/alert_component.rb +32 -0
  13. data/app/components/ragdoll/application_component.rb +6 -0
  14. data/app/components/ragdoll/card_component.html.erb +15 -0
  15. data/app/components/ragdoll/card_component.rb +21 -0
  16. data/app/components/ragdoll/document_list_component.html.erb +41 -0
  17. data/app/components/ragdoll/document_list_component.rb +13 -0
  18. data/app/components/ragdoll/document_table_component.html.erb +76 -0
  19. data/app/components/ragdoll/document_table_component.rb +13 -0
  20. data/app/components/ragdoll/empty_state_component.html.erb +12 -0
  21. data/app/components/ragdoll/empty_state_component.rb +17 -0
  22. data/app/components/ragdoll/flash_messages_component.html.erb +3 -0
  23. data/app/components/ragdoll/flash_messages_component.rb +37 -0
  24. data/app/components/ragdoll/navbar_component.html.erb +24 -0
  25. data/app/components/ragdoll/navbar_component.rb +31 -0
  26. data/app/components/ragdoll/page_header_component.html.erb +13 -0
  27. data/app/components/ragdoll/page_header_component.rb +15 -0
  28. data/app/components/ragdoll/stats_card_component.html.erb +11 -0
  29. data/app/components/ragdoll/stats_card_component.rb +17 -0
  30. data/app/components/ragdoll/status_badge_component.html.erb +3 -0
  31. data/app/components/ragdoll/status_badge_component.rb +30 -0
  32. data/app/controllers/ragdoll/api/v1/analytics_controller.rb +72 -0
  33. data/app/controllers/ragdoll/api/v1/base_controller.rb +29 -0
  34. data/app/controllers/ragdoll/api/v1/documents_controller.rb +148 -0
  35. data/app/controllers/ragdoll/api/v1/search_controller.rb +87 -0
  36. data/app/controllers/ragdoll/api/v1/system_controller.rb +97 -0
  37. data/app/controllers/ragdoll/application_controller.rb +17 -0
  38. data/app/controllers/ragdoll/configuration_controller.rb +82 -0
  39. data/app/controllers/ragdoll/dashboard_controller.rb +98 -0
  40. data/app/controllers/ragdoll/documents_controller.rb +460 -0
  41. data/app/controllers/ragdoll/documents_controller_backup.rb +68 -0
  42. data/app/controllers/ragdoll/jobs_controller.rb +116 -0
  43. data/app/controllers/ragdoll/search_controller.rb +368 -0
  44. data/app/jobs/application_job.rb +9 -0
  45. data/app/jobs/ragdoll/bulk_document_processing_job.rb +280 -0
  46. data/app/jobs/ragdoll/process_file_job.rb +166 -0
  47. data/app/services/ragdoll/worker_health_service.rb +111 -0
  48. data/app/views/layouts/ragdoll/application.html.erb +162 -0
  49. data/app/views/ragdoll/dashboard/analytics.html.erb +333 -0
  50. data/app/views/ragdoll/dashboard/index.html.erb +208 -0
  51. data/app/views/ragdoll/documents/edit.html.erb +91 -0
  52. data/app/views/ragdoll/documents/index.html.erb +302 -0
  53. data/app/views/ragdoll/documents/new.html.erb +1518 -0
  54. data/app/views/ragdoll/documents/show.html.erb +188 -0
  55. data/app/views/ragdoll/documents/upload_results.html.erb +248 -0
  56. data/app/views/ragdoll/jobs/index.html.erb +669 -0
  57. data/app/views/ragdoll/jobs/show.html.erb +129 -0
  58. data/app/views/ragdoll/search/index.html.erb +324 -0
  59. data/config/cable.yml +12 -0
  60. data/config/routes.rb +57 -2
  61. data/lib/generators/ragdoll/init/templates/INSTALL +3 -2
  62. data/lib/generators/ragdoll/init_generator.rb +68 -0
  63. data/lib/ragdoll/rails/engine.rb +48 -0
  64. data/lib/ragdoll/rails/version.rb +1 -1
  65. metadata +231 -6
  66. data/lib/generators/ragdoll/init/init_generator.rb +0 -26
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ class ProcessFileJob < ApplicationJob
5
+ queue_as :default
6
+
7
+ def perform(file_id, session_id, filename, temp_path)
8
+ ::Rails.logger.info "🚀 Ragdoll::ProcessFileJob starting: file_id=#{file_id}, session_id=#{session_id}, filename=#{filename}"
9
+ ::Rails.logger.info "📁 Temp file path: #{temp_path}"
10
+ ::Rails.logger.info "📊 Temp file exists: #{File.exist?(temp_path)}"
11
+ ::Rails.logger.info "📏 Temp file size: #{File.exist?(temp_path) ? File.size(temp_path) : 'N/A'} bytes"
12
+
13
+ begin
14
+ # Verify temp file exists before processing
15
+ unless File.exist?(temp_path)
16
+ raise "Temporary file not found: #{temp_path}"
17
+ end
18
+
19
+ # Broadcast start
20
+ broadcast_data = {
21
+ file_id: file_id,
22
+ filename: filename,
23
+ status: 'started',
24
+ progress: 0,
25
+ message: 'Starting file processing...'
26
+ }
27
+
28
+ ::Rails.logger.info "📡 Broadcasting start: #{broadcast_data}"
29
+ begin
30
+ ActionCable.server.broadcast("ragdoll_file_processing_#{session_id}", broadcast_data)
31
+ ::Rails.logger.info "✅ ActionCable broadcast sent successfully"
32
+
33
+ # Track job start in monitoring system
34
+ track_job_progress(session_id, file_id, filename, 0, 'started')
35
+ rescue => e
36
+ ::Rails.logger.error "❌ ActionCable broadcast failed: #{e.message}"
37
+ ::Rails.logger.error e.backtrace.first(3)
38
+ end
39
+
40
+ # Simulate progress updates during processing
41
+ broadcast_progress(session_id, file_id, filename, 25, 'Reading file...')
42
+ track_job_progress(session_id, file_id, filename, 25, 'processing')
43
+
44
+ # Use Ragdoll to add document
45
+ result = ::Ragdoll.add_document(path: temp_path)
46
+
47
+ broadcast_progress(session_id, file_id, filename, 75, 'Generating embeddings...')
48
+ track_job_progress(session_id, file_id, filename, 75, 'processing')
49
+
50
+ if result[:success] && result[:document_id]
51
+ document = ::Ragdoll::Document.find(result[:document_id])
52
+
53
+ # Broadcast completion
54
+ completion_data = {
55
+ file_id: file_id,
56
+ filename: filename,
57
+ status: 'completed',
58
+ progress: 100,
59
+ message: 'Processing completed successfully',
60
+ document_id: document.id
61
+ }
62
+
63
+ ::Rails.logger.info "🎉 Broadcasting completion: #{completion_data}"
64
+ begin
65
+ ActionCable.server.broadcast("ragdoll_file_processing_#{session_id}", completion_data)
66
+ ::Rails.logger.info "✅ Completion broadcast sent successfully"
67
+
68
+ # Mark job as completed in monitoring system
69
+ mark_job_completed(session_id, file_id)
70
+ rescue => e
71
+ ::Rails.logger.error "❌ Completion broadcast failed: #{e.message}"
72
+ end
73
+ else
74
+ raise "Processing failed: #{result[:error] || 'Unknown error'}"
75
+ end
76
+
77
+ rescue => e
78
+ ::Rails.logger.error "💥 Ragdoll::ProcessFileJob error: #{e.message}"
79
+ ::Rails.logger.error e.backtrace.first(5)
80
+
81
+ # Broadcast error
82
+ error_data = {
83
+ file_id: file_id,
84
+ filename: filename,
85
+ status: 'error',
86
+ progress: 0,
87
+ message: "Error: #{e.message}"
88
+ }
89
+
90
+ ::Rails.logger.info "📡 Broadcasting error: #{error_data}"
91
+ begin
92
+ ActionCable.server.broadcast("ragdoll_file_processing_#{session_id}", error_data)
93
+ ::Rails.logger.info "✅ Error broadcast sent successfully"
94
+
95
+ # Mark job as failed in monitoring system
96
+ mark_job_failed(session_id, file_id)
97
+ rescue => e
98
+ ::Rails.logger.error "❌ Error broadcast failed: #{e.message}"
99
+ end
100
+
101
+ # Re-raise the error to mark job as failed
102
+ raise e
103
+ ensure
104
+ # ALWAYS clean up temp file in ensure block
105
+ if temp_path && File.exist?(temp_path)
106
+ ::Rails.logger.info "🧹 Cleaning up temp file: #{temp_path}"
107
+ begin
108
+ File.delete(temp_path)
109
+ ::Rails.logger.info "✅ Temp file deleted successfully"
110
+ rescue => e
111
+ ::Rails.logger.error "❌ Failed to delete temp file: #{e.message}"
112
+ end
113
+ else
114
+ ::Rails.logger.info "📝 Temp file already cleaned up or doesn't exist: #{temp_path}"
115
+ end
116
+ end
117
+ end
118
+
119
+ private
120
+
121
+ def broadcast_progress(session_id, file_id, filename, progress, message)
122
+ broadcast_data = {
123
+ file_id: file_id,
124
+ filename: filename,
125
+ status: 'processing',
126
+ progress: progress,
127
+ message: message
128
+ }
129
+
130
+ ::Rails.logger.info "📡 Broadcasting progress: #{broadcast_data}"
131
+ begin
132
+ ActionCable.server.broadcast("ragdoll_file_processing_#{session_id}", broadcast_data)
133
+ ::Rails.logger.info "✅ Progress broadcast sent successfully"
134
+ rescue => e
135
+ ::Rails.logger.error "❌ Progress broadcast failed: #{e.message}"
136
+ end
137
+
138
+ # Small delay to simulate processing time
139
+ sleep(0.5)
140
+ end
141
+
142
+ def track_job_progress(session_id, file_id, filename, progress, status)
143
+ if defined?(JobFailureMonitorService)
144
+ JobFailureMonitorService.track_job_progress(session_id, file_id, filename, progress, status)
145
+ end
146
+ rescue => e
147
+ ::Rails.logger.error "❌ Failed to track job progress: #{e.message}"
148
+ end
149
+
150
+ def mark_job_completed(session_id, file_id)
151
+ if defined?(JobFailureMonitorService)
152
+ JobFailureMonitorService.mark_job_completed(session_id, file_id)
153
+ end
154
+ rescue => e
155
+ ::Rails.logger.error "❌ Failed to mark job as completed: #{e.message}"
156
+ end
157
+
158
+ def mark_job_failed(session_id, file_id)
159
+ if defined?(JobFailureMonitorService)
160
+ JobFailureMonitorService.mark_job_failed(session_id, file_id)
161
+ end
162
+ rescue => e
163
+ ::Rails.logger.error "❌ Failed to mark job as failed: #{e.message}"
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ragdoll
4
+ class WorkerHealthService
5
+ class << self
6
+ def check_worker_health
7
+ {
8
+ status: 'healthy',
9
+ workers: worker_status,
10
+ queues: queue_status,
11
+ timestamp: Time.current
12
+ }
13
+ rescue => e
14
+ {
15
+ status: 'error',
16
+ error: e.message,
17
+ timestamp: Time.current
18
+ }
19
+ end
20
+
21
+ def needs_restart?
22
+ # Check if there are stuck jobs or workers
23
+ stuck_jobs_count > 5 || !workers_running?
24
+ rescue
25
+ false
26
+ end
27
+
28
+ def process_stuck_jobs!(limit = 10)
29
+ processed = 0
30
+
31
+ # Find jobs that have been processing for too long (e.g., > 1 hour)
32
+ if defined?(SolidQueue::Job)
33
+ stuck_jobs = SolidQueue::Job
34
+ .where(finished_at: nil)
35
+ .where('created_at < ?', 1.hour.ago)
36
+ .limit(limit)
37
+
38
+ stuck_jobs.each do |job|
39
+ job.update(finished_at: Time.current)
40
+ processed += 1
41
+ end
42
+ end
43
+
44
+ processed
45
+ rescue => e
46
+ ::Rails.logger.error "Failed to process stuck jobs: #{e.message}"
47
+ 0
48
+ end
49
+
50
+ def restart_workers!
51
+ # In development, we typically don't restart workers
52
+ # This would be implemented differently in production
53
+ ::Rails.logger.info "Worker restart requested (no-op in development)"
54
+ true
55
+ rescue => e
56
+ ::Rails.logger.error "Failed to restart workers: #{e.message}"
57
+ false
58
+ end
59
+
60
+ private
61
+
62
+ def worker_status
63
+ if defined?(SolidQueue::Worker)
64
+ {
65
+ count: SolidQueue::Worker.count,
66
+ active: SolidQueue::Worker.where('last_heartbeat_at > ?', 5.minutes.ago).count
67
+ }
68
+ else
69
+ { count: 0, active: 0 }
70
+ end
71
+ rescue
72
+ { count: 0, active: 0, error: 'Unable to check worker status' }
73
+ end
74
+
75
+ def queue_status
76
+ if defined?(SolidQueue::Job)
77
+ {
78
+ pending: SolidQueue::Job.where(finished_at: nil).count,
79
+ completed: SolidQueue::Job.where.not(finished_at: nil).count,
80
+ failed: defined?(SolidQueue::FailedExecution) ? SolidQueue::FailedExecution.count : 0
81
+ }
82
+ else
83
+ { pending: 0, completed: 0, failed: 0 }
84
+ end
85
+ rescue
86
+ { pending: 0, completed: 0, failed: 0, error: 'Unable to check queue status' }
87
+ end
88
+
89
+ def stuck_jobs_count
90
+ return 0 unless defined?(SolidQueue::Job)
91
+
92
+ SolidQueue::Job
93
+ .where(finished_at: nil)
94
+ .where('created_at < ?', 1.hour.ago)
95
+ .count
96
+ rescue
97
+ 0
98
+ end
99
+
100
+ def workers_running?
101
+ return false unless defined?(SolidQueue::Worker)
102
+
103
+ SolidQueue::Worker
104
+ .where('last_heartbeat_at > ?', 5.minutes.ago)
105
+ .exists?
106
+ rescue
107
+ true # Assume workers are running if we can't check
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,162 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= content_for(:title) || "Ragdoll Dashboard" %></title>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="mobile-web-app-capable" content="yes">
8
+ <%= csrf_meta_tags %>
9
+ <%= csp_meta_tag %>
10
+ <meta name="csrf-token" content="<%= form_authenticity_token %>">
11
+
12
+ <%= yield :head %>
13
+
14
+ <%= favicon_link_tag "ragdoll.png", type: "image/png" %>
15
+ <%= favicon_link_tag "ragdoll.png", rel: "apple-touch-icon" %>
16
+ <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
17
+ <%= stylesheet_link_tag "ragdoll/bulk_upload_status", "data-turbo-track": "reload" %>
18
+
19
+ <!-- Bootstrap CSS from CDN -->
20
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
21
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
22
+
23
+ <!-- Rails UJS for handling method: :delete and data-confirm -->
24
+ <script src="https://cdn.jsdelivr.net/npm/@rails/ujs@7.0.0/lib/assets/compiled/rails-ujs.js"></script>
25
+
26
+ <!-- ActionCable from CDN with fallback handling -->
27
+ <script src="https://cdn.jsdelivr.net/gh/rails/rails@7-1-stable/actioncable/app/assets/javascripts/action_cable.js"></script>
28
+
29
+ <!-- Fallback CSS for when CDN is blocked -->
30
+ <style>
31
+ .container { max-width: 1200px; margin: 0 auto; padding: 0 15px; }
32
+ .row { display: flex; flex-wrap: wrap; margin: 0 -15px; }
33
+ .col-md-8 { flex: 0 0 66.666667%; max-width: 66.666667%; padding: 0 15px; }
34
+ .col-md-4 { flex: 0 0 33.333333%; max-width: 33.333333%; padding: 0 15px; }
35
+ .col-12 { flex: 0 0 100%; max-width: 100%; padding: 0 15px; }
36
+ .card { border: 1px solid #dee2e6; border-radius: 0.375rem; margin-bottom: 1rem; }
37
+ .card-header { padding: 0.75rem 1.25rem; background-color: #f8f9fa; border-bottom: 1px solid #dee2e6; }
38
+ .card-body { padding: 1.25rem; }
39
+ .btn { display: inline-block; padding: 0.375rem 0.75rem; font-size: 1rem; line-height: 1.5; border-radius: 0.375rem; border: 1px solid; text-decoration: none; cursor: pointer; }
40
+ .btn-primary { color: #fff; background-color: #007bff; border-color: #007bff; }
41
+ .navbar { padding: 0.5rem 0; background-color: #007bff !important; }
42
+ .navbar-brand { color: #fff !important; text-decoration: none; font-weight: bold; }
43
+ .nav-link { color: #fff !important; text-decoration: none; padding: 0.5rem 1rem; }
44
+ .alert { padding: 0.75rem 1.25rem; margin-bottom: 1rem; border: 1px solid; border-radius: 0.375rem; }
45
+ .alert-success { color: #155724; background-color: #d4edda; border-color: #c3e6cb; }
46
+ .alert-danger { color: #721c24; background-color: #f8d7da; border-color: #f5c6cb; }
47
+ .alert-info { color: #0c5460; background-color: #d1ecf1; border-color: #bee5eb; }
48
+ </style>
49
+ </head>
50
+
51
+ <body>
52
+ <nav class="navbar navbar-expand-lg navbar-dark bg-primary">
53
+ <div class="container">
54
+ <%= link_to "Ragdoll Dashboard", ragdoll.root_path, class: "navbar-brand" %>
55
+
56
+ <div class="navbar-nav ms-auto">
57
+ <%= link_to "Dashboard", ragdoll.dashboard_index_path, class: "nav-link" %>
58
+ <%= link_to "Documents", ragdoll.documents_path, class: "nav-link" %>
59
+ <%= link_to "Jobs", ragdoll.jobs_path, class: "nav-link" %>
60
+ <%= link_to "Search", ragdoll.search_index_path, class: "nav-link" %>
61
+ <%= link_to "Analytics", ragdoll.analytics_path, class: "nav-link" %>
62
+ <%= link_to "Back to Demo", main_app.root_path, class: "nav-link" %>
63
+ </div>
64
+ </div>
65
+ </nav>
66
+
67
+ <% if flash.any? %>
68
+ <div class="container mt-3">
69
+ <% flash.each do |type, message| %>
70
+ <div class="alert alert-<%= type == 'notice' ? 'success' : type == 'alert' ? 'danger' : 'info' %> alert-dismissible fade show" role="alert">
71
+ <%= message %>
72
+ <button type="button" class="btn-close" data-bs-dismiss="alert"></button>
73
+ </div>
74
+ <% end %>
75
+ </div>
76
+ <% end %>
77
+
78
+ <main>
79
+ <%= yield %>
80
+ </main>
81
+
82
+ <!-- JavaScript with fallback support -->
83
+ <script>
84
+ // Check if CDN resources loaded, provide fallbacks if not
85
+ console.log('🔧 Checking JavaScript dependencies...');
86
+
87
+ // ActionCable fallback and App global setup
88
+ window.App = window.App || {};
89
+
90
+ if (typeof ActionCable === 'undefined') {
91
+ console.warn('ActionCable not loaded from CDN, using minimal fallback');
92
+ window.ActionCable = {
93
+ createConsumer: function(url) {
94
+ console.log('ActionCable fallback: WebSocket functionality disabled');
95
+ return {
96
+ subscriptions: {
97
+ create: function(channel, callbacks) {
98
+ console.log('ActionCable fallback: Channel subscription disabled', channel);
99
+ if (callbacks && callbacks.connected) {
100
+ setTimeout(() => callbacks.connected(), 100);
101
+ }
102
+ return {
103
+ unsubscribe: function() { console.log('ActionCable fallback: Unsubscribe'); }
104
+ };
105
+ }
106
+ },
107
+ disconnect: function() { console.log('ActionCable fallback: Disconnect'); }
108
+ };
109
+ }
110
+ };
111
+ } else {
112
+ console.log('✅ ActionCable loaded from CDN');
113
+ }
114
+
115
+ // Ensure App.cable is available regardless of loading method
116
+ if (!window.App) {
117
+ window.App = {};
118
+ }
119
+
120
+ if (typeof ActionCable !== 'undefined' && !window.App.cable) {
121
+ try {
122
+ window.App.cable = ActionCable.createConsumer('/cable');
123
+ console.log('✅ App.cable initialized from CDN ActionCable');
124
+ console.log('✅ App.cable consumer ready:', window.App.cable);
125
+ } catch (e) {
126
+ console.error('❌ Failed to initialize App.cable:', e);
127
+ }
128
+ } else if (window.App.cable) {
129
+ console.log('✅ App.cable already initialized');
130
+ }
131
+
132
+ console.log('🚀 JavaScript dependencies check complete');
133
+
134
+ // Additional CDN loading diagnostics
135
+ console.log('📊 CDN Resource Status:');
136
+ console.log('- ActionCable CDN loaded:', typeof ActionCable !== 'undefined');
137
+ console.log('- Bootstrap CDN loaded:', typeof bootstrap !== 'undefined');
138
+ console.log('- Chart.js CDN loaded:', typeof Chart !== 'undefined');
139
+
140
+ console.log('🔧 Fallback system status:');
141
+ console.log('- ActionCable fallback:', window.ActionCable ? 'active' : 'inactive');
142
+
143
+ // Initialize Rails UJS
144
+ if (typeof Rails !== 'undefined') {
145
+ Rails.start();
146
+ console.log('✅ Rails UJS initialized');
147
+ } else {
148
+ console.warn('⚠️ Rails UJS not available');
149
+ }
150
+ </script>
151
+
152
+ <!-- JavaScript from CDN -->
153
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
154
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
155
+
156
+ <%= javascript_importmap_tags %>
157
+ <%= javascript_include_tag 'ragdoll/application', 'data-turbo-track': 'reload' %>
158
+ <%= javascript_include_tag 'ragdoll/bulk_upload_status', 'data-turbo-track': 'reload' %>
159
+
160
+ <%= yield :javascript %>
161
+ </body>
162
+ </html>