Packages
phoenix_kit
1.7.21
1.7.210
1.7.209
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit_web/live/modules/jobs/index.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Jobs"
current_path={@url_path}
project_title={@project_title}
current_locale={assigns[:current_locale]}
>
<div class="p-6">
<div class="mb-6">
<h1 class="text-3xl font-bold text-base-content">Jobs</h1>
<p class="text-base-content/70 mt-1">View background job status and history</p>
</div>
<%!-- Stats Cards --%>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-3 mb-6">
<div class="stat bg-base-100 rounded-lg shadow p-3">
<div class="stat-title text-xs">Available</div>
<div class="stat-value text-lg text-info">{Map.get(@stats, "available", 0)}</div>
</div>
<div class="stat bg-base-100 rounded-lg shadow p-3">
<div class="stat-title text-xs">Scheduled</div>
<div class="stat-value text-lg text-warning">{Map.get(@stats, "scheduled", 0)}</div>
</div>
<div class="stat bg-base-100 rounded-lg shadow p-3">
<div class="stat-title text-xs">Executing</div>
<div class="stat-value text-lg text-primary">{Map.get(@stats, "executing", 0)}</div>
</div>
<div class="stat bg-base-100 rounded-lg shadow p-3">
<div class="stat-title text-xs">Completed</div>
<div class="stat-value text-lg text-success">{Map.get(@stats, "completed", 0)}</div>
</div>
<div class="stat bg-base-100 rounded-lg shadow p-3">
<div class="stat-title text-xs">Retryable</div>
<div class="stat-value text-lg text-warning">{Map.get(@stats, "retryable", 0)}</div>
</div>
<div class="stat bg-base-100 rounded-lg shadow p-3">
<div class="stat-title text-xs">Discarded</div>
<div class="stat-value text-lg text-error">{Map.get(@stats, "discarded", 0)}</div>
</div>
<div class="stat bg-base-100 rounded-lg shadow p-3">
<div class="stat-title text-xs">Cancelled</div>
<div class="stat-value text-lg text-base-content/50">
{Map.get(@stats, "cancelled", 0)}
</div>
</div>
</div>
<%!-- Tabs --%>
<div role="tablist" class="tabs tabs-boxed mb-4 w-fit">
<button
type="button"
role="tab"
class={"tab #{if @active_tab == "oban", do: "tab-active"}"}
phx-click="switch_tab"
phx-value-tab="oban"
>
Oban Jobs
</button>
<button
type="button"
role="tab"
class={"tab #{if @active_tab == "scheduled", do: "tab-active"}"}
phx-click="switch_tab"
phx-value-tab="scheduled"
>
Scheduled Tasks
<%= if length(@scheduled_jobs) > 0 do %>
<span class="badge badge-sm ml-1">{length(@scheduled_jobs)}</span>
<% end %>
</button>
</div>
<%!-- Oban Jobs Tab --%>
<div class={if @active_tab != "oban", do: "hidden"}>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4">
<div class="flex flex-wrap gap-4 mb-4">
<form phx-change="filter_queue" class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Queue</span>
</label>
<select class="select select-bordered select-sm" name="queue">
<option value="all" selected={@filter_queue == "all"}>All Queues</option>
<%= for {queue, _count} <- @queue_stats do %>
<option value={queue} selected={@filter_queue == queue}>{queue}</option>
<% end %>
</select>
</form>
<form phx-change="filter_state" class="form-control">
<label class="label py-1">
<span class="label-text text-xs">State</span>
</label>
<select class="select select-bordered select-sm" name="state">
<option value="all" selected={@filter_state == "all"}>All States</option>
<option value="available" selected={@filter_state == "available"}>
available
</option>
<option value="scheduled" selected={@filter_state == "scheduled"}>
scheduled
</option>
<option value="executing" selected={@filter_state == "executing"}>
executing
</option>
<option value="completed" selected={@filter_state == "completed"}>
completed
</option>
<option value="retryable" selected={@filter_state == "retryable"}>
retryable
</option>
<option value="discarded" selected={@filter_state == "discarded"}>
discarded
</option>
<option value="cancelled" selected={@filter_state == "cancelled"}>
cancelled
</option>
</select>
</form>
<form phx-change="filter_worker" class="form-control">
<label class="label py-1">
<span class="label-text text-xs">Worker</span>
</label>
<select class="select select-bordered select-sm" name="worker">
<option value="all" selected={@filter_worker == "all"}>All Workers</option>
<%= for {worker, count} <- @worker_stats do %>
<option value={worker} selected={@filter_worker == worker}>
{short_worker_name(worker)} ({count})
</option>
<% end %>
</select>
</form>
<div class="flex-1"></div>
<div class="self-end flex items-center gap-4">
<%= if @hidden_workers != [] do %>
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-xs btn-ghost text-warning">
<.icon name="hero-eye-slash" class="w-4 h-4" />
{length(@hidden_workers)} hidden
</div>
<div
tabindex="0"
class="dropdown-content z-50 menu p-2 shadow bg-base-200 rounded-box w-64"
>
<div class="text-xs font-semibold px-2 py-1 text-base-content/70">
Hidden Workers
</div>
<%= for worker <- @hidden_workers do %>
<button
type="button"
class="btn btn-xs btn-ghost justify-start text-left"
phx-click="toggle_hide_worker"
phx-value-worker={worker}
>
<.icon name="hero-eye" class="w-3 h-3" />
<span class="truncate">{short_worker_name(worker)}</span>
</button>
<% end %>
<div class="divider my-1"></div>
<button
type="button"
class="btn btn-xs btn-ghost text-error"
phx-click="clear_hidden_workers"
>
Show all workers
</button>
</div>
</div>
<% end %>
<span class="text-sm text-base-content/70">
{@total_count} jobs | Auto-refresh: 30s
</span>
</div>
</div>
<%!-- Jobs Table --%>
<.table_default variant="zebra" size="sm">
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>ID</.table_default_header_cell>
<.table_default_header_cell>Queue</.table_default_header_cell>
<.table_default_header_cell>Worker</.table_default_header_cell>
<.table_default_header_cell>State</.table_default_header_cell>
<.table_default_header_cell>Attempt</.table_default_header_cell>
<.table_default_header_cell>Inserted</.table_default_header_cell>
<.table_default_header_cell>Scheduled</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= if Enum.empty?(@jobs) do %>
<.table_default_row hover={false}>
<.table_default_cell colspan={7} class="text-center py-8 text-base-content/50">
No jobs found
</.table_default_cell>
</.table_default_row>
<% else %>
<%= for job <- @jobs do %>
<.table_default_row
class="cursor-pointer hover:bg-base-200"
phx-click="show_job"
phx-value-id={job.id}
>
<.table_default_cell class="font-mono text-xs">{job.id}</.table_default_cell>
<.table_default_cell>
<span class="badge badge-sm badge-outline">{job.queue}</span>
</.table_default_cell>
<.table_default_cell class="text-sm">
<div class="flex items-center gap-1">
<span title={job.worker}>{short_worker_name(job.worker)}</span>
<button
type="button"
class="btn btn-ghost btn-xs opacity-30 hover:opacity-100"
phx-click="toggle_hide_worker"
phx-value-worker={job.worker}
title="Hide this worker"
>
<.icon name="hero-eye-slash" class="w-3 h-3" />
</button>
</div>
</.table_default_cell>
<.table_default_cell>
<span class={"badge badge-sm #{state_badge_class(job.state)}"}>
{job.state}
</span>
</.table_default_cell>
<.table_default_cell class="text-center">
{job.attempt}/{job.max_attempts}
</.table_default_cell>
<.table_default_cell class="text-xs text-base-content/70">
{format_datetime(job.inserted_at)}
</.table_default_cell>
<.table_default_cell class="text-xs text-base-content/70">
{format_datetime(job.scheduled_at)}
</.table_default_cell>
</.table_default_row>
<% end %>
<% end %>
</.table_default_body>
</.table_default>
<%!-- Pagination --%>
<%= if @total_pages > 1 do %>
<div class="flex justify-center gap-2 mt-4">
<button
class="btn btn-sm btn-outline"
phx-click="change_page"
phx-value-page={max(1, @current_page - 1)}
disabled={@current_page == 1}
>
Previous
</button>
<span class="btn btn-sm btn-ghost no-animation">
Page {@current_page} of {@total_pages}
</span>
<button
class="btn btn-sm btn-outline"
phx-click="change_page"
phx-value-page={min(@total_pages, @current_page + 1)}
disabled={@current_page == @total_pages}
>
Next
</button>
</div>
<% end %>
</div>
</div>
</div>
<%!-- Scheduled Tasks Tab --%>
<div class={if @active_tab != "scheduled", do: "hidden"}>
<div class="card bg-base-100 shadow-xl">
<div class="card-body p-4">
<div class="flex justify-between items-center mb-4">
<h3 class="font-semibold">Scheduled Tasks</h3>
<span class="text-sm text-base-content/70">
{length(@scheduled_jobs)} tasks | Auto-refresh: 30s
</span>
</div>
<.table_default variant="zebra" size="sm">
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>Type</.table_default_header_cell>
<.table_default_header_cell>Resource</.table_default_header_cell>
<.table_default_header_cell>Status</.table_default_header_cell>
<.table_default_header_cell>Scheduled For</.table_default_header_cell>
<.table_default_header_cell>Executed At</.table_default_header_cell>
<.table_default_header_cell>Args</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= if Enum.empty?(@scheduled_jobs) do %>
<.table_default_row hover={false}>
<.table_default_cell colspan={6} class="text-center py-8 text-base-content/50">
No scheduled tasks found
</.table_default_cell>
</.table_default_row>
<% else %>
<%= for job <- @scheduled_jobs do %>
<.table_default_row
class="cursor-pointer hover:bg-base-200"
phx-click="show_scheduled_job"
phx-value-id={job.id}
>
<.table_default_cell>
<span class="badge badge-sm badge-outline">{job.job_type}</span>
</.table_default_cell>
<.table_default_cell class="font-mono text-xs">
{job.resource_type}/{String.slice(job.resource_id || "", 0, 8)}...
</.table_default_cell>
<.table_default_cell>
<span class={"badge badge-sm #{scheduled_job_badge_class(job.status)}"}>
{job.status}
</span>
</.table_default_cell>
<.table_default_cell class="text-xs text-base-content/70">
{format_datetime(job.scheduled_at)}
</.table_default_cell>
<.table_default_cell class="text-xs text-base-content/70">
{format_datetime(job.executed_at)}
</.table_default_cell>
<.table_default_cell
class="text-xs max-w-xs truncate"
title={format_json(job.args)}
>
{if job.args && job.args != %{},
do: Map.get(job.args, "post_title") || "...",
else: "-"}
</.table_default_cell>
</.table_default_row>
<% end %>
<% end %>
</.table_default_body>
</.table_default>
</div>
</div>
</div>
<%!-- Job Detail Modal --%>
<%= if @selected_job do %>
<div class="modal modal-open" phx-window-keydown="close_job" phx-key="Escape">
<div class="modal-box max-w-4xl max-h-[90vh] overflow-y-auto">
<button
type="button"
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
phx-click="close_job"
>
✕
</button>
<h3 class="font-bold text-lg flex items-center gap-2 pr-8">
<span>{short_worker_name(@selected_job.worker)}</span>
<span class={"badge badge-sm #{state_badge_class(@selected_job.state)}"}>
{@selected_job.state}
</span>
</h3>
<p class="text-xs text-base-content/50 mb-4">{@selected_job.worker}</p>
<%!-- Basic Info Grid --%>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div>
<div class="text-xs text-base-content/50">ID</div>
<div class="font-mono text-sm">{@selected_job.id}</div>
</div>
<div>
<div class="text-xs text-base-content/50">Queue</div>
<div class="text-sm">{@selected_job.queue}</div>
</div>
<div>
<div class="text-xs text-base-content/50">Attempts</div>
<div class="text-sm">{@selected_job.attempt}/{@selected_job.max_attempts}</div>
</div>
<div>
<div class="text-xs text-base-content/50">Priority</div>
<div class="text-sm">{@selected_job.priority}</div>
</div>
</div>
<%!-- Timestamps --%>
<div class="mb-6">
<h4 class="font-semibold text-sm mb-2">Timestamps</h4>
<div class="grid grid-cols-2 md:grid-cols-3 gap-3 text-sm">
<div>
<span class="text-base-content/50">Inserted:</span>
<span class="font-mono text-xs ml-1">
{format_datetime(@selected_job.inserted_at)}
</span>
</div>
<div>
<span class="text-base-content/50">Scheduled:</span>
<span class="font-mono text-xs ml-1">
{format_datetime(@selected_job.scheduled_at)}
</span>
</div>
<div>
<span class="text-base-content/50">Attempted:</span>
<span class="font-mono text-xs ml-1">
{format_datetime(@selected_job.attempted_at)}
</span>
</div>
<div>
<span class="text-base-content/50">Completed:</span>
<span class="font-mono text-xs ml-1">
{format_datetime(@selected_job.completed_at)}
</span>
</div>
<%= if @selected_job.discarded_at do %>
<div>
<span class="text-error">Discarded:</span>
<span class="font-mono text-xs ml-1">
{format_datetime(@selected_job.discarded_at)}
</span>
</div>
<% end %>
<%= if @selected_job.cancelled_at do %>
<div>
<span class="text-warning">Cancelled:</span>
<span class="font-mono text-xs ml-1">
{format_datetime(@selected_job.cancelled_at)}
</span>
</div>
<% end %>
</div>
</div>
<%!-- Tags --%>
<%= if @selected_job.tags && @selected_job.tags != [] do %>
<div class="mb-6">
<h4 class="font-semibold text-sm mb-2">Tags</h4>
<div class="flex flex-wrap gap-1">
<%= for tag <- @selected_job.tags do %>
<span class="badge badge-sm badge-outline">{tag}</span>
<% end %>
</div>
</div>
<% end %>
<%!-- Args --%>
<div class="collapse collapse-arrow bg-base-200 mb-2">
<input type="checkbox" checked />
<div class="collapse-title font-semibold text-sm">
Arguments
</div>
<div class="collapse-content">
<pre class="text-xs bg-base-300 p-3 rounded overflow-x-auto"><code>{format_json(@selected_job.args)}</code></pre>
</div>
</div>
<%!-- Meta --%>
<%= if @selected_job.meta && @selected_job.meta != %{} do %>
<div class="collapse collapse-arrow bg-base-200 mb-2">
<input type="checkbox" />
<div class="collapse-title font-semibold text-sm">
Metadata
</div>
<div class="collapse-content">
<pre class="text-xs bg-base-300 p-3 rounded overflow-x-auto"><code>{format_json(@selected_job.meta)}</code></pre>
</div>
</div>
<% end %>
<%!-- Errors --%>
<%= if @selected_job.errors && @selected_job.errors != [] do %>
<div class="collapse collapse-arrow bg-error/10 mb-2">
<input type="checkbox" checked />
<div class="collapse-title font-semibold text-sm text-error">
Errors ({length(@selected_job.errors)})
</div>
<div class="collapse-content">
<%= for {error, idx} <- Enum.with_index(@selected_job.errors) do %>
<div class="mb-4 last:mb-0">
<div class="text-xs text-base-content/50 mb-1">Attempt {idx + 1}</div>
<pre class="text-xs bg-base-300 p-3 rounded overflow-x-auto whitespace-pre-wrap"><code>{format_json(error)}</code></pre>
</div>
<% end %>
</div>
</div>
<% end %>
<div class="modal-action">
<button type="button" class="btn" phx-click="close_job">Close</button>
</div>
</div>
<div class="modal-backdrop" phx-click="close_job"></div>
</div>
<% end %>
<%!-- Scheduled Job Detail Modal --%>
<%= if @selected_scheduled_job do %>
<div class="modal modal-open" phx-window-keydown="close_scheduled_job" phx-key="Escape">
<div class="modal-box max-w-3xl max-h-[90vh] overflow-y-auto">
<button
type="button"
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
phx-click="close_scheduled_job"
>
✕
</button>
<h3 class="font-bold text-lg flex items-center gap-2 pr-8">
<span>{@selected_scheduled_job.job_type}</span>
<span class={"badge badge-sm #{scheduled_job_badge_class(@selected_scheduled_job.status)}"}>
{@selected_scheduled_job.status}
</span>
</h3>
<p class="text-xs text-base-content/50 mb-4">
{@selected_scheduled_job.resource_type}/{@selected_scheduled_job.resource_id}
</p>
<%!-- Basic Info Grid --%>
<div class="grid grid-cols-2 md:grid-cols-3 gap-4 mb-6">
<div>
<div class="text-xs text-base-content/50">Handler</div>
<div class="text-sm font-mono text-xs">
{@selected_scheduled_job.handler_module}
</div>
</div>
<div>
<div class="text-xs text-base-content/50">Attempts</div>
<div class="text-sm">
{@selected_scheduled_job.attempts}/{@selected_scheduled_job.max_attempts}
</div>
</div>
<div>
<div class="text-xs text-base-content/50">Priority</div>
<div class="text-sm">{@selected_scheduled_job.priority}</div>
</div>
</div>
<%!-- Timestamps --%>
<div class="mb-6">
<h4 class="font-semibold text-sm mb-2">Timestamps</h4>
<div class="grid grid-cols-2 gap-3 text-sm">
<div>
<span class="text-base-content/50">Created:</span>
<span class="font-mono text-xs ml-1">
{format_datetime(@selected_scheduled_job.inserted_at)}
</span>
</div>
<div>
<span class="text-base-content/50">Scheduled For:</span>
<span class="font-mono text-xs ml-1">
{format_datetime(@selected_scheduled_job.scheduled_at)}
</span>
</div>
<%= if @selected_scheduled_job.executed_at do %>
<div>
<span class="text-success">Executed:</span>
<span class="font-mono text-xs ml-1">
{format_datetime(@selected_scheduled_job.executed_at)}
</span>
</div>
<% end %>
</div>
</div>
<%!-- Args --%>
<div class="collapse collapse-arrow bg-base-200 mb-2">
<input type="checkbox" checked />
<div class="collapse-title font-semibold text-sm">
Arguments
</div>
<div class="collapse-content">
<pre class="text-xs bg-base-300 p-3 rounded overflow-x-auto"><code>{format_json(@selected_scheduled_job.args)}</code></pre>
</div>
</div>
<%!-- Error --%>
<%= if @selected_scheduled_job.last_error do %>
<div class="collapse collapse-arrow bg-error/10 mb-2">
<input type="checkbox" checked />
<div class="collapse-title font-semibold text-sm text-error">
Last Error
</div>
<div class="collapse-content">
<pre class="text-xs bg-base-300 p-3 rounded overflow-x-auto whitespace-pre-wrap"><code>{@selected_scheduled_job.last_error}</code></pre>
</div>
</div>
<% end %>
<div class="modal-action">
<button type="button" class="btn" phx-click="close_scheduled_job">Close</button>
</div>
</div>
<div class="modal-backdrop" phx-click="close_scheduled_job"></div>
</div>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>