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,121 @@
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">Queue: <%= @queue_name %></h1>
7
+ <div class="mt-2">
8
+ <% if @paused %>
9
+ <span class="<%= status_pill('paused') %>">Paused</span>
10
+ <% else %>
11
+ <span class="<%= status_pill('ready') %>">Active</span>
12
+ <% end %>
13
+ </div>
14
+ </div>
15
+ <div class="flex items-center gap-3">
16
+ <% if @paused %>
17
+ <%= form_tag(solid_ops.resume_queue_path(@queue_name), method: :post, class: "inline") do %>
18
+ <button type="submit" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-lg hover:bg-emerald-100 transition-colors ring-1 ring-inset ring-emerald-600/20">
19
+ <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="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"/></svg>
20
+ Resume Queue
21
+ </button>
22
+ <% end %>
23
+ <% else %>
24
+ <%= form_tag(solid_ops.pause_queue_path(@queue_name), method: :post, class: "inline") do %>
25
+ <button type="submit" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-semibold text-amber-700 bg-amber-50 border border-amber-200 rounded-lg hover:bg-amber-100 transition-colors ring-1 ring-inset ring-amber-600/20"
26
+ onclick="return confirm('Pause this queue?')">
27
+ <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="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
28
+ Pause Queue
29
+ </button>
30
+ <% end %>
31
+ <% end %>
32
+ <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">
33
+ <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>
34
+ All Queues
35
+ </a>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
40
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm relative overflow-hidden">
41
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-yellow-400 to-yellow-500"></div>
42
+ <div class="p-4 pt-5">
43
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Ready</p>
44
+ <p class="text-2xl font-extrabold font-mono mt-1 text-yellow-600"><%= @ready_count %></p>
45
+ </div>
46
+ </div>
47
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm relative overflow-hidden">
48
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-indigo-400 to-indigo-600"></div>
49
+ <div class="p-4 pt-5">
50
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Scheduled</p>
51
+ <p class="text-2xl font-extrabold font-mono mt-1 text-indigo-600"><%= @scheduled_count %></p>
52
+ </div>
53
+ </div>
54
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm relative overflow-hidden">
55
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-blue-400 to-blue-600"></div>
56
+ <div class="p-4 pt-5">
57
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Claimed</p>
58
+ <p class="text-2xl font-extrabold font-mono mt-1 text-blue-600"><%= @claimed_count %></p>
59
+ </div>
60
+ </div>
61
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm relative overflow-hidden">
62
+ <div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-red-400 to-red-600"></div>
63
+ <div class="p-4 pt-5">
64
+ <p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Failed</p>
65
+ <p class="text-2xl font-extrabold font-mono mt-1 text-red-600"><%= @failed_count %></p>
66
+ </div>
67
+ </div>
68
+ </div>
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 Jobs in this Queue</h2>
73
+ </div>
74
+ <% if @jobs.any? %>
75
+ <div style="padding: 0.625rem 1rem; border-bottom: 1px solid rgb(243 244 246);">
76
+ <div style="position: relative; display: inline-block;">
77
+ <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>
78
+ <input type="text" data-solid-search="queue-jobs-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);">
79
+ </div>
80
+ </div>
81
+ <table id="queue-jobs-table" class="min-w-full divide-y divide-gray-100">
82
+ <thead>
83
+ <tr class="bg-gray-50">
84
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">ID</th>
85
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Class</th>
86
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Status</th>
87
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">Priority</th>
88
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Created</th>
89
+ <th class="px-6 py-3 text-right text-[11px] font-bold text-gray-500 uppercase tracking-wider">Actions</th>
90
+ </tr>
91
+ </thead>
92
+ <tbody class="divide-y divide-gray-50">
93
+ <% @jobs.each do |job| %>
94
+ <tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.job_path(job.id) %>'">
95
+ <td class="px-6 py-3.5 font-mono text-xs text-blue-600 font-medium"><%= job.id %></td>
96
+ <td class="px-6 py-3.5 font-mono text-sm text-gray-700"><%= job.class_name %></td>
97
+ <td class="px-6 py-3.5">
98
+ <% status = job.finished? ? "finished" : (job.failed_execution ? "failed" : (job.claimed_execution ? "claimed" : "ready")) %>
99
+ <span class="<%= status_pill(status) %>"><%= status %></span>
100
+ </td>
101
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= job.priority || 0 %></td>
102
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= time_ago_short(job.created_at) %></td>
103
+ <td class="px-6 py-3.5 text-right">
104
+ <svg class="w-4 h-4 text-gray-300 group-hover:text-blue-500 transition-colors inline-block" 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>
105
+ </td>
106
+ </tr>
107
+ <% end %>
108
+ </tbody>
109
+ </table>
110
+ <%= render "solid_ops/shared/pagination", current_page: @current_page, total_pages: @total_pages, total_count: @total_count %>
111
+ <% else %>
112
+ <div class="py-20 text-center">
113
+ <div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
114
+ <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="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"/></svg>
115
+ </div>
116
+ <p class="text-sm text-gray-500 font-medium">No jobs in this queue</p>
117
+ </div>
118
+ <% end %>
119
+ </div>
120
+ </div>
121
+
@@ -0,0 +1,64 @@
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">Recurring Tasks</h1>
7
+ <p class="text-sm text-gray-500 mt-1">Cron-style jobs configured in Solid Queue</p>
8
+ </div>
9
+ <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">
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
+ Queues
12
+ </a>
13
+ </div>
14
+
15
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
16
+ <% if @tasks.any? %>
17
+ <div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
18
+ <h2 class="text-sm font-bold text-gray-900">Scheduled Tasks</h2>
19
+ <span class="text-xs text-gray-400"><%= @tasks.size %> configured</span>
20
+ </div>
21
+ <div style="padding: 0.625rem 1rem; border-bottom: 1px solid rgb(243 244 246);">
22
+ <div style="position: relative; display: inline-block;">
23
+ <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>
24
+ <input type="text" data-solid-search="tasks-table" placeholder="Filter tasks…" 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);">
25
+ </div>
26
+ </div>
27
+ <table id="tasks-table" class="min-w-full divide-y divide-gray-100">
28
+ <thead>
29
+ <tr class="bg-gray-50">
30
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Key</th>
31
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Class / Command</th>
32
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Schedule</th>
33
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Queue</th>
34
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">Priority</th>
35
+ <th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Description</th>
36
+ </tr>
37
+ </thead>
38
+ <tbody class="divide-y divide-gray-50">
39
+ <% @tasks.each do |task| %>
40
+ <tr class="hover:bg-gray-50/80 transition-colors">
41
+ <td class="px-6 py-3.5 font-mono text-sm font-semibold text-gray-900"><%= task.key %></td>
42
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-700"><%= task.class_name.presence || task.command %></td>
43
+ <td class="px-6 py-3.5">
44
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-semibold bg-indigo-50 text-indigo-700 ring-1 ring-inset ring-indigo-700/10"><%= task.schedule %></span>
45
+ </td>
46
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= task.queue_name || "default" %></td>
47
+ <td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= task.priority || 0 %></td>
48
+ <td class="px-6 py-3.5 text-xs text-gray-500 max-w-xs truncate"><%= task.description || "—" %></td>
49
+ </tr>
50
+ <% end %>
51
+ </tbody>
52
+ </table>
53
+ <% else %>
54
+ <div class="py-20 text-center">
55
+ <div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
56
+ <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="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
57
+ </div>
58
+ <p class="text-sm text-gray-500 font-medium">No recurring tasks configured</p>
59
+ <p class="text-xs text-gray-400 mt-1">Define recurring tasks in your Solid Queue configuration</p>
60
+ </div>
61
+ <% end %>
62
+ </div>
63
+ </div>
64
+
@@ -0,0 +1,50 @@
1
+ <nav class="sticky top-0 z-50 bg-gradient-to-r from-slate-900 via-slate-800 to-slate-900 shadow-lg shadow-slate-900/10">
2
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
3
+ <div class="flex items-center justify-between h-14">
4
+ <div class="flex items-center gap-6">
5
+ <a href="<%= solid_ops.root_path %>" class="flex items-center gap-2.5 group">
6
+ <div class="w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center shadow-lg shadow-blue-500/25 group-hover:shadow-blue-500/40 transition-shadow">
7
+ <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/></svg>
8
+ </div>
9
+ <span class="font-bold text-base text-white tracking-tight">SolidOps</span>
10
+ </a>
11
+
12
+ <div class="hidden sm:flex items-center gap-1">
13
+ <%
14
+ nav_items = [
15
+ { label: "Dashboard", path: solid_ops.dashboard_path, match: [solid_ops.dashboard_path, solid_ops.root_path],
16
+ icon: "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" },
17
+ ]
18
+ if solid_queue_available?
19
+ nav_items << { label: "Queues", path: solid_ops.queues_path, match: [solid_ops.queues_path],
20
+ icon: "M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" }
21
+ nav_items << { label: "Running", path: solid_ops.running_jobs_path, match: [solid_ops.running_jobs_path],
22
+ icon: "M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" }
23
+ end
24
+ if solid_cache_available?
25
+ nav_items << { label: "Cache", path: solid_ops.cache_entries_path, match: [solid_ops.cache_entries_path],
26
+ icon: "M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" }
27
+ end
28
+ if solid_cable_available?
29
+ nav_items << { label: "Channels", path: solid_ops.channels_path, match: [solid_ops.channels_path],
30
+ icon: "M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.14 0M1.394 9.393c5.857-5.858 15.355-5.858 21.213 0" }
31
+ end
32
+ nav_items << { label: "Events", path: solid_ops.events_path, match: [solid_ops.events_path],
33
+ icon: "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" }
34
+ %>
35
+ <% nav_items.each do |item| %>
36
+ <% active = item[:match].any? { |p| current_page?(p) } %>
37
+ <a href="<%= item[:path] %>"
38
+ class="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-150
39
+ <%= active ? 'bg-white/15 text-white shadow-sm' : 'text-slate-400 hover:text-white hover:bg-white/5' %>">
40
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="<%= item[:icon] %>"/></svg>
41
+ <%= item[:label] %>
42
+ </a>
43
+ <% end %>
44
+ </div>
45
+ </div>
46
+
47
+ <span class="text-[10px] text-slate-500 font-mono bg-slate-800 px-2 py-0.5 rounded-full border border-slate-700">v<%= SolidOps::VERSION %></span>
48
+ </div>
49
+ </div>
50
+ </nav>
@@ -0,0 +1,31 @@
1
+ <% if total_pages > 1 %>
2
+ <div class="px-6 py-3 border-t border-gray-100 flex items-center justify-between bg-gray-50/50">
3
+ <p class="text-xs text-gray-500">
4
+ Page <span class="font-semibold text-gray-700"><%= current_page %></span> of
5
+ <span class="font-semibold text-gray-700"><%= total_pages %></span>
6
+ <span class="text-gray-400 ml-1">(<%= number_with_delimiter(total_count) %> total)</span>
7
+ </p>
8
+ <nav class="flex items-center gap-1">
9
+ <% if current_page > 1 %>
10
+ <a href="<%= "#{request.path}?#{request.query_parameters.merge('page' => current_page - 1).to_query}" %>"
11
+ class="px-2.5 py-1 text-xs font-medium text-gray-600 bg-white border border-gray-200 rounded-md hover:bg-gray-50 transition-colors">&larr; Prev</a>
12
+ <% end %>
13
+
14
+ <% pages_to_show(current_page, total_pages).each do |pg| %>
15
+ <% if pg == :gap %>
16
+ <span class="px-1.5 py-1 text-xs text-gray-400">&hellip;</span>
17
+ <% elsif pg == current_page %>
18
+ <span class="px-2.5 py-1 text-xs font-bold text-white bg-blue-600 rounded-md shadow-sm"><%= pg %></span>
19
+ <% else %>
20
+ <a href="<%= "#{request.path}?#{request.query_parameters.merge('page' => pg).to_query}" %>"
21
+ class="px-2.5 py-1 text-xs font-medium text-gray-600 bg-white border border-gray-200 rounded-md hover:bg-gray-50 transition-colors"><%= pg %></a>
22
+ <% end %>
23
+ <% end %>
24
+
25
+ <% if current_page < total_pages %>
26
+ <a href="<%= "#{request.path}?#{request.query_parameters.merge('page' => current_page + 1).to_query}" %>"
27
+ class="px-2.5 py-1 text-xs font-medium text-gray-600 bg-white border border-gray-200 rounded-md hover:bg-gray-50 transition-colors">Next &rarr;</a>
28
+ <% end %>
29
+ </nav>
30
+ </div>
31
+ <% end %>
@@ -0,0 +1,10 @@
1
+ <% window_param = params[:window].presence || "1h" %>
2
+ <div class="flex items-center gap-1 bg-gray-100 rounded-lg p-1">
3
+ <% %w[5m 15m 30m 1h 6h 24h 7d].each do |w| %>
4
+ <a href="<%= url_for(request.query_parameters.merge(window: w)) %>"
5
+ class="px-3 py-1.5 rounded-md text-xs font-medium transition-all duration-150
6
+ <%= window_param == w ? 'bg-white text-gray-900 shadow-sm ring-1 ring-gray-200' : 'text-gray-500 hover:text-gray-700' %>">
7
+ <%= w %>
8
+ </a>
9
+ <% end %>
10
+ </div>
@@ -0,0 +1,63 @@
1
+ <% content_for :title, "#{@component_name} Not Available" %>
2
+
3
+ <main class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
4
+ <div class="text-center">
5
+ <div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-amber-50 mb-6 ring-1 ring-inset ring-amber-500/20">
6
+ <svg class="w-8 h-8 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
7
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5"
8
+ d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
9
+ </svg>
10
+ </div>
11
+
12
+ <h1 class="text-xl font-extrabold text-gray-900 tracking-tight mb-2"><%= @component_name %> is not available</h1>
13
+ <p class="text-sm text-gray-500 mb-8">
14
+ <%= @component_name %> provides <%= @component_description %>.
15
+ It needs to be installed and configured before SolidOps can manage it.
16
+ </p>
17
+ </div>
18
+
19
+ <div class="bg-white rounded-xl border border-gray-200 shadow-sm divide-y divide-gray-100 overflow-hidden">
20
+ <div class="px-6 py-4 border-b border-gray-100">
21
+ <h2 class="text-[11px] font-bold text-gray-500 uppercase tracking-wider">Setup steps</h2>
22
+ </div>
23
+ <div class="px-6 py-5">
24
+ <ol class="space-y-4 text-sm">
25
+ <li class="flex gap-3">
26
+ <span class="flex-shrink-0 w-6 h-6 rounded-lg bg-blue-100 text-blue-700 flex items-center justify-center text-xs font-bold ring-1 ring-inset ring-blue-700/10">1</span>
27
+ <div>
28
+ <p class="text-gray-700 font-medium">Add the gem to your Gemfile</p>
29
+ <code class="mt-1.5 block bg-gray-50 rounded-lg px-3 py-2 text-xs font-mono text-gray-600 ring-1 ring-inset ring-gray-900/5">gem "<%= @component_gem %>"</code>
30
+ </div>
31
+ </li>
32
+ <li class="flex gap-3">
33
+ <span class="flex-shrink-0 w-6 h-6 rounded-lg bg-blue-100 text-blue-700 flex items-center justify-center text-xs font-bold ring-1 ring-inset ring-blue-700/10">2</span>
34
+ <div>
35
+ <p class="text-gray-700 font-medium">Install and run migrations</p>
36
+ <code class="mt-1.5 block bg-gray-50 rounded-lg px-3 py-2 text-xs font-mono text-gray-600 ring-1 ring-inset ring-gray-900/5">bundle install && <%= @install_command %></code>
37
+ </div>
38
+ </li>
39
+ <li class="flex gap-3">
40
+ <span class="flex-shrink-0 w-6 h-6 rounded-lg bg-blue-100 text-blue-700 flex items-center justify-center text-xs font-bold ring-1 ring-inset ring-blue-700/10">3</span>
41
+ <div>
42
+ <p class="text-gray-700 font-medium">Restart your Rails server</p>
43
+ </div>
44
+ </li>
45
+ </ol>
46
+ </div>
47
+
48
+ <div class="px-6 py-4 bg-gray-50/50">
49
+ <p class="text-xs text-gray-400">
50
+ Already installed? Make sure the database is created and migrations have been run.
51
+ <br>
52
+ <code class="font-mono text-gray-500">bin/rails db:create db:migrate</code>
53
+ </p>
54
+ </div>
55
+ </div>
56
+
57
+ <div class="mt-8 text-center">
58
+ <a href="<%= solid_ops.root_path %>" class="inline-flex items-center gap-1.5 text-xs font-medium text-blue-600 hover:text-blue-800 transition-colors">
59
+ <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>
60
+ Back to Dashboard
61
+ </a>
62
+ </div>
63
+ </main>
data/config/routes.rb ADDED
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ SolidOps::Engine.routes.draw do
4
+ root to: "dashboard#index"
5
+ get "dashboard", to: "dashboard#index", as: :dashboard
6
+ get "dashboard/jobs", to: "dashboard#jobs", as: :dashboard_jobs
7
+ get "dashboard/cache", to: "dashboard#cache", as: :dashboard_cache
8
+ get "dashboard/cable", to: "dashboard#cable", as: :dashboard_cable
9
+
10
+ # Queue management (Solid Queue)
11
+ resources :queues, only: %i[index show] do
12
+ member do
13
+ post :pause
14
+ post :resume
15
+ end
16
+ end
17
+ resources :jobs, only: %i[show destroy] do
18
+ member do
19
+ post :retry
20
+ post :discard
21
+ end
22
+ collection do
23
+ get :running
24
+ get :failed
25
+ post :retry_all
26
+ post :discard_all
27
+ post :clear_finished
28
+ end
29
+ end
30
+ resources :recurring_tasks, only: [:index], path: "recurring-tasks"
31
+ resources :processes, only: [:index]
32
+
33
+ # Cache management (Solid Cache)
34
+ resources :cache_entries, only: %i[index show destroy], path: "cache" do
35
+ collection do
36
+ post :clear_all
37
+ end
38
+ end
39
+
40
+ # Cable management (Solid Cable)
41
+ resources :channels, only: %i[index show] do
42
+ collection do
43
+ post :trim
44
+ end
45
+ end
46
+
47
+ # Event explorer
48
+ resources :events, only: %i[index show]
49
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateSolidOpsEvents < ActiveRecord::Migration[7.1]
4
+ def change
5
+ create_table :solid_ops_events do |t|
6
+ t.string :event_type, null: false
7
+ t.string :name, null: false
8
+
9
+ t.string :correlation_id
10
+ t.string :request_id
11
+ t.string :tenant_id
12
+ t.string :actor_id
13
+
14
+ t.float :duration_ms
15
+ t.datetime :occurred_at, null: false
16
+
17
+ t.json :metadata, null: false, default: {}
18
+
19
+ t.timestamps
20
+ end
21
+
22
+ add_index :solid_ops_events, :occurred_at
23
+ add_index :solid_ops_events, :event_type
24
+ add_index :solid_ops_events, :correlation_id
25
+ add_index :solid_ops_events, :request_id
26
+ add_index :solid_ops_events, :tenant_id
27
+ add_index :solid_ops_events, :actor_id
28
+ add_index :solid_ops_events, :name
29
+ add_index :solid_ops_events, %i[event_type occurred_at]
30
+ end
31
+ end