cogworker 0.1.0 → 0.2.1
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 +4 -4
- data/lib/cogworker/attempts.rb +47 -0
- data/lib/cogworker/basic_fetch.rb +25 -2
- data/lib/cogworker/heartbeat.rb +4 -2
- data/lib/cogworker/history/storage.rb +58 -22
- data/lib/cogworker/history.rb +57 -7
- data/lib/cogworker/manager.rb +4 -0
- data/lib/cogworker/periodic/ticker.rb +16 -1
- data/lib/cogworker/process.rb +13 -5
- data/lib/cogworker/processor.rb +4 -0
- data/lib/cogworker/queue.rb +17 -0
- data/lib/cogworker/redis_keys.rb +5 -0
- data/lib/cogworker/throughput.rb +51 -0
- data/lib/cogworker/version.rb +1 -1
- data/lib/cogworker/web/assets/nocturne/fonts/inter-latin.woff2 +0 -0
- data/lib/cogworker/web/assets/nocturne/styles.css +582 -0
- data/lib/cogworker/web/assets/phosphor/Phosphor.woff2 +0 -0
- data/lib/cogworker/web/assets/phosphor/style.css +4627 -0
- data/lib/cogworker/web/layout.rb +232 -141
- data/lib/cogworker/web/routes/history.rb +86 -32
- data/lib/cogworker/web/routes/jobs.rb +469 -0
- data/lib/cogworker/web/routes/overview.rb +746 -0
- data/lib/cogworker/web/routes/schedules.rb +197 -0
- data/lib/cogworker/web/routes/workers.rb +227 -0
- data/lib/cogworker/web.rb +4 -3
- metadata +11 -9
- data/lib/cogworker/web/assets/tailwind.css +0 -1
- data/lib/cogworker/web/routes/busy.rb +0 -99
- data/lib/cogworker/web/routes/dead.rb +0 -93
- data/lib/cogworker/web/routes/periodic.rb +0 -67
- data/lib/cogworker/web/routes/queues.rb +0 -101
- data/lib/cogworker/web/routes/retries.rb +0 -89
- data/lib/cogworker/web/routes/scheduled.rb +0 -49
- data/lib/cogworker/web/routes/stats.rb +0 -298
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Cogworker
|
|
4
|
-
class Web
|
|
5
|
-
module Routes
|
|
6
|
-
# Lists jobs that exhausted their retries, with delete and retry
|
|
7
|
-
# (requeue) actions.
|
|
8
|
-
module Dead
|
|
9
|
-
CONTENT_ID = 'dead-content'
|
|
10
|
-
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
def registered(app)
|
|
14
|
-
app.get('/dead') do
|
|
15
|
-
content = Dead.render_content(request.script_name)
|
|
16
|
-
if hx_request?
|
|
17
|
-
content
|
|
18
|
-
else
|
|
19
|
-
Layout.wrap('Dead', Layout.poll_div(CONTENT_ID, request.script_name, 'dead', content),
|
|
20
|
-
script_name: request.script_name)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
app.post('/dead/delete') do
|
|
25
|
-
Cogworker.config.redis { |c| c.zrem(RedisKeys::DEAD, params['raw']) }
|
|
26
|
-
Dead.respond(self)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
app.post('/dead/delete_all') do
|
|
30
|
-
Cogworker.config.redis { |c| c.del(RedisKeys::DEAD) }
|
|
31
|
-
Dead.respond(self)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Puts the job back on its original queue for one more attempt —
|
|
35
|
-
# same "graduate out of this ZSET, back onto cogworker:queue:<q>"
|
|
36
|
-
# move `Routes::Retries#retry_now` does for `cogworker:retry`,
|
|
37
|
-
# and `Cogworker::Scheduled#graduate` does for both `cogworker:
|
|
38
|
-
# schedule`/`cogworker:retry`. `zrem` winning (not losing) is what
|
|
39
|
-
# actually gates the requeue: without it, two browser tabs (or a
|
|
40
|
-
# slow double-click) retrying the same entry at once could both
|
|
41
|
-
# see it and each push a copy. NOTE: for a single (non-Array)
|
|
42
|
-
# member, the `redis` gem's `#zrem` returns a **Boolean**, not the
|
|
43
|
-
# raw `1`/`0` integer reply — `== 1` would always be false here
|
|
44
|
-
# (`true == 1` is `false` in Ruby); check truthiness instead, the
|
|
45
|
-
# same way `Cogworker::Scheduled#graduate`'s `next unless won` does.
|
|
46
|
-
app.post('/dead/retry') do
|
|
47
|
-
raw = params['raw']
|
|
48
|
-
Cogworker.config.redis do |c|
|
|
49
|
-
if c.zrem(RedisKeys::DEAD, raw)
|
|
50
|
-
job = JSON.parse(raw)
|
|
51
|
-
c.sadd(RedisKeys::QUEUES, job['queue'])
|
|
52
|
-
c.lpush(RedisKeys.queue(job['queue']), raw)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
Dead.respond(self)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# After an action, htmx gets the refreshed fragment swapped into
|
|
60
|
-
# #dead-content in place; a plain form submission (no JS) falls back
|
|
61
|
-
# to a normal redirect back to the full page.
|
|
62
|
-
def respond(action)
|
|
63
|
-
if action.hx_request?
|
|
64
|
-
render_content(action.request.script_name)
|
|
65
|
-
else
|
|
66
|
-
action.redirect(Layout.path(action.request.script_name, 'dead'))
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def render_content(script_name)
|
|
71
|
-
# Newest DiedAt first — `zrevrange` (`cogworker:dead`'s score is
|
|
72
|
-
# the epoch it died at), not `zrange`.
|
|
73
|
-
entries = Cogworker.config.redis { |c| c.zrevrange(RedisKeys::DEAD, 0, -1, withscores: true) }
|
|
74
|
-
delete_path = Layout.path(script_name, 'dead/delete')
|
|
75
|
-
retry_path = Layout.path(script_name, 'dead/retry')
|
|
76
|
-
rows = entries.map do |raw, score|
|
|
77
|
-
job = JSON.parse(raw)
|
|
78
|
-
[job['jid'], Layout.h(job['class']), Layout.time_tag(score),
|
|
79
|
-
Layout.form_button(retry_path, 'raw', raw, 'retry', hx_target: "##{CONTENT_ID}", variant: :primary),
|
|
80
|
-
Layout.form_button(delete_path, 'raw', raw, 'delete', hx_target: "##{CONTENT_ID}", variant: :danger)]
|
|
81
|
-
end
|
|
82
|
-
delete_all_path = Layout.path(script_name, 'dead/delete_all')
|
|
83
|
-
delete_all_button = Layout.action_button(delete_all_path, 'delete all', hx_target: "##{CONTENT_ID}",
|
|
84
|
-
variant: :danger)
|
|
85
|
-
%(<div class="mb-3 flex justify-end">#{delete_all_button}</div>) +
|
|
86
|
-
Layout.table(%w[JID Class DiedAt Retry Delete], rows, empty_message: 'No dead jobs.')
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
Cogworker::Web.register(Cogworker::Web::Routes::Dead)
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'fugit'
|
|
4
|
-
require 'json'
|
|
5
|
-
|
|
6
|
-
module Cogworker
|
|
7
|
-
class Web
|
|
8
|
-
module Routes
|
|
9
|
-
# Read-only: lists every registered `config.periodic { |mgr|
|
|
10
|
-
# mgr.register(...) }` entry and when it last/next fires. Sourced
|
|
11
|
-
# entirely from Redis (`periodic:schedule`/`periodic:last_slot:<pjid>`),
|
|
12
|
-
# not from any in-process `Periodic::Manager` — those are only
|
|
13
|
-
# published once an actual worker process's `Periodic::Ticker` has
|
|
14
|
-
# booted (see `Ticker#publish_schedule!`), so a Web-UI-only process
|
|
15
|
-
# (no worker ever started) shows the empty state here, same as Busy
|
|
16
|
-
# shows no processes with no worker running.
|
|
17
|
-
module Periodic
|
|
18
|
-
CONTENT_ID = 'periodic-content'
|
|
19
|
-
|
|
20
|
-
module_function
|
|
21
|
-
|
|
22
|
-
def registered(app)
|
|
23
|
-
app.get('/periodic') do
|
|
24
|
-
content = Routes::Periodic.render_content
|
|
25
|
-
if hx_request?
|
|
26
|
-
content
|
|
27
|
-
else
|
|
28
|
-
Layout.wrap('Periodic', Layout.poll_div(CONTENT_ID, request.script_name, 'periodic', content),
|
|
29
|
-
script_name: request.script_name)
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def render_content
|
|
35
|
-
schedule = Cogworker.config.redis { |c| c.hgetall(RedisKeys::PERIODIC_SCHEDULE) }
|
|
36
|
-
rows = schedule.map { |pjid, raw| row_for(pjid, JSON.parse(raw)) }
|
|
37
|
-
Layout.table(%w[Class Cron Args NextRun LastRun Unique], rows,
|
|
38
|
-
empty_message: 'Nothing registered yet (no worker process has booted the periodic ' \
|
|
39
|
-
'scheduler — periodic jobs are published to Redis by ' \
|
|
40
|
-
'Periodic::Ticker on worker startup, not by the Web UI process).')
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def row_for(pjid, entry)
|
|
44
|
-
[Layout.h(entry['class']), Layout.h(entry['cron']), Layout.h(entry['args'].to_json),
|
|
45
|
-
next_run_tag(entry['cron']), last_run_tag(pjid), unique_cell(entry['unique'])]
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def next_run_tag(cron)
|
|
49
|
-
Layout.time_tag(Fugit::Cron.parse(cron)&.next_time(Time.now)&.to_t)
|
|
50
|
-
rescue StandardError
|
|
51
|
-
Layout.h('invalid cron')
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def last_run_tag(pjid)
|
|
55
|
-
last_slot = Cogworker.config.redis { |c| c.get(RedisKeys.periodic_last_slot(pjid)) }
|
|
56
|
-
last_slot ? Layout.time_tag(last_slot.to_f) : %(<span class="text-gray-400 italic">never</span>)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
def unique_cell(unique)
|
|
60
|
-
unique.nil? || unique.empty? ? '' : Layout.badge(unique, variant: :warning)
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
Cogworker::Web.register(Cogworker::Web::Routes::Periodic)
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Cogworker
|
|
4
|
-
class Web
|
|
5
|
-
module Routes
|
|
6
|
-
# Lists every known queue with its size/latency, and a per-queue drill
|
|
7
|
-
# down page listing that queue's pending jobs.
|
|
8
|
-
module Queues
|
|
9
|
-
CONTENT_ID = 'queues-content'
|
|
10
|
-
QUEUE_CONTENT_ID = 'queue-content'
|
|
11
|
-
|
|
12
|
-
module_function
|
|
13
|
-
|
|
14
|
-
def registered(app)
|
|
15
|
-
app.get('/queues') do
|
|
16
|
-
content = Queues.render_content(request.script_name)
|
|
17
|
-
if hx_request?
|
|
18
|
-
content
|
|
19
|
-
else
|
|
20
|
-
Layout.wrap('Queues', Layout.poll_div(CONTENT_ID, request.script_name, 'queues', content),
|
|
21
|
-
script_name: request.script_name)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
app.get('/queues/:name') do
|
|
26
|
-
name = url_params('name')
|
|
27
|
-
content = Queues.render_queue_content(name, request.script_name)
|
|
28
|
-
if hx_request?
|
|
29
|
-
content
|
|
30
|
-
else
|
|
31
|
-
Layout.wrap("Queue: #{Layout.h(name)}",
|
|
32
|
-
Layout.poll_div(QUEUE_CONTENT_ID, request.script_name, "queues/#{name}", content),
|
|
33
|
-
script_name: request.script_name)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# `raw` is the exact JSON string the job entry was pushed with —
|
|
38
|
-
# same "raw" identity `Routes::Dead`/`Routes::Retries` already key
|
|
39
|
-
# their own per-row delete off — so `Queue#delete` can `LREM` it
|
|
40
|
-
# back out of the list.
|
|
41
|
-
app.post('/queues/:name/delete') do
|
|
42
|
-
name = url_params('name')
|
|
43
|
-
Cogworker::Queue.new(name).delete(params['raw'])
|
|
44
|
-
Queues.respond(self, name)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
app.post('/queues/:name/delete_all') do
|
|
48
|
-
name = url_params('name')
|
|
49
|
-
Cogworker::Queue.new(name).clear
|
|
50
|
-
Queues.respond(self, name)
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# After an action, htmx gets the refreshed fragment swapped into
|
|
55
|
-
# #queue-content in place; a plain form submission (no JS) falls
|
|
56
|
-
# back to a normal redirect back to the queue's own page.
|
|
57
|
-
def respond(action, name)
|
|
58
|
-
if action.hx_request?
|
|
59
|
-
render_queue_content(name, action.request.script_name)
|
|
60
|
-
else
|
|
61
|
-
action.redirect(Layout.path(action.request.script_name, "queues/#{name}"))
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def render_content(script_name)
|
|
66
|
-
names = Cogworker.config.redis { |c| c.smembers(RedisKeys::QUEUES) }.sort
|
|
67
|
-
rows = names.map do |name|
|
|
68
|
-
q = Cogworker::Queue.new(name)
|
|
69
|
-
link = Layout.path(script_name, "queues/#{Layout.h(name)}")
|
|
70
|
-
[%(<a class="text-indigo-600 dark:text-indigo-400 hover:underline font-medium" href="#{link}">#{Layout.h(name)}</a>),
|
|
71
|
-
q.size, q.latency.round(2)]
|
|
72
|
-
end
|
|
73
|
-
Layout.table(%w[Name Size Latency], rows, empty_message: 'No queues yet — push a job to create one.')
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def render_queue_content(name, script_name)
|
|
77
|
-
delete_path = Layout.path(script_name, "queues/#{name}/delete")
|
|
78
|
-
rows = Cogworker::Queue.new(name).map do |r|
|
|
79
|
-
[r.jid, Layout.h(r.klass), Layout.h(r.args.to_s),
|
|
80
|
-
Layout.form_button(delete_path, 'raw', r.value, 'delete', hx_target: "##{QUEUE_CONTENT_ID}",
|
|
81
|
-
variant: :danger)]
|
|
82
|
-
end
|
|
83
|
-
delete_all_button(name, script_name, rows.empty?) +
|
|
84
|
-
Layout.table(%w[JID Class Args Delete], rows, empty_message: 'This queue is empty.')
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# Nothing to bulk-delete once the queue is already empty.
|
|
88
|
-
def delete_all_button(name, script_name, empty)
|
|
89
|
-
return '' if empty
|
|
90
|
-
|
|
91
|
-
delete_all_path = Layout.path(script_name, "queues/#{name}/delete_all")
|
|
92
|
-
button = Layout.action_button(delete_all_path, 'delete all', hx_target: "##{QUEUE_CONTENT_ID}",
|
|
93
|
-
variant: :danger)
|
|
94
|
-
%(<div class="mb-3 flex justify-end">#{button}</div>)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
Cogworker::Web.register(Cogworker::Web::Routes::Queues)
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Cogworker
|
|
4
|
-
class Web
|
|
5
|
-
module Routes
|
|
6
|
-
# Lists jobs currently scheduled for retry, with delete and
|
|
7
|
-
# retry-now (skip the backoff) actions.
|
|
8
|
-
module Retries
|
|
9
|
-
CONTENT_ID = 'retries-content'
|
|
10
|
-
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
def registered(app)
|
|
14
|
-
app.get('/retries') do
|
|
15
|
-
content = Retries.render_content(request.script_name)
|
|
16
|
-
if hx_request?
|
|
17
|
-
content
|
|
18
|
-
else
|
|
19
|
-
Layout.wrap('Retries', Layout.poll_div(CONTENT_ID, request.script_name, 'retries', content),
|
|
20
|
-
script_name: request.script_name)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
app.post('/retries/delete') do
|
|
25
|
-
Cogworker.config.redis { |c| c.zrem(RedisKeys::RETRY, params['raw']) }
|
|
26
|
-
Retries.respond(self)
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
app.post('/retries/retry_now') do
|
|
30
|
-
raw = params['raw']
|
|
31
|
-
Cogworker.config.redis do |c|
|
|
32
|
-
# For a single (non-Array) member, the `redis` gem's `#zrem`
|
|
33
|
-
# returns a **Boolean**, not the raw `1`/`0` integer reply —
|
|
34
|
-
# `== 1` is always false here (`true == 1` is `false` in
|
|
35
|
-
# Ruby); check truthiness instead, matching
|
|
36
|
-
# `Cogworker::Scheduled#graduate`'s `next unless won`.
|
|
37
|
-
if c.zrem(RedisKeys::RETRY, raw)
|
|
38
|
-
job = JSON.parse(raw)
|
|
39
|
-
c.sadd(RedisKeys::QUEUES, job['queue'])
|
|
40
|
-
c.lpush(RedisKeys.queue(job['queue']), raw)
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
Retries.respond(self)
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# After an action, htmx gets the refreshed fragment swapped into
|
|
48
|
-
# #retries-content in place; a plain form submission (no JS) falls
|
|
49
|
-
# back to a normal redirect back to the full page.
|
|
50
|
-
def respond(action)
|
|
51
|
-
if action.hx_request?
|
|
52
|
-
render_content(action.request.script_name)
|
|
53
|
-
else
|
|
54
|
-
action.redirect(Layout.path(action.request.script_name, 'retries'))
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def render_content(script_name)
|
|
59
|
-
entries = Cogworker.config.redis { |c| c.zrange(RedisKeys::RETRY, 0, -1) }
|
|
60
|
-
rows = entries.map do |raw|
|
|
61
|
-
job = JSON.parse(raw)
|
|
62
|
-
[job['jid'], Layout.h(job['class']), error_cell(job), delete_form(script_name, raw),
|
|
63
|
-
retry_now_form(script_name, raw)]
|
|
64
|
-
end
|
|
65
|
-
Layout.table(%w[JID Class Error Delete RetryNow], rows, empty_message: 'No retries pending.')
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
def error_cell(job)
|
|
69
|
-
<<~HTML
|
|
70
|
-
<div class="font-mono text-xs text-red-700 dark:text-red-400">#{Layout.h(job['error_class'])}</div>
|
|
71
|
-
<div class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-xs">#{Layout.h(job['error_message'])}</div>
|
|
72
|
-
HTML
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def delete_form(script_name, raw)
|
|
76
|
-
action = Layout.path(script_name, 'retries/delete')
|
|
77
|
-
Layout.form_button(action, 'raw', raw, 'delete', hx_target: "##{CONTENT_ID}", variant: :danger)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def retry_now_form(script_name, raw)
|
|
81
|
-
action = Layout.path(script_name, 'retries/retry_now')
|
|
82
|
-
Layout.form_button(action, 'raw', raw, 'retry now', hx_target: "##{CONTENT_ID}", variant: :primary)
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
Cogworker::Web.register(Cogworker::Web::Routes::Retries)
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Cogworker
|
|
4
|
-
class Web
|
|
5
|
-
module Routes
|
|
6
|
-
# Lists jobs pushed with `perform_in`/`perform_at`, still waiting for
|
|
7
|
-
# their run-at time, with a delete action.
|
|
8
|
-
module Scheduled
|
|
9
|
-
CONTENT_ID = 'scheduled-content'
|
|
10
|
-
|
|
11
|
-
module_function
|
|
12
|
-
|
|
13
|
-
def registered(app)
|
|
14
|
-
app.get('/scheduled') do
|
|
15
|
-
content = Scheduled.render_content(request.script_name)
|
|
16
|
-
if hx_request?
|
|
17
|
-
content
|
|
18
|
-
else
|
|
19
|
-
Layout.wrap('Scheduled', Layout.poll_div(CONTENT_ID, request.script_name, 'scheduled', content),
|
|
20
|
-
script_name: request.script_name)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
app.post('/scheduled/delete') do
|
|
25
|
-
Cogworker.config.redis { |c| c.zrem(RedisKeys::SCHEDULE, params['raw']) }
|
|
26
|
-
if hx_request?
|
|
27
|
-
Scheduled.render_content(request.script_name)
|
|
28
|
-
else
|
|
29
|
-
redirect Layout.path(request.script_name, 'scheduled')
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def render_content(script_name)
|
|
35
|
-
entries = Cogworker.config.redis { |c| c.zrange(RedisKeys::SCHEDULE, 0, -1, withscores: true) }
|
|
36
|
-
delete_path = Layout.path(script_name, 'scheduled/delete')
|
|
37
|
-
rows = entries.map do |raw, score|
|
|
38
|
-
job = JSON.parse(raw)
|
|
39
|
-
[job['jid'], Layout.h(job['class']), Layout.time_tag(score),
|
|
40
|
-
Layout.form_button(delete_path, 'raw', raw, 'delete', hx_target: "##{CONTENT_ID}", variant: :danger)]
|
|
41
|
-
end
|
|
42
|
-
Layout.table(%w[JID Class RunAt Delete], rows, empty_message: 'Nothing scheduled.')
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
Cogworker::Web.register(Cogworker::Web::Routes::Scheduled)
|