Packages

phoenix_kit

1.7.76
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
phoenix_kit lib modules emails web queue.html.heex
Raw

lib/modules/emails/web/queue.html.heex

<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={gettext("Email Queue")}
current_path={@url_path}
project_title={@project_title}
>
<div class="container flex-col mx-auto px-4 py-6">
<.admin_page_header
back={Routes.path("/admin/emails")}
title={gettext("Email Queue")}
subtitle={gettext("Real-time monitoring and queue management")}
/>
<%!-- Action Buttons --%>
<div class="flex justify-between items-center gap-2 mb-6">
<div class="text-sm text-base-content/70">
{gettext("Last updated:")} {UtilsDate.format_time_with_user_format(@last_updated)}
</div>
<div class="flex gap-2">
<.button phx-click="refresh" class="btn btn-outline btn-sm">
<.icon name="hero-arrow-path" class="w-4 h-4 mr-1" /> {gettext("Refresh")}
</.button>
</div>
</div>
<%!-- System Status Cards --%>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={
if @system_status[:system_enabled], do: gettext("Online"), else: gettext("Offline")
}
title={gettext("System Status")}
subtitle={
if @system_status[:system_enabled],
do: gettext("Email system is operational"),
else: gettext("Email system is disabled")
}
>
<:icon>
<.icon name="hero-server" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={UtilsNumber.format(@system_status[:total_sent_today] || 0)}
title={gettext("Sent Today")}
subtitle={gettext("Total emails sent since midnight")}
>
<:icon>
<.icon name="hero-envelope" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={length(@failed_emails)}
title={gettext("Failed (24h)")}
subtitle={gettext("Failed emails in last 24 hours")}
>
<:icon>
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
<PhoenixKitWeb.Components.Core.StatCard.stat_card
compact={true}
rounded="xl"
value={@system_status[:retention_days] || 90}
title={gettext("Retention")}
subtitle={gettext("Days of data retention")}
>
<:icon>
<.icon name="hero-clock" class="w-5 h-5" />
</:icon>
</PhoenixKitWeb.Components.Core.StatCard.stat_card>
</div>
<%!-- Rate Limit Status --%>
<div class="card bg-base-100 shadow-sm mb-6">
<div class="card-body">
<h2 class="card-title text-lg mb-4">{gettext("Rate Limit Status")}</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<%!-- Global Rate Limit --%>
<div class="stat bg-base-200 rounded-lg min-w-0 overflow-hidden">
<div class="stat-title">{gettext("Global Hourly Limit")}</div>
<div class="stat-value text-sm break-all">
{UtilsNumber.format(@rate_limit_status[:global][:count] || 0)} / {UtilsNumber.format(
@rate_limit_status[:global][:limit] || 0
)}
</div>
<div class="stat-desc">
<div class="mt-2">
<progress
class={[
"progress w-full",
((@rate_limit_status[:global][:percentage] || 0) > 80 && "progress-error") ||
((@rate_limit_status[:global][:percentage] || 0) > 60 && "progress-warning") ||
"progress-success"
]}
value={@rate_limit_status[:global][:percentage] || 0}
max="100"
>
</progress>
<span class="text-xs">
{gettext("%{pct}% used", pct: @rate_limit_status[:global][:percentage] || 0)}
</span>
</div>
</div>
</div>
<%!-- Recipients --%>
<div class="stat bg-base-200 rounded-lg min-w-0 overflow-hidden">
<div class="stat-title">{gettext("Recipient Limits")}</div>
<div class="stat-value text-sm">
{@rate_limit_status[:recipients][:active_limits] || 0}
</div>
<div class="stat-desc">
{gettext("Active recipient rate limits (%{count} emails)",
count: @rate_limit_status[:recipients][:total_emails] || 0
)}
</div>
</div>
<%!-- Senders --%>
<div class="stat bg-base-200 rounded-lg min-w-0 overflow-hidden">
<div class="stat-title">{gettext("Sender Limits")}</div>
<div class="stat-value text-sm">
{@rate_limit_status[:senders][:active_limits] || 0}
</div>
<div class="stat-desc">
{gettext("Active sender rate limits (%{count} emails)",
count: @rate_limit_status[:senders][:total_emails] || 0
)}
</div>
</div>
<%!-- Blocklist --%>
<div class="stat bg-base-200 rounded-lg min-w-0 overflow-hidden">
<div class="stat-title">{gettext("Blocklist")}</div>
<div class="stat-value text-sm">
{@rate_limit_status[:blocklist][:active_blocks] || 0}
</div>
<div class="stat-desc">
{gettext("Active blocks (%{count} expired today)",
count: @rate_limit_status[:blocklist][:expired_today] || 0
)}
</div>
</div>
</div>
</div>
</div>
<%!-- Failed Emails Section --%>
<%= if length(@failed_emails) > 0 do %>
<div class="card bg-base-100 shadow-sm mb-6">
<div class="card-body">
<div class="flex justify-between items-center mb-4">
<h2 class="card-title text-lg">{gettext("Failed Emails (Last 24 Hours)")}</h2>
<%= if length(@selected_emails) > 0 do %>
<div class="flex gap-2">
<span class="badge badge-primary">
{gettext("%{count} selected", count: length(@selected_emails))}
</span>
<button phx-click="clear_selection" class="btn btn-xs btn-ghost">
{gettext("Clear")}
</button>
<button
phx-click="set_bulk_action"
phx-value-action="retry"
class="btn btn-xs btn-success"
>
{gettext("Retry Selected")}
</button>
</div>
<% else %>
<button phx-click="select_all_failed" class="btn btn-xs btn-outline">
{gettext("Select All")}
</button>
<% end %>
</div>
<%= if @bulk_action do %>
<div class="alert alert-warning mb-4">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<p class="font-semibold">
{gettext("Confirm bulk action: %{action}", action: String.upcase(@bulk_action))}
</p>
<p class="text-sm">
{gettext("This will %{action} %{count} emails",
action: @bulk_action,
count: length(@selected_emails)
)}
</p>
</div>
<div class="flex gap-2">
<button phx-click="execute_bulk_action" class="btn btn-sm btn-warning">
{gettext("Confirm")}
</button>
<button phx-click="clear_selection" class="btn btn-sm btn-ghost">
{gettext("Cancel")}
</button>
</div>
</div>
<% end %>
<.table_default
id="failed-emails-table"
size="sm"
toggleable={true}
items={@failed_emails}
card_title={fn e -> e.to end}
card_fields={
fn e ->
[
%{label: gettext("Subject"), value: e.subject || "—"},
%{
label: gettext("Error"),
value: e.error_message || gettext("Unknown error")
},
%{
label: gettext("Time"),
value: UtilsDate.format_datetime_with_user_format(e.sent_at)
}
]
end
}
>
<:card_actions :let={email}>
<button
phx-click="retry_email"
phx-value-email_uuid={email.uuid}
class="btn btn-xs btn-outline btn-success"
>
<.icon name="hero-arrow-path" class="w-3 h-3 mr-1" />{gettext("Retry")}
</button>
</:card_actions>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>
<input type="checkbox" class="checkbox checkbox-sm" disabled />
</.table_default_header_cell>
<.table_default_header_cell>{gettext("Recipient")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Subject")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Error")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Time")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Actions")}</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= for email <- @failed_emails do %>
<.table_default_row>
<.table_default_cell>
<input
type="checkbox"
class="checkbox checkbox-sm"
checked={email.uuid in @selected_emails}
phx-click="toggle_email_selection"
phx-value-email_uuid={email.uuid}
/>
</.table_default_cell>
<.table_default_cell class="font-mono text-xs">
{email.to}
</.table_default_cell>
<.table_default_cell class="truncate max-w-xs">
{email.subject}
</.table_default_cell>
<.table_default_cell class="text-error text-xs truncate max-w-xs">
{email.error_message || gettext("Unknown error")}
</.table_default_cell>
<.table_default_cell class="text-xs">
{UtilsDate.format_datetime_with_user_format(email.sent_at)}
</.table_default_cell>
<.table_default_cell>
<button
phx-click="retry_email"
phx-value-email_uuid={email.uuid}
class="btn btn-xs btn-outline btn-success tooltip tooltip-bottom"
data-tip={gettext("Retry")}
>
<.icon name="hero-arrow-path" class="w-3 h-3 hidden sm:inline" />
<span class="sm:hidden whitespace-nowrap">{gettext("Retry")}</span>
</button>
</.table_default_cell>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
</div>
</div>
<% end %>
<%!-- Recent Activity Section --%>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<h2 class="card-title text-lg mb-4">{gettext("Recent Activity (Last 20 Emails)")}</h2>
<%= if @loading do %>
<div class="flex justify-center items-center h-32">
<span class="loading loading-spinner loading-md"></span>
<span class="ml-2">{gettext("Loading activity...")}</span>
</div>
<% else %>
<.table_default
id="recent-activity-table"
size="sm"
toggleable={true}
items={@recent_activity}
card_title={fn email -> email.to end}
card_fields={
fn email ->
events =
[
email.delivered_at && gettext("Delivered"),
email.opened_at && gettext("Opened"),
email.clicked_at && gettext("Clicked")
]
|> Enum.filter(& &1)
|> Enum.join(", ")
[
%{
label: gettext("Status"),
value: String.capitalize(email.status || "unknown")
},
%{label: gettext("Subject"), value: email.subject || "—"},
%{
label: gettext("Sent At"),
value: UtilsDate.format_datetime_with_user_format(email.sent_at)
},
%{label: gettext("Events"), value: if(events == "", do: "—", else: events)}
]
end
}
>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>{gettext("Status")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Recipient")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Subject")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Sent At")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Events")}</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= for email <- @recent_activity do %>
<.table_default_row>
<.table_default_cell>
<div class={[
"badge badge-sm h-auto",
(email.status == "sent" && "badge-success") ||
(email.status == "failed" &&
"badge-error") || "badge-warning"
]}>
{String.capitalize(email.status || "unknown")}
</div>
</.table_default_cell>
<.table_default_cell class="font-mono text-xs">
{email.to}
</.table_default_cell>
<.table_default_cell class="truncate max-w-xs">
{email.subject}
</.table_default_cell>
<.table_default_cell class="text-xs">
{UtilsDate.format_datetime_with_user_format(email.sent_at)}
</.table_default_cell>
<.table_default_cell>
<div class="flex gap-1">
<%= if email.delivered_at do %>
<div class="badge badge-xs badge-success" title={gettext("Delivered")}>
<.icon name="hero-check" class="w-3 h-3" />
</div>
<% end %>
<%= if email.opened_at do %>
<div class="badge badge-xs badge-info" title={gettext("Opened")}>
<.icon name="hero-envelope-open" class="w-3 h-3" />
</div>
<% end %>
<%= if email.clicked_at do %>
<div class="badge badge-xs badge-secondary" title={gettext("Clicked")}>
<.icon name="hero-cursor-arrow-rays" class="w-3 h-3" />
</div>
<% end %>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
<%= if length(@recent_activity) == 0 do %>
<.table_default_row>
<.table_default_cell colspan={5} class="text-center py-8 text-base-content/60">
{gettext("No recent email activity")}
</.table_default_cell>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
<% end %>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>