nats_pubsub 1.0.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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/exe/nats_pubsub +44 -0
  3. data/lib/generators/nats_pubsub/config/config_generator.rb +174 -0
  4. data/lib/generators/nats_pubsub/config/templates/env.example.tt +46 -0
  5. data/lib/generators/nats_pubsub/config/templates/nats_pubsub.rb.tt +105 -0
  6. data/lib/generators/nats_pubsub/initializer/initializer_generator.rb +36 -0
  7. data/lib/generators/nats_pubsub/initializer/templates/nats_pubsub.rb +27 -0
  8. data/lib/generators/nats_pubsub/install/install_generator.rb +75 -0
  9. data/lib/generators/nats_pubsub/migrations/migrations_generator.rb +74 -0
  10. data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_inbox.rb.erb +88 -0
  11. data/lib/generators/nats_pubsub/migrations/templates/create_nats_pubsub_outbox.rb.erb +81 -0
  12. data/lib/generators/nats_pubsub/subscriber/subscriber_generator.rb +139 -0
  13. data/lib/generators/nats_pubsub/subscriber/templates/subscriber.rb.tt +117 -0
  14. data/lib/generators/nats_pubsub/subscriber/templates/subscriber_spec.rb.tt +116 -0
  15. data/lib/generators/nats_pubsub/subscriber/templates/subscriber_test.rb.tt +117 -0
  16. data/lib/nats_pubsub/active_record/publishable.rb +192 -0
  17. data/lib/nats_pubsub/cli.rb +105 -0
  18. data/lib/nats_pubsub/core/base_repository.rb +73 -0
  19. data/lib/nats_pubsub/core/config.rb +152 -0
  20. data/lib/nats_pubsub/core/config_presets.rb +139 -0
  21. data/lib/nats_pubsub/core/connection.rb +103 -0
  22. data/lib/nats_pubsub/core/constants.rb +190 -0
  23. data/lib/nats_pubsub/core/duration.rb +113 -0
  24. data/lib/nats_pubsub/core/error_action.rb +288 -0
  25. data/lib/nats_pubsub/core/event.rb +275 -0
  26. data/lib/nats_pubsub/core/health_check.rb +470 -0
  27. data/lib/nats_pubsub/core/logging.rb +72 -0
  28. data/lib/nats_pubsub/core/message_context.rb +193 -0
  29. data/lib/nats_pubsub/core/presets.rb +222 -0
  30. data/lib/nats_pubsub/core/retry_strategy.rb +71 -0
  31. data/lib/nats_pubsub/core/structured_logger.rb +141 -0
  32. data/lib/nats_pubsub/core/subject.rb +185 -0
  33. data/lib/nats_pubsub/instrumentation.rb +327 -0
  34. data/lib/nats_pubsub/middleware/active_record.rb +18 -0
  35. data/lib/nats_pubsub/middleware/chain.rb +92 -0
  36. data/lib/nats_pubsub/middleware/logging.rb +48 -0
  37. data/lib/nats_pubsub/middleware/retry_logger.rb +24 -0
  38. data/lib/nats_pubsub/middleware/structured_logging.rb +57 -0
  39. data/lib/nats_pubsub/models/event_model.rb +73 -0
  40. data/lib/nats_pubsub/models/inbox_event.rb +109 -0
  41. data/lib/nats_pubsub/models/model_codec_setup.rb +61 -0
  42. data/lib/nats_pubsub/models/model_utils.rb +57 -0
  43. data/lib/nats_pubsub/models/outbox_event.rb +113 -0
  44. data/lib/nats_pubsub/publisher/envelope_builder.rb +99 -0
  45. data/lib/nats_pubsub/publisher/fluent_batch.rb +262 -0
  46. data/lib/nats_pubsub/publisher/outbox_publisher.rb +97 -0
  47. data/lib/nats_pubsub/publisher/outbox_repository.rb +117 -0
  48. data/lib/nats_pubsub/publisher/publish_argument_parser.rb +108 -0
  49. data/lib/nats_pubsub/publisher/publish_result.rb +149 -0
  50. data/lib/nats_pubsub/publisher/publisher.rb +156 -0
  51. data/lib/nats_pubsub/rails/health_endpoint.rb +239 -0
  52. data/lib/nats_pubsub/railtie.rb +52 -0
  53. data/lib/nats_pubsub/subscribers/dlq_handler.rb +69 -0
  54. data/lib/nats_pubsub/subscribers/error_context.rb +137 -0
  55. data/lib/nats_pubsub/subscribers/error_handler.rb +110 -0
  56. data/lib/nats_pubsub/subscribers/graceful_shutdown.rb +128 -0
  57. data/lib/nats_pubsub/subscribers/inbox/inbox_message.rb +79 -0
  58. data/lib/nats_pubsub/subscribers/inbox/inbox_processor.rb +53 -0
  59. data/lib/nats_pubsub/subscribers/inbox/inbox_repository.rb +74 -0
  60. data/lib/nats_pubsub/subscribers/message_context.rb +86 -0
  61. data/lib/nats_pubsub/subscribers/message_processor.rb +225 -0
  62. data/lib/nats_pubsub/subscribers/message_router.rb +77 -0
  63. data/lib/nats_pubsub/subscribers/pool.rb +166 -0
  64. data/lib/nats_pubsub/subscribers/registry.rb +114 -0
  65. data/lib/nats_pubsub/subscribers/subscriber.rb +186 -0
  66. data/lib/nats_pubsub/subscribers/subscription_manager.rb +206 -0
  67. data/lib/nats_pubsub/subscribers/worker.rb +152 -0
  68. data/lib/nats_pubsub/tasks/install.rake +10 -0
  69. data/lib/nats_pubsub/testing/helpers.rb +199 -0
  70. data/lib/nats_pubsub/testing/matchers.rb +208 -0
  71. data/lib/nats_pubsub/testing/test_harness.rb +250 -0
  72. data/lib/nats_pubsub/testing.rb +157 -0
  73. data/lib/nats_pubsub/topology/overlap_guard.rb +88 -0
  74. data/lib/nats_pubsub/topology/stream.rb +102 -0
  75. data/lib/nats_pubsub/topology/stream_support.rb +170 -0
  76. data/lib/nats_pubsub/topology/subject_matcher.rb +77 -0
  77. data/lib/nats_pubsub/topology/topology.rb +24 -0
  78. data/lib/nats_pubsub/version.rb +8 -0
  79. data/lib/nats_pubsub/web/views/dashboard.erb +55 -0
  80. data/lib/nats_pubsub/web/views/inbox_detail.erb +91 -0
  81. data/lib/nats_pubsub/web/views/inbox_list.erb +62 -0
  82. data/lib/nats_pubsub/web/views/layout.erb +68 -0
  83. data/lib/nats_pubsub/web/views/outbox_detail.erb +77 -0
  84. data/lib/nats_pubsub/web/views/outbox_list.erb +62 -0
  85. data/lib/nats_pubsub/web.rb +181 -0
  86. data/lib/nats_pubsub.rb +290 -0
  87. metadata +225 -0
@@ -0,0 +1,91 @@
1
+ <h2>Inbox Event Details</h2>
2
+
3
+ <div class="card">
4
+ <div class="detail-row">
5
+ <div class="detail-label">Event ID</div>
6
+ <div><%= @event.event_id %></div>
7
+ </div>
8
+
9
+ <div class="detail-row">
10
+ <div class="detail-label">Subject</div>
11
+ <div><%= @event.subject %></div>
12
+ </div>
13
+
14
+ <div class="detail-row">
15
+ <div class="detail-label">Status</div>
16
+ <div><span class="badge <%= status_badge_class(@event.status) %>"><%= @event.status %></span></div>
17
+ </div>
18
+
19
+ <% if @event.respond_to?(:stream) && @event.stream %>
20
+ <div class="detail-row">
21
+ <div class="detail-label">Stream</div>
22
+ <div><%= @event.stream %></div>
23
+ </div>
24
+ <% end %>
25
+
26
+ <% if @event.respond_to?(:stream_seq) && @event.stream_seq %>
27
+ <div class="detail-row">
28
+ <div class="detail-label">Stream Sequence</div>
29
+ <div><%= @event.stream_seq %></div>
30
+ </div>
31
+ <% end %>
32
+
33
+ <% if @event.respond_to?(:deliveries) %>
34
+ <div class="detail-row">
35
+ <div class="detail-label">Deliveries</div>
36
+ <div><%= @event.deliveries %></div>
37
+ </div>
38
+ <% end %>
39
+
40
+ <% if @event.respond_to?(:received_at) %>
41
+ <div class="detail-row">
42
+ <div class="detail-label">Received At</div>
43
+ <div><%= format_time(@event.received_at) %></div>
44
+ </div>
45
+ <% end %>
46
+
47
+ <% if @event.respond_to?(:processed_at) %>
48
+ <div class="detail-row">
49
+ <div class="detail-label">Processed At</div>
50
+ <div><%= format_time(@event.processed_at) %></div>
51
+ </div>
52
+ <% end %>
53
+
54
+ <div class="detail-row">
55
+ <div class="detail-label">Created At</div>
56
+ <div><%= format_time(@event.created_at) %></div>
57
+ </div>
58
+
59
+ <div class="detail-row">
60
+ <div class="detail-label">Updated At</div>
61
+ <div><%= format_time(@event.updated_at) %></div>
62
+ </div>
63
+
64
+ <% if @event.respond_to?(:last_error) && @event.last_error %>
65
+ <div class="detail-row">
66
+ <div class="detail-label">Last Error</div>
67
+ <div style="color: #e74c3c;"><%= @event.last_error %></div>
68
+ </div>
69
+ <% end %>
70
+
71
+ <h3 style="margin-top: 30px; margin-bottom: 15px;">Payload</h3>
72
+ <pre><%= JSON.pretty_generate(@event.payload) %></pre>
73
+
74
+ <% if @event.respond_to?(:headers) && @event.headers %>
75
+ <h3 style="margin-top: 30px; margin-bottom: 15px;">Headers</h3>
76
+ <pre><%= JSON.pretty_generate(@event.headers) %></pre>
77
+ <% end %>
78
+
79
+ <div style="margin-top: 30px; display: flex; gap: 10px;">
80
+ <a href="/inbox" class="btn btn-secondary">Back to List</a>
81
+ <% if @event.status == 'failed' %>
82
+ <form method="post" action="/inbox/<%= @event.id %>/reprocess" style="display: inline;">
83
+ <button type="submit" class="btn btn-primary">Reprocess</button>
84
+ </form>
85
+ <% end %>
86
+ <form method="post" action="/inbox/<%= @event.id %>" style="display: inline;">
87
+ <input type="hidden" name="_method" value="DELETE">
88
+ <button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this event?')">Delete</button>
89
+ </form>
90
+ </div>
91
+ </div>
@@ -0,0 +1,62 @@
1
+ <h2>Inbox Events</h2>
2
+
3
+ <div class="filters">
4
+ <form method="get" action="/inbox" style="display: flex; gap: 10px;">
5
+ <select name="status" onchange="this.form.submit()">
6
+ <option value="">All Statuses</option>
7
+ <option value="received" <%= 'selected' if @status_filter == 'received' %>>Received</option>
8
+ <option value="processing" <%= 'selected' if @status_filter == 'processing' %>>Processing</option>
9
+ <option value="processed" <%= 'selected' if @status_filter == 'processed' %>>Processed</option>
10
+ <option value="failed" <%= 'selected' if @status_filter == 'failed' %>>Failed</option>
11
+ </select>
12
+ </form>
13
+ </div>
14
+
15
+ <div class="card">
16
+ <table>
17
+ <thead>
18
+ <tr>
19
+ <th>Event ID</th>
20
+ <th>Subject</th>
21
+ <th>Status</th>
22
+ <th>Deliveries</th>
23
+ <th>Received At</th>
24
+ <th>Actions</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <% if @events.any? %>
29
+ <% @events.each do |event| %>
30
+ <tr>
31
+ <td><a href="/inbox/<%= event.id %>"><%= truncate(event.event_id, 30) %></a></td>
32
+ <td><%= truncate(event.subject, 40) %></td>
33
+ <td><span class="badge <%= status_badge_class(event.status) %>"><%= event.status %></span></td>
34
+ <td><%= event.respond_to?(:deliveries) ? event.deliveries : 'N/A' %></td>
35
+ <td><%= format_time(event.respond_to?(:received_at) ? event.received_at : event.created_at) %></td>
36
+ <td>
37
+ <a href="/inbox/<%= event.id %>" class="btn btn-secondary" style="padding: 4px 8px;">View</a>
38
+ </td>
39
+ </tr>
40
+ <% end %>
41
+ <% else %>
42
+ <tr>
43
+ <td colspan="6" style="text-align: center; padding: 40px;">No events found</td>
44
+ </tr>
45
+ <% end %>
46
+ </tbody>
47
+ </table>
48
+
49
+ <% if @total_pages > 1 %>
50
+ <div class="pagination">
51
+ <% if @page > 1 %>
52
+ <a href="?page=<%= @page - 1 %><%= "&status=#{@status_filter}" if @status_filter %>">« Previous</a>
53
+ <% end %>
54
+
55
+ <span>Page <%= @page %> of <%= @total_pages %></span>
56
+
57
+ <% if @page < @total_pages %>
58
+ <a href="?page=<%= @page + 1 %><%= "&status=#{@status_filter}" if @status_filter %>">Next »</a>
59
+ <% end %>
60
+ </div>
61
+ <% end %>
62
+ </div>
@@ -0,0 +1,68 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NatsPubsub Web UI</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: #f5f5f5; color: #333; }
10
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
11
+ header { background: #2c3e50; color: white; padding: 20px 0; margin-bottom: 30px; }
12
+ header h1 { font-size: 24px; font-weight: 600; }
13
+ header .container { display: flex; justify-content: space-between; align-items: center; }
14
+ nav { display: flex; gap: 20px; }
15
+ nav a { color: white; text-decoration: none; padding: 8px 16px; border-radius: 4px; }
16
+ nav a:hover, nav a.active { background: rgba(255,255,255,0.1); }
17
+ .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
18
+ .stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
19
+ .stat-card h3 { font-size: 14px; color: #666; margin-bottom: 10px; text-transform: uppercase; }
20
+ .stat-card .value { font-size: 32px; font-weight: bold; color: #2c3e50; }
21
+ .stat-card.success .value { color: #27ae60; }
22
+ .stat-card.danger .value { color: #e74c3c; }
23
+ .stat-card.warning .value { color: #f39c12; }
24
+ .card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 20px; margin-bottom: 20px; }
25
+ .card h2 { margin-bottom: 20px; color: #2c3e50; }
26
+ table { width: 100%; border-collapse: collapse; }
27
+ table th, table td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
28
+ table th { background: #f8f9fa; font-weight: 600; color: #666; font-size: 12px; text-transform: uppercase; }
29
+ table tr:hover { background: #f8f9fa; }
30
+ .badge { display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; }
31
+ .badge.success { background: #d4edda; color: #155724; }
32
+ .badge.danger { background: #f8d7da; color: #721c24; }
33
+ .badge.warning { background: #fff3cd; color: #856404; }
34
+ .badge.secondary { background: #e2e3e5; color: #383d41; }
35
+ .btn { display: inline-block; padding: 8px 16px; border-radius: 4px; text-decoration: none; border: none; cursor: pointer; font-size: 14px; }
36
+ .btn-primary { background: #3498db; color: white; }
37
+ .btn-danger { background: #e74c3c; color: white; }
38
+ .btn-secondary { background: #95a5a6; color: white; }
39
+ .btn:hover { opacity: 0.9; }
40
+ .pagination { display: flex; gap: 10px; justify-content: center; margin-top: 20px; }
41
+ .pagination a, .pagination span { padding: 8px 12px; border-radius: 4px; text-decoration: none; background: white; color: #333; }
42
+ .pagination a:hover { background: #3498db; color: white; }
43
+ .pagination span { background: #3498db; color: white; }
44
+ .filters { margin-bottom: 20px; display: flex; gap: 10px; }
45
+ .filters select { padding: 8px; border-radius: 4px; border: 1px solid #ddd; }
46
+ pre { background: #f4f4f4; padding: 15px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
47
+ .detail-row { display: grid; grid-template-columns: 200px 1fr; gap: 15px; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #eee; }
48
+ .detail-row:last-child { border-bottom: none; }
49
+ .detail-label { font-weight: 600; color: #666; }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <header>
54
+ <div class="container">
55
+ <h1>🚀 NatsPubsub</h1>
56
+ <nav>
57
+ <a href="/">Dashboard</a>
58
+ <a href="/outbox">Outbox</a>
59
+ <a href="/inbox">Inbox</a>
60
+ </nav>
61
+ </div>
62
+ </header>
63
+
64
+ <div class="container">
65
+ <%= yield %>
66
+ </div>
67
+ </body>
68
+ </html>
@@ -0,0 +1,77 @@
1
+ <h2>Outbox Event Details</h2>
2
+
3
+ <div class="card">
4
+ <div class="detail-row">
5
+ <div class="detail-label">Event ID</div>
6
+ <div><%= @event.event_id %></div>
7
+ </div>
8
+
9
+ <div class="detail-row">
10
+ <div class="detail-label">Subject</div>
11
+ <div><%= @event.subject %></div>
12
+ </div>
13
+
14
+ <div class="detail-row">
15
+ <div class="detail-label">Status</div>
16
+ <div><span class="badge <%= status_badge_class(@event.status) %>"><%= @event.status %></span></div>
17
+ </div>
18
+
19
+ <% if @event.respond_to?(:attempts) %>
20
+ <div class="detail-row">
21
+ <div class="detail-label">Attempts</div>
22
+ <div><%= @event.attempts %></div>
23
+ </div>
24
+ <% end %>
25
+
26
+ <% if @event.respond_to?(:enqueued_at) %>
27
+ <div class="detail-row">
28
+ <div class="detail-label">Enqueued At</div>
29
+ <div><%= format_time(@event.enqueued_at) %></div>
30
+ </div>
31
+ <% end %>
32
+
33
+ <% if @event.respond_to?(:sent_at) %>
34
+ <div class="detail-row">
35
+ <div class="detail-label">Sent At</div>
36
+ <div><%= format_time(@event.sent_at) %></div>
37
+ </div>
38
+ <% end %>
39
+
40
+ <div class="detail-row">
41
+ <div class="detail-label">Created At</div>
42
+ <div><%= format_time(@event.created_at) %></div>
43
+ </div>
44
+
45
+ <div class="detail-row">
46
+ <div class="detail-label">Updated At</div>
47
+ <div><%= format_time(@event.updated_at) %></div>
48
+ </div>
49
+
50
+ <% if @event.respond_to?(:last_error) && @event.last_error %>
51
+ <div class="detail-row">
52
+ <div class="detail-label">Last Error</div>
53
+ <div style="color: #e74c3c;"><%= @event.last_error %></div>
54
+ </div>
55
+ <% end %>
56
+
57
+ <h3 style="margin-top: 30px; margin-bottom: 15px;">Payload</h3>
58
+ <pre><%= JSON.pretty_generate(@event.payload) %></pre>
59
+
60
+ <% if @event.respond_to?(:headers) && @event.headers %>
61
+ <h3 style="margin-top: 30px; margin-bottom: 15px;">Headers</h3>
62
+ <pre><%= JSON.pretty_generate(@event.headers) %></pre>
63
+ <% end %>
64
+
65
+ <div style="margin-top: 30px; display: flex; gap: 10px;">
66
+ <a href="/outbox" class="btn btn-secondary">Back to List</a>
67
+ <% if @event.status == 'failed' %>
68
+ <form method="post" action="/outbox/<%= @event.id %>/retry" style="display: inline;">
69
+ <button type="submit" class="btn btn-primary">Retry</button>
70
+ </form>
71
+ <% end %>
72
+ <form method="post" action="/outbox/<%= @event.id %>" style="display: inline;">
73
+ <input type="hidden" name="_method" value="DELETE">
74
+ <button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this event?')">Delete</button>
75
+ </form>
76
+ </div>
77
+ </div>
@@ -0,0 +1,62 @@
1
+ <h2>Outbox Events</h2>
2
+
3
+ <div class="filters">
4
+ <form method="get" action="/outbox" style="display: flex; gap: 10px;">
5
+ <select name="status" onchange="this.form.submit()">
6
+ <option value="">All Statuses</option>
7
+ <option value="pending" <%= 'selected' if @status_filter == 'pending' %>>Pending</option>
8
+ <option value="publishing" <%= 'selected' if @status_filter == 'publishing' %>>Publishing</option>
9
+ <option value="sent" <%= 'selected' if @status_filter == 'sent' %>>Sent</option>
10
+ <option value="failed" <%= 'selected' if @status_filter == 'failed' %>>Failed</option>
11
+ </select>
12
+ </form>
13
+ </div>
14
+
15
+ <div class="card">
16
+ <table>
17
+ <thead>
18
+ <tr>
19
+ <th>Event ID</th>
20
+ <th>Subject</th>
21
+ <th>Status</th>
22
+ <th>Attempts</th>
23
+ <th>Created At</th>
24
+ <th>Actions</th>
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ <% if @events.any? %>
29
+ <% @events.each do |event| %>
30
+ <tr>
31
+ <td><a href="/outbox/<%= event.id %>"><%= truncate(event.event_id, 30) %></a></td>
32
+ <td><%= truncate(event.subject, 40) %></td>
33
+ <td><span class="badge <%= status_badge_class(event.status) %>"><%= event.status %></span></td>
34
+ <td><%= event.respond_to?(:attempts) ? event.attempts : 'N/A' %></td>
35
+ <td><%= format_time(event.created_at) %></td>
36
+ <td>
37
+ <a href="/outbox/<%= event.id %>" class="btn btn-secondary" style="padding: 4px 8px;">View</a>
38
+ </td>
39
+ </tr>
40
+ <% end %>
41
+ <% else %>
42
+ <tr>
43
+ <td colspan="6" style="text-align: center; padding: 40px;">No events found</td>
44
+ </tr>
45
+ <% end %>
46
+ </tbody>
47
+ </table>
48
+
49
+ <% if @total_pages > 1 %>
50
+ <div class="pagination">
51
+ <% if @page > 1 %>
52
+ <a href="?page=<%= @page - 1 %><%= "&status=#{@status_filter}" if @status_filter %>">« Previous</a>
53
+ <% end %>
54
+
55
+ <span>Page <%= @page %> of <%= @total_pages %></span>
56
+
57
+ <% if @page < @total_pages %>
58
+ <a href="?page=<%= @page + 1 %><%= "&status=#{@status_filter}" if @status_filter %>">Next »</a>
59
+ <% end %>
60
+ </div>
61
+ <% end %>
62
+ </div>
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require 'json'
5
+ require 'erb'
6
+ require_relative 'models/model_utils'
7
+
8
+ module NatsPubsub
9
+ # Web UI for monitoring Inbox and Outbox events
10
+ # Mount this in your Rails routes or run standalone
11
+ #
12
+ # Example (Rails):
13
+ # mount NatsPubsub::Web, at: '/jetstream'
14
+ #
15
+ # Example (Standalone):
16
+ # run NatsPubsub::Web
17
+ class Web < Sinatra::Base
18
+ set :root, File.expand_path('../..', __dir__)
19
+ set :views, proc { File.join(root, 'lib/nats_pubsub/web/views') }
20
+ set :public_folder, proc { File.join(root, 'lib/nats_pubsub/web/public') }
21
+
22
+ helpers do
23
+ def outbox_model
24
+ @outbox_model ||= ModelUtils.constantize(NatsPubsub.config.outbox_model)
25
+ end
26
+
27
+ def inbox_model
28
+ @inbox_model ||= ModelUtils.constantize(NatsPubsub.config.inbox_model)
29
+ end
30
+
31
+ def format_time(time)
32
+ return 'N/A' unless time
33
+
34
+ time.strftime('%Y-%m-%d %H:%M:%S %Z')
35
+ end
36
+
37
+ def status_badge_class(status)
38
+ case status.to_s.downcase
39
+ when 'sent', 'processed'
40
+ 'success'
41
+ when 'failed'
42
+ 'danger'
43
+ when 'publishing', 'processing'
44
+ 'warning'
45
+ else
46
+ 'secondary'
47
+ end
48
+ end
49
+
50
+ def truncate(text, length = 50)
51
+ return '' unless text
52
+
53
+ text = text.to_s
54
+ text.length > length ? "#{text[0...length]}..." : text
55
+ end
56
+ end
57
+
58
+ # Dashboard
59
+ get '/' do
60
+ @outbox_stats = calculate_outbox_stats
61
+ @inbox_stats = calculate_inbox_stats
62
+ erb :dashboard
63
+ end
64
+
65
+ # Outbox list
66
+ get '/outbox' do
67
+ @page = (params[:page] || 1).to_i
68
+ @per_page = 50
69
+ @status_filter = params[:status]
70
+
71
+ query = outbox_model
72
+ query = query.where(status: @status_filter) if @status_filter
73
+ query = query.order(created_at: :desc)
74
+
75
+ @total = query.count
76
+ @events = query.offset((@page - 1) * @per_page).limit(@per_page)
77
+ @total_pages = (@total.to_f / @per_page).ceil
78
+
79
+ erb :outbox_list
80
+ end
81
+
82
+ # Outbox detail
83
+ get '/outbox/:id' do
84
+ @event = outbox_model.find(params[:id])
85
+ erb :outbox_detail
86
+ end
87
+
88
+ # Retry outbox event
89
+ post '/outbox/:id/retry' do
90
+ event = outbox_model.find(params[:id])
91
+ event.update!(status: 'pending', last_error: nil)
92
+
93
+ redirect back
94
+ end
95
+
96
+ # Delete outbox event
97
+ delete '/outbox/:id' do
98
+ event = outbox_model.find(params[:id])
99
+ event.destroy
100
+
101
+ redirect '/outbox'
102
+ end
103
+
104
+ # Inbox list
105
+ get '/inbox' do
106
+ @page = (params[:page] || 1).to_i
107
+ @per_page = 50
108
+ @status_filter = params[:status]
109
+
110
+ query = inbox_model
111
+ query = query.where(status: @status_filter) if @status_filter
112
+ query = query.order(created_at: :desc)
113
+
114
+ @total = query.count
115
+ @events = query.offset((@page - 1) * @per_page).limit(@per_page)
116
+ @total_pages = (@total.to_f / @per_page).ceil
117
+
118
+ erb :inbox_list
119
+ end
120
+
121
+ # Inbox detail
122
+ get '/inbox/:id' do
123
+ @event = inbox_model.find(params[:id])
124
+ erb :inbox_detail
125
+ end
126
+
127
+ # Reprocess inbox event
128
+ post '/inbox/:id/reprocess' do
129
+ event = inbox_model.find(params[:id])
130
+ event.update!(status: 'received', last_error: nil)
131
+
132
+ redirect back
133
+ end
134
+
135
+ # Delete inbox event
136
+ delete '/inbox/:id' do
137
+ event = inbox_model.find(params[:id])
138
+ event.destroy
139
+
140
+ redirect '/inbox'
141
+ end
142
+
143
+ # Health check
144
+ get '/health' do
145
+ content_type :json
146
+ { status: 'ok', timestamp: Time.now.utc }.to_json
147
+ end
148
+
149
+ private
150
+
151
+ def calculate_outbox_stats
152
+ return {} unless ModelUtils.ar_class?(outbox_model)
153
+
154
+ {
155
+ total: outbox_model.count,
156
+ pending: outbox_model.where(status: 'pending').count,
157
+ publishing: outbox_model.where(status: 'publishing').count,
158
+ sent: outbox_model.where(status: 'sent').count,
159
+ failed: outbox_model.where(status: 'failed').count
160
+ }
161
+ rescue StandardError => e
162
+ Logging.error("Failed to calculate outbox stats: #{e.message}")
163
+ {}
164
+ end
165
+
166
+ def calculate_inbox_stats
167
+ return {} unless ModelUtils.ar_class?(inbox_model)
168
+
169
+ {
170
+ total: inbox_model.count,
171
+ received: inbox_model.where(status: 'received').count,
172
+ processing: inbox_model.where(status: 'processing').count,
173
+ processed: inbox_model.where(status: 'processed').count,
174
+ failed: inbox_model.where(status: 'failed').count
175
+ }
176
+ rescue StandardError => e
177
+ Logging.error("Failed to calculate inbox stats: #{e.message}")
178
+ {}
179
+ end
180
+ end
181
+ end