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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +308 -0
- data/Rakefile +12 -0
- data/app/assets/stylesheets/solid_ops/application.css +1 -0
- data/app/controllers/solid_ops/application_controller.rb +127 -0
- data/app/controllers/solid_ops/cache_entries_controller.rb +38 -0
- data/app/controllers/solid_ops/channels_controller.rb +30 -0
- data/app/controllers/solid_ops/dashboard_controller.rb +80 -0
- data/app/controllers/solid_ops/events_controller.rb +37 -0
- data/app/controllers/solid_ops/jobs_controller.rb +64 -0
- data/app/controllers/solid_ops/processes_controller.rb +11 -0
- data/app/controllers/solid_ops/queues_controller.rb +75 -0
- data/app/controllers/solid_ops/recurring_tasks_controller.rb +11 -0
- data/app/helpers/solid_ops/application_helper.rb +112 -0
- data/app/jobs/solid_ops/purge_job.rb +16 -0
- data/app/models/solid_ops/event.rb +34 -0
- data/app/views/layouts/solid_ops/application.html.erb +118 -0
- data/app/views/solid_ops/cache_entries/index.html.erb +86 -0
- data/app/views/solid_ops/cache_entries/show.html.erb +153 -0
- data/app/views/solid_ops/channels/index.html.erb +81 -0
- data/app/views/solid_ops/channels/show.html.erb +66 -0
- data/app/views/solid_ops/dashboard/cable.html.erb +98 -0
- data/app/views/solid_ops/dashboard/cache.html.erb +104 -0
- data/app/views/solid_ops/dashboard/index.html.erb +169 -0
- data/app/views/solid_ops/dashboard/jobs.html.erb +108 -0
- data/app/views/solid_ops/events/index.html.erb +98 -0
- data/app/views/solid_ops/events/show.html.erb +108 -0
- data/app/views/solid_ops/jobs/failed.html.erb +89 -0
- data/app/views/solid_ops/jobs/running.html.erb +134 -0
- data/app/views/solid_ops/jobs/show.html.erb +116 -0
- data/app/views/solid_ops/processes/index.html.erb +69 -0
- data/app/views/solid_ops/queues/index.html.erb +182 -0
- data/app/views/solid_ops/queues/show.html.erb +121 -0
- data/app/views/solid_ops/recurring_tasks/index.html.erb +64 -0
- data/app/views/solid_ops/shared/_nav.html.erb +50 -0
- data/app/views/solid_ops/shared/_pagination.html.erb +31 -0
- data/app/views/solid_ops/shared/_time_window.html.erb +10 -0
- data/app/views/solid_ops/shared/component_unavailable.html.erb +63 -0
- data/config/routes.rb +49 -0
- data/db/migrate/20260224000100_create_solid_ops_events.rb +31 -0
- data/lib/generators/solid_ops/install/install_generator.rb +348 -0
- data/lib/generators/solid_ops/install/templates/create_solid_ops_events.rb +31 -0
- data/lib/generators/solid_ops/install/templates/solid_ops_initializer.rb +31 -0
- data/lib/solid_ops/configuration.rb +28 -0
- data/lib/solid_ops/context.rb +34 -0
- data/lib/solid_ops/current.rb +10 -0
- data/lib/solid_ops/engine.rb +60 -0
- data/lib/solid_ops/job_extension.rb +50 -0
- data/lib/solid_ops/middleware.rb +52 -0
- data/lib/solid_ops/subscribers.rb +215 -0
- data/lib/solid_ops/version.rb +5 -0
- data/lib/solid_ops.rb +25 -0
- data/lib/tasks/solid_ops.rake +32 -0
- data/log/test.log +2 -0
- data/sig/solid_ops.rbs +4 -0
- metadata +119 -0
|
@@ -0,0 +1,134 @@
|
|
|
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">Jobs</p>
|
|
6
|
+
<h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Running Jobs</h1>
|
|
7
|
+
<p class="text-sm text-gray-500 mt-1"><%= @running_count %> <%= @running_count == 1 ? "job" : "jobs" %> currently executing<%= " (showing first 500)" if @running_count > 500 %></p>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="flex items-center gap-3">
|
|
10
|
+
<a href="<%= solid_ops.failed_jobs_path %>" class="inline-flex items-center gap-1.5 px-4 py-2 text-sm text-red-600 border border-red-200 rounded-lg hover:bg-red-50 transition-colors shadow-sm">
|
|
11
|
+
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/></svg>
|
|
12
|
+
Failed Jobs
|
|
13
|
+
</a>
|
|
14
|
+
<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">
|
|
15
|
+
<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>
|
|
16
|
+
Queues
|
|
17
|
+
</a>
|
|
18
|
+
</div>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<%# Summary cards %>
|
|
22
|
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
|
|
23
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
24
|
+
<div class="h-1 bg-gradient-to-r from-blue-500 to-blue-400"></div>
|
|
25
|
+
<div class="px-5 py-4">
|
|
26
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wider">Running</p>
|
|
27
|
+
<p class="text-2xl font-extrabold text-gray-900 mt-1"><%= @running_count %></p>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
31
|
+
<div class="h-1 bg-gradient-to-r from-amber-500 to-amber-400"></div>
|
|
32
|
+
<div class="px-5 py-4">
|
|
33
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wider">Queues Active</p>
|
|
34
|
+
<p class="text-2xl font-extrabold text-gray-900 mt-1"><%= @running_jobs.map(&:queue_name).uniq.size %></p>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
38
|
+
<div class="h-1 bg-gradient-to-r from-emerald-500 to-emerald-400"></div>
|
|
39
|
+
<div class="px-5 py-4">
|
|
40
|
+
<p class="text-xs font-medium text-gray-500 uppercase tracking-wider">Longest Running</p>
|
|
41
|
+
<p class="text-2xl font-extrabold text-gray-900 mt-1">
|
|
42
|
+
<% oldest = @running_jobs.min_by { |j| j.claimed_execution.created_at } %>
|
|
43
|
+
<%= oldest ? duration_since(oldest.claimed_execution.created_at) : "—" %>
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
50
|
+
<% if @running_jobs.any? %>
|
|
51
|
+
<div style="padding: 0.625rem 1rem; border-bottom: 1px solid rgb(243 244 246);">
|
|
52
|
+
<div style="position: relative; display: inline-block;">
|
|
53
|
+
<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>
|
|
54
|
+
<input type="text" data-solid-search="running-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);">
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<table id="running-table" class="min-w-full divide-y divide-gray-100">
|
|
58
|
+
<thead>
|
|
59
|
+
<tr class="bg-gray-50">
|
|
60
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">ID</th>
|
|
61
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Class</th>
|
|
62
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Queue</th>
|
|
63
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Worker</th>
|
|
64
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Running For</th>
|
|
65
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Claimed At</th>
|
|
66
|
+
<th class="px-6 py-3 text-right text-[11px] font-bold text-gray-500 uppercase tracking-wider">Details</th>
|
|
67
|
+
</tr>
|
|
68
|
+
</thead>
|
|
69
|
+
<tbody class="divide-y divide-gray-50">
|
|
70
|
+
<% @running_jobs.each do |job| %>
|
|
71
|
+
<% claimed = job.claimed_execution %>
|
|
72
|
+
<% elapsed = duration_since(claimed.created_at) %>
|
|
73
|
+
<% long_running = (Time.current - claimed.created_at) > 300 %>
|
|
74
|
+
<tr class="hover:bg-blue-50/30 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.job_path(job.id) %>'">
|
|
75
|
+
<td class="px-6 py-3.5 font-mono text-xs text-blue-600 font-medium"><%= job.id %></td>
|
|
76
|
+
<td class="px-6 py-3.5">
|
|
77
|
+
<p class="font-mono text-sm text-gray-700"><%= job.class_name %></p>
|
|
78
|
+
</td>
|
|
79
|
+
<td class="px-6 py-3.5" onclick="event.stopPropagation()">
|
|
80
|
+
<a href="<%= solid_ops.queue_path(job.queue_name) %>" class="font-mono text-xs text-gray-500 hover:text-blue-600"><%= job.queue_name %></a>
|
|
81
|
+
</td>
|
|
82
|
+
<td class="px-6 py-3.5">
|
|
83
|
+
<% process = claimed.process if claimed.respond_to?(:process) %>
|
|
84
|
+
<% if process %>
|
|
85
|
+
<p class="font-mono text-xs text-gray-600"><%= process.hostname %></p>
|
|
86
|
+
<p class="font-mono text-[10px] text-gray-400">PID <%= process.pid %></p>
|
|
87
|
+
<% else %>
|
|
88
|
+
<span class="text-xs text-gray-400">—</span>
|
|
89
|
+
<% end %>
|
|
90
|
+
</td>
|
|
91
|
+
<td class="px-6 py-3.5">
|
|
92
|
+
<span class="inline-flex items-center gap-1.5 text-sm font-semibold <%= long_running ? 'text-amber-600' : 'text-blue-600' %>">
|
|
93
|
+
<% if long_running %>
|
|
94
|
+
<svg class="w-3.5 h-3.5 text-amber-500" 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>
|
|
95
|
+
<% else %>
|
|
96
|
+
<span class="relative flex h-2 w-2">
|
|
97
|
+
<span class="absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75 animate-ping"></span>
|
|
98
|
+
<span class="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span>
|
|
99
|
+
</span>
|
|
100
|
+
<% end %>
|
|
101
|
+
<%= elapsed %>
|
|
102
|
+
</span>
|
|
103
|
+
</td>
|
|
104
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= format_datetime(claimed.created_at) %></td>
|
|
105
|
+
<td class="px-6 py-3.5 text-right">
|
|
106
|
+
<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>
|
|
107
|
+
</td>
|
|
108
|
+
</tr>
|
|
109
|
+
<% end %>
|
|
110
|
+
</tbody>
|
|
111
|
+
</table>
|
|
112
|
+
<% else %>
|
|
113
|
+
<div class="py-20 text-center">
|
|
114
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
|
|
115
|
+
<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 12H4m16 0l-4 4m4-4l-4-4"/></svg>
|
|
116
|
+
</div>
|
|
117
|
+
<p class="text-sm text-gray-600 font-semibold">No jobs running</p>
|
|
118
|
+
<p class="text-xs text-gray-400 mt-1">Jobs will appear here when workers claim them for execution</p>
|
|
119
|
+
</div>
|
|
120
|
+
<% end %>
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<% if @running_jobs.any? %>
|
|
124
|
+
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg px-4 py-3">
|
|
125
|
+
<div class="flex items-start gap-3">
|
|
126
|
+
<svg class="w-5 h-5 text-blue-500 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/></svg>
|
|
127
|
+
<div>
|
|
128
|
+
<p class="text-sm font-semibold text-blue-800">Running jobs cannot be stopped</p>
|
|
129
|
+
<p class="text-xs text-blue-600 mt-0.5">Once a job is claimed by a worker, it runs to completion. To prevent new jobs from starting, pause the queue. Jobs running longer than 5 minutes are flagged with a warning icon.</p>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<% end %>
|
|
134
|
+
</div>
|
|
@@ -0,0 +1,116 @@
|
|
|
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 › Jobs</p>
|
|
6
|
+
<h1 class="text-2xl font-extrabold text-gray-900 tracking-tight">Job #<%= @job.id %></h1>
|
|
7
|
+
<p class="text-sm text-gray-500 mt-1 font-mono"><%= @job.class_name %></p>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="flex items-center gap-3">
|
|
10
|
+
<% if @job.failed_execution %>
|
|
11
|
+
<%= form_tag(solid_ops.retry_job_path(@job.id), 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
|
+
<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>
|
|
14
|
+
Retry
|
|
15
|
+
</button>
|
|
16
|
+
<% end %>
|
|
17
|
+
<%= form_tag(solid_ops.discard_job_path(@job.id), 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-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"
|
|
19
|
+
onclick="return confirm('Discard this job?')">
|
|
20
|
+
<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>
|
|
21
|
+
Discard
|
|
22
|
+
</button>
|
|
23
|
+
<% end %>
|
|
24
|
+
<% end %>
|
|
25
|
+
<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">
|
|
26
|
+
<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>
|
|
27
|
+
Queues
|
|
28
|
+
</a>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
|
33
|
+
<!-- Job details -->
|
|
34
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
|
35
|
+
<div class="px-6 py-4 border-b border-gray-100">
|
|
36
|
+
<h3 class="text-sm font-bold text-gray-900">Job Details</h3>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="p-6">
|
|
39
|
+
<dl class="space-y-4">
|
|
40
|
+
<% status = @job.finished? ? "finished" : (@job.failed_execution ? "failed" : (@job.claimed_execution ? "claimed" : "ready")) %>
|
|
41
|
+
<div class="flex items-start">
|
|
42
|
+
<dt class="w-32 flex-shrink-0 text-xs font-bold text-gray-400 uppercase tracking-wide pt-0.5">Status</dt>
|
|
43
|
+
<dd><span class="<%= status_pill(status) %>"><%= status %></span></dd>
|
|
44
|
+
</div>
|
|
45
|
+
<% [
|
|
46
|
+
["Class", @job.class_name],
|
|
47
|
+
["Queue", @job.queue_name],
|
|
48
|
+
["Priority", @job.priority || 0],
|
|
49
|
+
["Active Job ID", @job.active_job_id],
|
|
50
|
+
["Concurrency Key", @job.concurrency_key],
|
|
51
|
+
["Created", format_datetime(@job.created_at)],
|
|
52
|
+
["Scheduled At", format_datetime(@job.scheduled_at)],
|
|
53
|
+
["Finished At", format_datetime(@job.finished_at)],
|
|
54
|
+
].each do |label, value| %>
|
|
55
|
+
<div class="flex items-start">
|
|
56
|
+
<dt class="w-32 flex-shrink-0 text-xs font-bold text-gray-400 uppercase tracking-wide pt-0.5"><%= label %></dt>
|
|
57
|
+
<dd class="font-mono text-sm text-gray-700"><%= value.presence || "—" %></dd>
|
|
58
|
+
</div>
|
|
59
|
+
<% end %>
|
|
60
|
+
</dl>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- Arguments -->
|
|
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">
|
|
67
|
+
<h3 class="text-sm font-bold text-gray-900">Arguments</h3>
|
|
68
|
+
</div>
|
|
69
|
+
<div class="p-6">
|
|
70
|
+
<%
|
|
71
|
+
args_data = @job.arguments
|
|
72
|
+
formatted_args = begin
|
|
73
|
+
parsed = args_data.is_a?(String) ? JSON.parse(args_data) : args_data
|
|
74
|
+
JSON.pretty_generate(parsed)
|
|
75
|
+
rescue
|
|
76
|
+
args_data.to_s
|
|
77
|
+
end
|
|
78
|
+
%>
|
|
79
|
+
<pre class="bg-gray-50 rounded-lg p-4 text-xs font-mono text-gray-700 max-h-80 overflow-y-auto ring-1 ring-inset ring-gray-900/5" style="white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word;"><%= formatted_args %></pre>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<!-- Error info -->
|
|
85
|
+
<% if @job.failed_execution %>
|
|
86
|
+
<div class="bg-red-50 rounded-xl border border-red-200 shadow-sm overflow-hidden mb-8 ring-1 ring-inset ring-red-600/10">
|
|
87
|
+
<div class="px-6 py-4 border-b border-red-200/50">
|
|
88
|
+
<h3 class="text-sm font-bold text-red-900">Error Details</h3>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="p-6">
|
|
91
|
+
<% error = begin; JSON.parse(@job.failed_execution.error.to_s); rescue; {}; end %>
|
|
92
|
+
<dl class="space-y-4">
|
|
93
|
+
<div class="flex items-start">
|
|
94
|
+
<dt class="w-32 flex-shrink-0 text-xs font-bold text-red-400 uppercase tracking-wide pt-0.5">Exception</dt>
|
|
95
|
+
<dd class="font-mono text-sm text-red-700 font-bold"><%= error["exception_class"] || "Unknown" %></dd>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="flex items-start">
|
|
98
|
+
<dt class="w-32 flex-shrink-0 text-xs font-bold text-red-400 uppercase tracking-wide pt-0.5">Message</dt>
|
|
99
|
+
<dd class="font-mono text-sm text-red-700"><%= error["message"] || "No message" %></dd>
|
|
100
|
+
</div>
|
|
101
|
+
<div class="flex items-start">
|
|
102
|
+
<dt class="w-32 flex-shrink-0 text-xs font-bold text-red-400 uppercase tracking-wide pt-0.5">Failed At</dt>
|
|
103
|
+
<dd class="font-mono text-sm text-red-700"><%= format_datetime(@job.failed_execution.created_at) %></dd>
|
|
104
|
+
</div>
|
|
105
|
+
</dl>
|
|
106
|
+
<% if error["backtrace"].present? %>
|
|
107
|
+
<div class="mt-6">
|
|
108
|
+
<p class="text-xs font-bold text-red-400 uppercase tracking-wide mb-2">Backtrace</p>
|
|
109
|
+
<pre class="bg-red-100/50 rounded-lg p-4 text-xs font-mono text-red-800 max-h-64 overflow-y-auto ring-1 ring-inset ring-red-600/10" style="white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word;"><%= Array(error["backtrace"]).first(20).join("\n") %></pre>
|
|
110
|
+
</div>
|
|
111
|
+
<% end %>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
<% end %>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
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">Processes</h1>
|
|
7
|
+
<p class="text-sm text-gray-500 mt-1">Active Solid Queue workers and supervisors</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 @processes.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">Active Processes</h2>
|
|
19
|
+
<span class="text-xs text-gray-400"><%= @processes.size %> running</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="processes-table" placeholder="Filter processes…" 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="processes-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="num">ID</th>
|
|
31
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Kind</th>
|
|
32
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Hostname</th>
|
|
33
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">PID</th>
|
|
34
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Last Heartbeat</th>
|
|
35
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Started</th>
|
|
36
|
+
</tr>
|
|
37
|
+
</thead>
|
|
38
|
+
<tbody class="divide-y divide-gray-50">
|
|
39
|
+
<% @processes.each do |proc| %>
|
|
40
|
+
<% meta = proc.metadata || {} %>
|
|
41
|
+
<tr class="hover:bg-gray-50/80 transition-colors">
|
|
42
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-700"><%= proc.id %></td>
|
|
43
|
+
<td class="px-6 py-3.5">
|
|
44
|
+
<% is_supervisor = proc.respond_to?(:supervisor?) ? proc.supervisor? : (proc.kind.to_s.downcase == "supervisor") %>
|
|
45
|
+
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-[11px] font-semibold ring-1 ring-inset
|
|
46
|
+
<%= is_supervisor ? 'bg-indigo-50 text-indigo-700 ring-indigo-700/10' : 'bg-blue-50 text-blue-700 ring-blue-700/10' %>">
|
|
47
|
+
<%= is_supervisor ? "Supervisor" : (meta["kind"] || "Worker") %>
|
|
48
|
+
</span>
|
|
49
|
+
</td>
|
|
50
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= proc.hostname rescue "—" %></td>
|
|
51
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= proc.pid rescue "—" %></td>
|
|
52
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= time_ago_short(proc.last_heartbeat_at) %></td>
|
|
53
|
+
<td class="px-6 py-3.5 font-mono text-xs text-gray-500"><%= format_datetime(proc.created_at) %></td>
|
|
54
|
+
</tr>
|
|
55
|
+
<% end %>
|
|
56
|
+
</tbody>
|
|
57
|
+
</table>
|
|
58
|
+
<% else %>
|
|
59
|
+
<div class="py-20 text-center">
|
|
60
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
|
|
61
|
+
<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.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/></svg>
|
|
62
|
+
</div>
|
|
63
|
+
<p class="text-sm text-gray-500 font-medium">No active processes</p>
|
|
64
|
+
<p class="text-xs text-gray-400 mt-1">Start Solid Queue workers to see them here</p>
|
|
65
|
+
</div>
|
|
66
|
+
<% end %>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
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">Queue Management</h1>
|
|
6
|
+
<p class="text-sm text-gray-500 mt-1">Solid Queue — manage queues, jobs, and workers</p>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="flex items-center gap-3">
|
|
9
|
+
<a href="<%= solid_ops.failed_jobs_path %>" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium 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">
|
|
10
|
+
<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="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/></svg>
|
|
11
|
+
Failed (<%= @failed_count %>)
|
|
12
|
+
</a>
|
|
13
|
+
<% if @finished_count > 0 %>
|
|
14
|
+
<%= form_tag(solid_ops.clear_finished_jobs_path, method: :post, class: "inline") do %>
|
|
15
|
+
<button type="submit" class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-gray-600 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors shadow-sm"
|
|
16
|
+
onclick="return confirm('Clear <%= @finished_count %> finished jobs?')">
|
|
17
|
+
<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>
|
|
18
|
+
Clear Finished (<%= number_with_delimiter(@finished_count) %>)
|
|
19
|
+
</button>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<!-- Global stats -->
|
|
26
|
+
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-8">
|
|
27
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
28
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-gray-400 to-gray-500"></div>
|
|
29
|
+
<div class="p-4 pt-5">
|
|
30
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Total Jobs</p>
|
|
31
|
+
<p class="text-2xl font-extrabold font-mono mt-1 text-gray-900"><%= number_with_delimiter(@total_jobs) %></p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
35
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-yellow-400 to-yellow-500"></div>
|
|
36
|
+
<div class="p-4 pt-5">
|
|
37
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Ready</p>
|
|
38
|
+
<p class="text-2xl font-extrabold font-mono mt-1 text-yellow-600"><%= number_with_delimiter(@ready_count) %></p>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
42
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-indigo-400 to-indigo-600"></div>
|
|
43
|
+
<div class="p-4 pt-5">
|
|
44
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Scheduled</p>
|
|
45
|
+
<p class="text-2xl font-extrabold font-mono mt-1 text-indigo-600"><%= number_with_delimiter(@scheduled_count) %></p>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
49
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-blue-400 to-blue-600"></div>
|
|
50
|
+
<div class="p-4 pt-5">
|
|
51
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Claimed</p>
|
|
52
|
+
<p class="text-2xl font-extrabold font-mono mt-1 text-blue-600"><%= number_with_delimiter(@claimed_count) %></p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
56
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-red-400 to-red-600"></div>
|
|
57
|
+
<div class="p-4 pt-5">
|
|
58
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Failed</p>
|
|
59
|
+
<p class="text-2xl font-extrabold font-mono mt-1 text-red-600"><%= number_with_delimiter(@failed_count) %></p>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm hover:shadow-md transition-shadow relative overflow-hidden">
|
|
63
|
+
<div class="absolute top-0 inset-x-0 h-1 bg-gradient-to-r from-orange-400 to-orange-500"></div>
|
|
64
|
+
<div class="p-4 pt-5">
|
|
65
|
+
<p class="text-xs font-semibold text-gray-500 uppercase tracking-wide">Blocked</p>
|
|
66
|
+
<p class="text-2xl font-extrabold font-mono mt-1 text-orange-600"><%= number_with_delimiter(@blocked_count) %></p>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<!-- Queues table -->
|
|
72
|
+
<div class="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden mb-8">
|
|
73
|
+
<div class="px-6 py-4 border-b border-gray-100 flex items-center justify-between">
|
|
74
|
+
<h2 class="text-sm font-bold text-gray-900">Queues</h2>
|
|
75
|
+
<span class="text-xs text-gray-400"><%= @queues.size %> queue<%= "s" unless @queues.size == 1 %></span>
|
|
76
|
+
</div>
|
|
77
|
+
<% if @queues.any? %>
|
|
78
|
+
<div style="padding: 0.625rem 1rem; border-bottom: 1px solid rgb(243 244 246);">
|
|
79
|
+
<div style="position: relative; display: inline-block;">
|
|
80
|
+
<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>
|
|
81
|
+
<input type="text" data-solid-search="queues-table" placeholder="Filter queues…" 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);">
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
<table id="queues-table" class="min-w-full divide-y divide-gray-100">
|
|
85
|
+
<thead>
|
|
86
|
+
<tr class="bg-gray-50">
|
|
87
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Queue</th>
|
|
88
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">Total</th>
|
|
89
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">Ready</th>
|
|
90
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">Scheduled</th>
|
|
91
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">Claimed</th>
|
|
92
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="num">Failed</th>
|
|
93
|
+
<th class="px-6 py-3 text-left text-[11px] font-bold text-gray-500 uppercase tracking-wider" data-sort="text">Status</th>
|
|
94
|
+
<th class="px-6 py-3 text-right text-[11px] font-bold text-gray-500 uppercase tracking-wider">Actions</th>
|
|
95
|
+
</tr>
|
|
96
|
+
</thead>
|
|
97
|
+
<tbody class="divide-y divide-gray-50">
|
|
98
|
+
<% @queues.each do |q| %>
|
|
99
|
+
<tr class="hover:bg-gray-50/80 transition-colors group" style="cursor: pointer;" onclick="window.location='<%= solid_ops.queue_path(q[:name]) %>'">
|
|
100
|
+
<td class="px-6 py-3.5">
|
|
101
|
+
<span class="font-mono text-sm text-blue-600 font-bold"><%= q[:name] %></span>
|
|
102
|
+
</td>
|
|
103
|
+
<td class="px-6 py-3.5 font-mono text-sm font-semibold text-gray-900"><%= q[:total] %></td>
|
|
104
|
+
<td class="px-6 py-3.5 font-mono text-sm text-yellow-600"><%= q[:ready] %></td>
|
|
105
|
+
<td class="px-6 py-3.5 font-mono text-sm text-indigo-600"><%= q[:scheduled] %></td>
|
|
106
|
+
<td class="px-6 py-3.5 font-mono text-sm text-blue-600"><%= q[:claimed] %></td>
|
|
107
|
+
<td class="px-6 py-3.5 font-mono text-sm text-red-600"><%= q[:failed] %></td>
|
|
108
|
+
<td class="px-6 py-3.5">
|
|
109
|
+
<% if q[:paused] %>
|
|
110
|
+
<span class="<%= status_pill('paused') %>">Paused</span>
|
|
111
|
+
<% else %>
|
|
112
|
+
<span class="<%= status_pill('ready') %>">Active</span>
|
|
113
|
+
<% end %>
|
|
114
|
+
</td>
|
|
115
|
+
<td class="px-6 py-3.5 text-right" onclick="event.stopPropagation()">
|
|
116
|
+
<% if q[:paused] %>
|
|
117
|
+
<%= form_tag(solid_ops.resume_queue_path(q[:name]), method: :post, class: "inline") do %>
|
|
118
|
+
<button type="submit" class="px-3 py-1.5 text-xs font-semibold text-emerald-700 bg-emerald-50 border border-emerald-200 rounded-md hover:bg-emerald-100 transition-colors ring-1 ring-inset ring-emerald-600/20">Resume</button>
|
|
119
|
+
<% end %>
|
|
120
|
+
<% else %>
|
|
121
|
+
<%= form_tag(solid_ops.pause_queue_path(q[:name]), method: :post, class: "inline") do %>
|
|
122
|
+
<button type="submit" class="px-3 py-1.5 text-xs font-semibold text-amber-700 bg-amber-50 border border-amber-200 rounded-md hover:bg-amber-100 transition-colors ring-1 ring-inset ring-amber-600/20"
|
|
123
|
+
onclick="if(!confirm('Pause queue \'<%= q[:name] %>\'')){event.preventDefault();}">Pause</button>
|
|
124
|
+
<% end %>
|
|
125
|
+
<% end %>
|
|
126
|
+
</td>
|
|
127
|
+
</tr>
|
|
128
|
+
<% end %>
|
|
129
|
+
</tbody>
|
|
130
|
+
</table>
|
|
131
|
+
<% else %>
|
|
132
|
+
<div class="py-20 text-center">
|
|
133
|
+
<div class="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gray-100 mb-4">
|
|
134
|
+
<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="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"/></svg>
|
|
135
|
+
</div>
|
|
136
|
+
<p class="text-sm text-gray-500 font-medium">No queues found</p>
|
|
137
|
+
<p class="text-xs text-gray-400 mt-1">Jobs haven't been enqueued yet</p>
|
|
138
|
+
</div>
|
|
139
|
+
<% end %>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<!-- Quick links -->
|
|
143
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
144
|
+
<a href="<%= solid_ops.processes_path %>" class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm hover:shadow-md hover:border-blue-200 transition-all group relative overflow-hidden">
|
|
145
|
+
<div class="absolute top-0 inset-x-0 h-0.5 bg-gradient-to-r from-blue-500 to-blue-600 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
|
146
|
+
<div class="flex items-center gap-3">
|
|
147
|
+
<div class="w-10 h-10 rounded-xl bg-blue-50 flex items-center justify-center ring-1 ring-inset ring-blue-500/10">
|
|
148
|
+
<svg class="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" 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>
|
|
149
|
+
</div>
|
|
150
|
+
<div>
|
|
151
|
+
<p class="text-sm font-bold text-gray-900 group-hover:text-blue-600 transition-colors">Processes</p>
|
|
152
|
+
<p class="text-xs text-gray-400">View active workers and supervisors</p>
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
</a>
|
|
156
|
+
<a href="<%= solid_ops.recurring_tasks_path %>" class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm hover:shadow-md hover:border-indigo-200 transition-all group relative overflow-hidden">
|
|
157
|
+
<div class="absolute top-0 inset-x-0 h-0.5 bg-gradient-to-r from-indigo-500 to-indigo-600 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
|
158
|
+
<div class="flex items-center gap-3">
|
|
159
|
+
<div class="w-10 h-10 rounded-xl bg-indigo-50 flex items-center justify-center ring-1 ring-inset ring-indigo-500/10">
|
|
160
|
+
<svg class="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
|
161
|
+
</div>
|
|
162
|
+
<div>
|
|
163
|
+
<p class="text-sm font-bold text-gray-900 group-hover:text-indigo-600 transition-colors">Recurring Tasks</p>
|
|
164
|
+
<p class="text-xs text-gray-400">View configured cron-style jobs</p>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</a>
|
|
168
|
+
<a href="<%= solid_ops.dashboard_jobs_path %>" class="bg-white rounded-xl border border-gray-200 p-5 shadow-sm hover:shadow-md hover:border-emerald-200 transition-all group relative overflow-hidden">
|
|
169
|
+
<div class="absolute top-0 inset-x-0 h-0.5 bg-gradient-to-r from-emerald-500 to-emerald-600 opacity-0 group-hover:opacity-100 transition-opacity"></div>
|
|
170
|
+
<div class="flex items-center gap-3">
|
|
171
|
+
<div class="w-10 h-10 rounded-xl bg-emerald-50 flex items-center justify-center ring-1 ring-inset ring-emerald-500/10">
|
|
172
|
+
<svg class="w-5 h-5 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/></svg>
|
|
173
|
+
</div>
|
|
174
|
+
<div>
|
|
175
|
+
<p class="text-sm font-bold text-gray-900 group-hover:text-emerald-600 transition-colors">Job Analytics</p>
|
|
176
|
+
<p class="text-xs text-gray-400">Performance stats from events</p>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</a>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|