solid_ops 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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/CHANGELOG.md +5 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +308 -0
  7. data/Rakefile +12 -0
  8. data/app/assets/stylesheets/solid_ops/application.css +1 -0
  9. data/app/controllers/solid_ops/application_controller.rb +127 -0
  10. data/app/controllers/solid_ops/cache_entries_controller.rb +38 -0
  11. data/app/controllers/solid_ops/channels_controller.rb +30 -0
  12. data/app/controllers/solid_ops/dashboard_controller.rb +80 -0
  13. data/app/controllers/solid_ops/events_controller.rb +37 -0
  14. data/app/controllers/solid_ops/jobs_controller.rb +64 -0
  15. data/app/controllers/solid_ops/processes_controller.rb +11 -0
  16. data/app/controllers/solid_ops/queues_controller.rb +75 -0
  17. data/app/controllers/solid_ops/recurring_tasks_controller.rb +11 -0
  18. data/app/helpers/solid_ops/application_helper.rb +112 -0
  19. data/app/jobs/solid_ops/purge_job.rb +16 -0
  20. data/app/models/solid_ops/event.rb +34 -0
  21. data/app/views/layouts/solid_ops/application.html.erb +118 -0
  22. data/app/views/solid_ops/cache_entries/index.html.erb +86 -0
  23. data/app/views/solid_ops/cache_entries/show.html.erb +153 -0
  24. data/app/views/solid_ops/channels/index.html.erb +81 -0
  25. data/app/views/solid_ops/channels/show.html.erb +66 -0
  26. data/app/views/solid_ops/dashboard/cable.html.erb +98 -0
  27. data/app/views/solid_ops/dashboard/cache.html.erb +104 -0
  28. data/app/views/solid_ops/dashboard/index.html.erb +169 -0
  29. data/app/views/solid_ops/dashboard/jobs.html.erb +108 -0
  30. data/app/views/solid_ops/events/index.html.erb +98 -0
  31. data/app/views/solid_ops/events/show.html.erb +108 -0
  32. data/app/views/solid_ops/jobs/failed.html.erb +89 -0
  33. data/app/views/solid_ops/jobs/running.html.erb +134 -0
  34. data/app/views/solid_ops/jobs/show.html.erb +116 -0
  35. data/app/views/solid_ops/processes/index.html.erb +69 -0
  36. data/app/views/solid_ops/queues/index.html.erb +182 -0
  37. data/app/views/solid_ops/queues/show.html.erb +121 -0
  38. data/app/views/solid_ops/recurring_tasks/index.html.erb +64 -0
  39. data/app/views/solid_ops/shared/_nav.html.erb +50 -0
  40. data/app/views/solid_ops/shared/_pagination.html.erb +31 -0
  41. data/app/views/solid_ops/shared/_time_window.html.erb +10 -0
  42. data/app/views/solid_ops/shared/component_unavailable.html.erb +63 -0
  43. data/config/routes.rb +49 -0
  44. data/db/migrate/20260224000100_create_solid_ops_events.rb +31 -0
  45. data/lib/generators/solid_ops/install/install_generator.rb +348 -0
  46. data/lib/generators/solid_ops/install/templates/create_solid_ops_events.rb +31 -0
  47. data/lib/generators/solid_ops/install/templates/solid_ops_initializer.rb +31 -0
  48. data/lib/solid_ops/configuration.rb +28 -0
  49. data/lib/solid_ops/context.rb +34 -0
  50. data/lib/solid_ops/current.rb +10 -0
  51. data/lib/solid_ops/engine.rb +60 -0
  52. data/lib/solid_ops/job_extension.rb +50 -0
  53. data/lib/solid_ops/middleware.rb +52 -0
  54. data/lib/solid_ops/subscribers.rb +215 -0
  55. data/lib/solid_ops/version.rb +5 -0
  56. data/lib/solid_ops.rb +25 -0
  57. data/lib/tasks/solid_ops.rake +32 -0
  58. data/log/test.log +2 -0
  59. data/sig/solid_ops.rbs +4 -0
  60. metadata +119 -0
@@ -0,0 +1,169 @@
1
+
2
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
3
+ <div class="flex items-center justify-between mb-8">
4
+ <div>
5
+ <h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Dashboard</h1>
6
+ <p class="text-sm text-gray-500 mt-1">Observability overview for the Solid Trifecta</p>
7
+ </div>
8
+ <%= render "solid_ops/shared/time_window" %>
9
+ </div>
10
+
11
+ <!-- Summary cards -->
12
+ <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
13
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
14
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-gray-400 to-gray-500"></div>
15
+ <div class="p-5 pt-6">
16
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Total Events</p>
17
+ <p class="text-3xl font-extrabold font-mono mt-2 text-gray-900"><%= number_with_delimiter(@total_events) %></p>
18
+ </div>
19
+ </div>
20
+
21
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
22
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-blue-400 to-blue-600"></div>
23
+ <div class="p-5 pt-6">
24
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Correlations</p>
25
+ <p class="text-3xl font-extrabold font-mono mt-2 text-blue-600"><%= number_with_delimiter(@unique_correlations) %></p>
26
+ <p class="text-xs text-gray-400 mt-1">unique request flows</p>
27
+ </div>
28
+ </div>
29
+
30
+ <% if (job_stat = @stats["job.perform"]) %>
31
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
32
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-emerald-400 to-emerald-600"></div>
33
+ <div class="p-5 pt-6">
34
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Jobs Performed</p>
35
+ <p class="text-3xl font-extrabold font-mono mt-2 text-emerald-600"><%= job_stat.event_count %></p>
36
+ <p class="text-xs text-gray-400 mt-1">avg <%= format_duration(job_stat.avg_duration) %></p>
37
+ </div>
38
+ </div>
39
+ <% end %>
40
+
41
+ <% if (cache_reads = @stats["cache.read"]) %>
42
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
43
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-amber-400 to-amber-500"></div>
44
+ <div class="p-5 pt-6">
45
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Cache Reads</p>
46
+ <p class="text-3xl font-extrabold font-mono mt-2 text-amber-600"><%= cache_reads.event_count %></p>
47
+ <p class="text-xs text-gray-400 mt-1">avg <%= format_duration(cache_reads.avg_duration) %></p>
48
+ </div>
49
+ </div>
50
+ <% end %>
51
+
52
+ <% if (cable_stat = @stats["cable.broadcast"]) %>
53
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
54
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-purple-400 to-purple-600"></div>
55
+ <div class="p-5 pt-6">
56
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Broadcasts</p>
57
+ <p class="text-3xl font-extrabold font-mono mt-2 text-purple-600"><%= cable_stat.event_count %></p>
58
+ <p class="text-xs text-gray-400 mt-1">avg <%= format_duration(cable_stat.avg_duration) %></p>
59
+ </div>
60
+ </div>
61
+ <% end %>
62
+ </div>
63
+
64
+ <!-- Component Status -->
65
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden mb-8">
66
+ <div class="px-6 py-4 border-b border-gray-100">
67
+ <h2 class="text-sm font-bold text-gray-900">Solid Components</h2>
68
+ </div>
69
+ <div class="p-6">
70
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
71
+ <% component_diagnostics.each do |key, diag| %>
72
+ <div class="rounded-xl border <%= diag[:available] ? 'border-emerald-200 bg-gradient-to-br from-emerald-50 to-green-50' : 'border-amber-200 bg-gradient-to-br from-amber-50 to-yellow-50' %> p-5 transition-shadow hover:shadow-sm">
73
+ <div class="flex items-center gap-2.5 mb-2">
74
+ <% if diag[:available] %>
75
+ <div class="w-7 h-7 rounded-lg bg-emerald-100 flex items-center justify-center">
76
+ <svg class="w-4 h-4 text-emerald-600" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>
77
+ </div>
78
+ <% else %>
79
+ <div class="w-7 h-7 rounded-lg bg-amber-100 flex items-center justify-center">
80
+ <svg class="w-4 h-4 text-amber-600" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd"/></svg>
81
+ </div>
82
+ <% end %>
83
+ <span class="text-sm font-bold <%= diag[:available] ? 'text-emerald-900' : 'text-amber-900' %>">
84
+ Solid <%= key.to_s.capitalize %>
85
+ </span>
86
+ </div>
87
+ <p class="text-xs <%= diag[:available] ? 'text-emerald-700' : 'text-amber-700' %> font-mono break-all leading-relaxed">
88
+ <%= diag[:reason] %>
89
+ </p>
90
+ </div>
91
+ <% end %>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Event breakdown -->
97
+ <% if @stats.any? %>
98
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm mb-8 overflow-hidden">
99
+ <div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
100
+ <h2 class="text-sm font-bold text-gray-900">Event Breakdown</h2>
101
+ <span class="text-xs text-gray-400"><%= @stats.size %> event types</span>
102
+ </div>
103
+ <table class="min-w-full divide-y divide-gray-100">
104
+ <thead>
105
+ <tr class="bg-gray-50">
106
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Type</th>
107
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Count</th>
108
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Avg Duration</th>
109
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Max Duration</th>
110
+ </tr>
111
+ </thead>
112
+ <tbody class="divide-y divide-gray-50">
113
+ <% @stats.each do |type, stat| %>
114
+ <tr class="hover:bg-gray-50/80 transition-colors">
115
+ <td class="px-6 py-3.5"><span class="<%= event_pill_class(type) %>"><%= type %></span></td>
116
+ <td class="px-6 py-3.5 font-mono text-sm font-semibold text-gray-900"><%= stat.event_count %></td>
117
+ <td class="px-6 py-3.5 font-mono text-sm text-gray-500"><%= format_duration(stat.avg_duration) %></td>
118
+ <td class="px-6 py-3.5 font-mono text-sm text-gray-500"><%= format_duration(stat.max_duration) %></td>
119
+ </tr>
120
+ <% end %>
121
+ </tbody>
122
+ </table>
123
+ </div>
124
+ <% end %>
125
+
126
+ <!-- Recent events -->
127
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden mb-8">
128
+ <div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
129
+ <h2 class="text-sm font-bold text-gray-900">Recent Events</h2>
130
+ <a href="<%= solid_ops.events_path %>" class="inline-flex items-center gap-1 text-xs font-medium text-blue-600 hover:text-blue-700 transition-colors">
131
+ View all
132
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
133
+ </a>
134
+ </div>
135
+ <% if @recent_events.any? %>
136
+ <table class="min-w-full divide-y divide-gray-100">
137
+ <thead>
138
+ <tr class="bg-gray-50">
139
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Time</th>
140
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Type</th>
141
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Name</th>
142
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Duration</th>
143
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Correlation</th>
144
+ </tr>
145
+ </thead>
146
+ <tbody class="divide-y divide-gray-50">
147
+ <% @recent_events.each do |e| %>
148
+ <tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.event_path(e.id) %>'">
149
+ <td class="px-6 py-3.5 font-mono text-xs text-blue-600 font-medium"><%= format_time(e.occurred_at) %></td>
150
+ <td class="px-6 py-3.5"><span class="<%= event_pill_class(e.event_type) %>"><%= e.event_type %></span></td>
151
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-700 max-w-xs truncate"><%= e.name %></td>
152
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= format_duration(e.duration_ms) %></td>
153
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-400"><%= e.correlation_id&.first(8) %></td>
154
+ </tr>
155
+ <% end %>
156
+ </tbody>
157
+ </table>
158
+ <% else %>
159
+ <div class="py-20 text-center">
160
+ <div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
161
+ <svg class="h-7 w-7 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/></svg>
162
+ </div>
163
+ <p class="text-sm text-gray-500 font-medium">No events in this time window</p>
164
+ <p class="text-xs text-gray-400 mt-1">Try selecting a larger time range above</p>
165
+ </div>
166
+ <% end %>
167
+ </div>
168
+ </div>
169
+
@@ -0,0 +1,108 @@
1
+
2
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
3
+ <div class="flex items-center justify-between mb-8">
4
+ <div>
5
+ <p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Dashboard</p>
6
+ <h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Job Events</h1>
7
+ <p class="text-sm text-gray-500 mt-1">ActiveJob performance observed via instrumentation</p>
8
+ </div>
9
+ <%= render "solid_ops/shared/time_window" %>
10
+ </div>
11
+
12
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
13
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
14
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-blue-400 to-blue-600"></div>
15
+ <div class="p-5 pt-6">
16
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Enqueued</p>
17
+ <p class="text-3xl font-extrabold font-mono mt-2 text-blue-600"><%= number_with_delimiter(@enqueue_count) %></p>
18
+ </div>
19
+ </div>
20
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
21
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-emerald-400 to-emerald-600"></div>
22
+ <div class="p-5 pt-6">
23
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Performed</p>
24
+ <p class="text-3xl font-extrabold font-mono mt-2 text-emerald-600"><%= number_with_delimiter(@perform_count) %></p>
25
+ <p class="text-xs text-gray-400 mt-1">avg <%= format_duration(@avg_perform_ms) %></p>
26
+ </div>
27
+ </div>
28
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
29
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-gray-300 to-gray-400"></div>
30
+ <div class="p-5 pt-6">
31
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Max Duration</p>
32
+ <p class="text-3xl font-extrabold font-mono mt-2 text-gray-900"><%= format_duration(@max_perform_ms) %></p>
33
+ </div>
34
+ </div>
35
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
36
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r <%= @error_count > 0 ? 'from-red-400 to-red-600' : 'from-gray-200 to-gray-300' %>"></div>
37
+ <div class="p-5 pt-6">
38
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Errors</p>
39
+ <p class="text-3xl font-extrabold font-mono mt-2 <%= @error_count > 0 ? 'text-red-600' : 'text-gray-400' %>"><%= @error_count %></p>
40
+ </div>
41
+ </div>
42
+ </div>
43
+
44
+ <% if @top_jobs.any? %>
45
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm mb-8 overflow-hidden">
46
+ <div class="px-6 py-4 border-b border-gray-100">
47
+ <h2 class="text-sm font-bold text-gray-900">Top Jobs by Volume</h2>
48
+ </div>
49
+ <table class="min-w-full divide-y divide-gray-100">
50
+ <thead>
51
+ <tr class="bg-gray-50">
52
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Job Class</th>
53
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Executions</th>
54
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Avg Duration</th>
55
+ </tr>
56
+ </thead>
57
+ <tbody class="divide-y divide-gray-50">
58
+ <% @top_jobs.each do |j| %>
59
+ <tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.events_path(event_type: 'job.perform', q: j.name) %>'">
60
+ <td class="px-6 py-3.5 font-mono text-sm text-blue-600 font-medium"><%= j.name %></td>
61
+ <td class="px-6 py-3.5 font-mono text-sm font-semibold text-gray-900"><%= j.event_count %></td>
62
+ <td class="px-6 py-3.5 font-mono text-sm text-gray-500"><%= format_duration(j.avg_duration) %></td>
63
+ </tr>
64
+ <% end %>
65
+ </tbody>
66
+ </table>
67
+ </div>
68
+ <% end %>
69
+
70
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
71
+ <div class="px-6 py-4 border-b border-gray-100">
72
+ <h2 class="text-sm font-bold text-gray-900">Recent Job Events</h2>
73
+ </div>
74
+ <% if @recent.any? %>
75
+ <table class="min-w-full divide-y divide-gray-100">
76
+ <thead>
77
+ <tr class="bg-gray-50">
78
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Time</th>
79
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Type</th>
80
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Name</th>
81
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Duration</th>
82
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider">Correlation</th>
83
+ </tr>
84
+ </thead>
85
+ <tbody class="divide-y divide-gray-50">
86
+ <% @recent.each do |e| %>
87
+ <tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.event_path(e.id) %>'">
88
+ <td class="px-6 py-3.5 font-mono text-xs text-blue-600 font-medium"><%= format_time(e.occurred_at) %></td>
89
+ <td class="px-6 py-3.5"><span class="<%= event_pill_class(e.event_type) %>"><%= e.event_type %></span></td>
90
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-700"><%= e.name %></td>
91
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= format_duration(e.duration_ms) %></td>
92
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-400"><%= e.correlation_id&.first(8) %></td>
93
+ </tr>
94
+ <% end %>
95
+ </tbody>
96
+ </table>
97
+ <% else %>
98
+ <div class="py-20 text-center">
99
+ <div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
100
+ <svg class="h-7 w-7 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 13.255A23.193 23.193 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
101
+ </div>
102
+ <p class="text-sm text-gray-500 font-medium">No job events in this time window</p>
103
+ <p class="text-xs text-gray-400 mt-1">Try selecting a larger time range above</p>
104
+ </div>
105
+ <% end %>
106
+ </div>
107
+ </div>
108
+
@@ -0,0 +1,98 @@
1
+
2
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
3
+ <div class="mb-8">
4
+ <h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Events</h1>
5
+ <p class="text-sm text-gray-500 mt-1">Browse and filter all captured instrumentation events</p>
6
+ </div>
7
+
8
+ <!-- Filters -->
9
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden mb-6">
10
+ <div class="px-6 py-4 border-b border-gray-100">
11
+ <h2 class="text-sm font-bold text-gray-900">Filters</h2>
12
+ </div>
13
+ <div class="p-6">
14
+ <form method="get" action="<%= solid_ops.events_path %>" class="flex flex-wrap gap-3 items-end">
15
+ <div>
16
+ <label class="block text-[11px] font-bold text-gray-500 uppercase tracking-wide mb-1.5">Type</label>
17
+ <select name="event_type" class="rounded-lg border-gray-300 text-sm py-2 px-3 min-w-[160px] focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
18
+ <option value="">All</option>
19
+ <% %w[cable.broadcast job.enqueue job.perform_start job.perform cache.read cache.write cache.delete].each do |t| %>
20
+ <option value="<%= t %>" <%= "selected" if params[:event_type].to_s == t %>><%= t %></option>
21
+ <% end %>
22
+ </select>
23
+ </div>
24
+ <div>
25
+ <label class="block text-[11px] font-bold text-gray-500 uppercase tracking-wide mb-1.5">Correlation ID</label>
26
+ <input name="correlation_id" value="<%= params[:correlation_id].to_s %>" placeholder="uuid" class="rounded-lg border-gray-300 text-sm py-2 px-3 min-w-[160px] focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
27
+ </div>
28
+ <div>
29
+ <label class="block text-[11px] font-bold text-gray-500 uppercase tracking-wide mb-1.5">Tenant</label>
30
+ <input name="tenant_id" value="<%= params[:tenant_id].to_s %>" placeholder="tenant" class="rounded-lg border-gray-300 text-sm py-2 px-3 min-w-[120px] focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
31
+ </div>
32
+ <div>
33
+ <label class="block text-[11px] font-bold text-gray-500 uppercase tracking-wide mb-1.5">Actor</label>
34
+ <input name="actor_id" value="<%= params[:actor_id].to_s %>" placeholder="actor" class="rounded-lg border-gray-300 text-sm py-2 px-3 min-w-[120px] focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
35
+ </div>
36
+ <div>
37
+ <label class="block text-[11px] font-bold text-gray-500 uppercase tracking-wide mb-1.5">Name contains</label>
38
+ <input name="q" value="<%= params[:q].to_s %>" placeholder="search..." class="rounded-lg border-gray-300 text-sm py-2 px-3 min-w-[160px] focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
39
+ </div>
40
+ <div>
41
+ <label class="block text-[11px] font-bold text-gray-500 uppercase tracking-wide mb-1.5">Since</label>
42
+ <input name="since" type="datetime-local" value="<%= params[:since].to_s %>" class="rounded-lg border-gray-300 text-sm py-2 px-3 focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500">
43
+ </div>
44
+ <div class="flex gap-2">
45
+ <button type="submit" class="inline-flex items-center gap-1.5 px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors shadow-sm">
46
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/></svg>
47
+ Filter
48
+ </button>
49
+ <a href="<%= solid_ops.events_path %>" class="px-4 py-2 bg-white text-gray-500 text-sm font-medium rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors">Clear</a>
50
+ </div>
51
+ </form>
52
+ </div>
53
+ </div>
54
+
55
+ <!-- Results -->
56
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
57
+ <% if @events.any? %>
58
+ <table id="events-table" class="min-w-full divide-y divide-gray-100">
59
+ <thead>
60
+ <tr class="bg-gray-50">
61
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Time</th>
62
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Type</th>
63
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Name</th>
64
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">Duration</th>
65
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Correlation</th>
66
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Tenant</th>
67
+ </tr>
68
+ </thead>
69
+ <tbody class="divide-y divide-gray-50">
70
+ <% @events.each do |e| %>
71
+ <tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.event_path(e.id) %>'">
72
+ <td class="px-6 py-3.5 font-mono text-xs whitespace-nowrap text-blue-600"><%= format_datetime(e.occurred_at) %></td>
73
+ <td class="px-6 py-3.5"><span class="<%= event_pill_class(e.event_type) %>"><%= e.event_type %></span></td>
74
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-700 max-w-xs truncate"><%= e.name %></td>
75
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= format_duration(e.duration_ms) %></td>
76
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-400" onclick="event.stopPropagation()">
77
+ <% if e.correlation_id.present? %>
78
+ <a href="<%= solid_ops.events_path(correlation_id: e.correlation_id) %>" class="text-blue-600 hover:text-blue-700"><%= e.correlation_id.first(8) %></a>
79
+ <% end %>
80
+ </td>
81
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-400"><%= e.tenant_id %></td>
82
+ </tr>
83
+ <% end %>
84
+ </tbody>
85
+ </table>
86
+ <%= render "solid_ops/shared/pagination", current_page: @current_page, total_pages: @total_pages, total_count: @total_count %>
87
+ <% else %>
88
+ <div class="py-20 text-center">
89
+ <div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
90
+ <svg class="h-7 w-7 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
91
+ </div>
92
+ <p class="text-sm text-gray-500 font-medium">No events found matching your filters</p>
93
+ <p class="text-xs text-gray-400 mt-1">Try adjusting filter criteria or clearing all filters</p>
94
+ </div>
95
+ <% end %>
96
+ </div>
97
+ </div>
98
+
@@ -0,0 +1,108 @@
1
+
2
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
3
+ <div class="flex items-center justify-between mb-8">
4
+ <div>
5
+ <p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Events</p>
6
+ <h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Event Detail</h1>
7
+ <p class="text-sm text-gray-500 mt-1">ID: <%= @event.id %></p>
8
+ </div>
9
+ <a href="<%= solid_ops.events_path %>" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm text-gray-500 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors shadow-sm">
10
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
11
+ All Events
12
+ </a>
13
+ </div>
14
+
15
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
16
+ <!-- Overview -->
17
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
18
+ <div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
19
+ <h3 class="text-sm font-bold text-gray-900">Overview</h3>
20
+ <span class="<%= event_pill_class(@event.event_type) %>"><%= @event.event_type %></span>
21
+ </div>
22
+ <div class="p-6">
23
+ <dl class="space-y-3">
24
+ <% [
25
+ ["Name", @event.name],
26
+ ["Type", @event.event_type],
27
+ ["Occurred", format_datetime(@event.occurred_at)],
28
+ ["Duration", format_duration(@event.duration_ms)],
29
+ ["Correlation ID", @event.correlation_id],
30
+ ["Request ID", @event.request_id],
31
+ ["Tenant", @event.tenant_id],
32
+ ["Actor", @event.actor_id],
33
+ ].each do |label, value| %>
34
+ <div class="flex items-start">
35
+ <dt class="w-32 flex-shrink-0 text-[11px] font-bold text-gray-400 uppercase tracking-wide pt-0.5"><%= label %></dt>
36
+ <dd class="font-mono text-sm text-gray-700"><%= value.presence || "—" %></dd>
37
+ </div>
38
+ <% end %>
39
+ </dl>
40
+ </div>
41
+ </div>
42
+
43
+ <!-- Metadata -->
44
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
45
+ <div class="px-6 py-4 border-b border-gray-100">
46
+ <h3 class="text-sm font-bold text-gray-900">Metadata</h3>
47
+ </div>
48
+ <div class="p-6">
49
+ <% if @event.metadata.present? %>
50
+ <pre class="bg-gray-50 rounded-lg p-4 text-xs font-mono text-gray-700 overflow-x-auto max-h-80 overflow-y-auto ring-1 ring-inset ring-gray-900/5"><%= JSON.pretty_generate(@event.metadata) rescue @event.metadata.to_s %></pre>
51
+ <% else %>
52
+ <div class="text-center py-8">
53
+ <div class="inline-flex items-center justify-center w-10 h-10 rounded-xl bg-gray-100 mb-3">
54
+ <svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/></svg>
55
+ </div>
56
+ <p class="text-sm text-gray-400">No metadata recorded</p>
57
+ </div>
58
+ <% end %>
59
+ </div>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- Correlation Timeline -->
64
+ <% if @related.present? && @related.size > 1 %>
65
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
66
+ <div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
67
+ <h3 class="text-sm font-bold text-gray-900">Correlation Timeline</h3>
68
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-gray-100 text-gray-600 ring-1 ring-inset ring-gray-500/10"><%= @related.size %> related event<%= "s" unless @related.size == 1 %></span>
69
+ </div>
70
+ <div class="p-6">
71
+ <div class="relative pl-6 border-l-2 border-gray-200 space-y-6">
72
+ <% @related.each do |rel| %>
73
+ <% is_current = rel.id == @event.id %>
74
+ <div class="relative">
75
+ <%
76
+ dot_color = case rel.event_type.to_s
77
+ when /^cable\./ then is_current ? "bg-purple-500" : "bg-purple-200 border-2 border-purple-400"
78
+ when /^job\./ then is_current ? "bg-blue-500" : "bg-blue-200 border-2 border-blue-400"
79
+ when /^cache\./ then is_current ? "bg-emerald-500" : "bg-emerald-200 border-2 border-emerald-400"
80
+ else is_current ? "bg-gray-500" : "bg-gray-200 border-2 border-gray-400"
81
+ end
82
+ %>
83
+ <div class="absolute -left-[25px] top-1 w-3 h-3 rounded-full <%= dot_color %> <%= 'ring-4 ring-blue-50' if is_current %>"></div>
84
+ <div class="<%= is_current ? 'bg-blue-50/50 -mx-3 px-3 py-2 rounded-lg ring-1 ring-inset ring-blue-100' : '' %>">
85
+ <div class="flex items-center gap-2 flex-wrap">
86
+ <span class="<%= event_pill_class(rel.event_type) %> text-[10px]"><%= rel.event_type %></span>
87
+ <span class="font-mono text-xs text-gray-400"><%= format_datetime(rel.occurred_at) %></span>
88
+ <% if rel.duration_ms %>
89
+ <span class="font-mono text-xs text-gray-400"><%= format_duration(rel.duration_ms) %></span>
90
+ <% end %>
91
+ </div>
92
+ <div class="font-mono text-xs mt-1">
93
+ <% if is_current %>
94
+ <strong class="text-gray-900"><%= rel.name %></strong>
95
+ <span class="text-gray-400">(this event)</span>
96
+ <% else %>
97
+ <a href="<%= solid_ops.event_path(rel.id) %>" class="text-blue-600 hover:text-blue-700"><%= rel.name %></a>
98
+ <% end %>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ <% end %>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ <% end %>
107
+ </div>
108
+
@@ -0,0 +1,89 @@
1
+
2
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 animate-fade-in">
3
+ <div class="flex items-center justify-between mb-8">
4
+ <div>
5
+ <p class="text-xs font-medium text-gray-400 uppercase tracking-wider mb-1">Queues</p>
6
+ <h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Failed Jobs</h1>
7
+ <p class="text-sm text-gray-500 mt-1"><%= @total_count %> jobs with failed executions</p>
8
+ </div>
9
+ <div class="flex items-center gap-3">
10
+ <% if @failed_jobs.any? %>
11
+ <%= form_tag(solid_ops.retry_all_jobs_path, method: :post, class: "inline") do %>
12
+ <button type="submit" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-semibold text-blue-700 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 transition-colors ring-1 ring-inset ring-blue-700/10"
13
+ onclick="return confirm('Retry all failed jobs?')">
14
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>
15
+ Retry All
16
+ </button>
17
+ <% end %>
18
+ <%= form_tag(solid_ops.discard_all_jobs_path, method: :post, class: "inline") do %>
19
+ <button type="submit" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-semibold text-red-700 bg-red-50 border border-red-200 rounded-lg hover:bg-red-100 transition-colors ring-1 ring-inset ring-red-600/10"
20
+ onclick="return confirm('Discard all failed jobs? This cannot be undone.')">
21
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
22
+ Discard All
23
+ </button>
24
+ <% end %>
25
+ <% end %>
26
+ <a href="<%= solid_ops.queues_path %>" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm text-gray-500 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors shadow-sm">
27
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/></svg>
28
+ Queues
29
+ </a>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
34
+ <% if @failed_jobs.any? %>
35
+ <div style="padding: 0.625rem 1rem; border-bottom: 1px solid rgb(243 244 246);">
36
+ <div style="position: relative; display: inline-block;">
37
+ <svg style="position: absolute; left: 0.625rem; top: 50%; transform: translateY(-50%); width: 1rem; height: 1rem; color: #9ca3af; pointer-events: none;" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg>
38
+ <input type="text" data-solid-search="failed-table" placeholder="Filter jobs…" style="padding: 0.375rem 0.75rem 0.375rem 2rem; font-size: 0.875rem; border: 1px solid #e5e7eb; border-radius: 0.5rem; width: 18rem; background: rgba(249,250,251,0.5);">
39
+ </div>
40
+ </div>
41
+ <table id="failed-table" class="min-w-full divide-y divide-gray-100">
42
+ <thead>
43
+ <tr class="bg-gray-50">
44
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">ID</th>
45
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Class</th>
46
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Queue</th>
47
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Error</th>
48
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Failed At</th>
49
+ <th class="px-6 py-3 text-right text-[11px] font-bold text-gray-500 uppercase tracking-wider">Actions</th>
50
+ </tr>
51
+ </thead>
52
+ <tbody class="divide-y divide-gray-50">
53
+ <% @failed_jobs.each do |job| %>
54
+ <% error = begin; JSON.parse(job.failed_execution.error.to_s); rescue; {}; end %>
55
+ <tr class="hover:bg-red-50/30 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.job_path(job.id) %>'">
56
+ <td class="px-6 py-3.5 font-mono text-xs text-blue-600 font-medium"><%= job.id %></td>
57
+ <td class="px-6 py-3.5 font-mono text-sm text-gray-700"><%= job.class_name %></td>
58
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= job.queue_name %></td>
59
+ <td class="px-6 py-3.5">
60
+ <p class="text-xs font-bold text-red-700"><%= error["exception_class"] || "Unknown" %></p>
61
+ <p class="text-xs text-red-500/80 truncate max-w-xs mt-0.5"><%= error["message"]&.truncate(80) || "No message" %></p>
62
+ </td>
63
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= time_ago_short(job.failed_execution.created_at) %></td>
64
+ <td class="px-6 py-3.5 text-right whitespace-nowrap" onclick="event.stopPropagation()">
65
+ <%= form_tag(solid_ops.retry_job_path(job.id), method: :post, class: "inline") do %>
66
+ <button type="submit" class="px-2.5 py-1 text-xs font-semibold text-blue-700 bg-blue-50 border border-blue-200 rounded-md hover:bg-blue-100 transition-colors">Retry</button>
67
+ <% end %>
68
+ <%= form_tag(solid_ops.discard_job_path(job.id), method: :post, class: "inline ml-1") do %>
69
+ <button type="submit" class="px-2.5 py-1 text-xs font-semibold text-red-700 bg-red-50 border border-red-200 rounded-md hover:bg-red-100 transition-colors"
70
+ onclick="if(!confirm('Discard this job?')){event.preventDefault();}">Discard</button>
71
+ <% end %>
72
+ </td>
73
+ </tr>
74
+ <% end %>
75
+ </tbody>
76
+ </table>
77
+ <%= render "solid_ops/shared/pagination", current_page: @current_page, total_pages: @total_pages, total_count: @total_count %>
78
+ <% else %>
79
+ <div class="py-20 text-center">
80
+ <div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-emerald-50 mb-4">
81
+ <svg class="h-7 w-7 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
82
+ </div>
83
+ <p class="text-sm text-emerald-700 font-semibold">No failed jobs</p>
84
+ <p class="text-xs text-emerald-600/70 mt-1">Everything is running smoothly</p>
85
+ </div>
86
+ <% end %>
87
+ </div>
88
+ </div>
89
+