Current section

Files

Jump to
phoenix_kit_emails lib phoenix_kit modules emails web queue.html.heex
Raw

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

<div class="container flex-col mx-auto px-4 py-6">
<%!-- 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 (always rendered; empty-state is a body row) --%>
<div class="bg-base-100 mb-6">
<%= 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
}
>
<:toolbar_title>
<h2 class="text-lg font-semibold">{gettext("Failed Emails (Last 24 Hours)")}</h2>
</:toolbar_title>
<:toolbar_actions>
<%= if length(@selected_emails) > 0 do %>
<span class="badge badge-primary">
{gettext("%{count} selected", count: length(@selected_emails))}
</span>
<button phx-click="clear_selection" class="btn btn-sm btn-ghost">
{gettext("Clear")}
</button>
<button
phx-click="set_bulk_action"
phx-value-action="retry"
class="btn btn-sm btn-success"
>
{gettext("Retry Selected")}
</button>
<% else %>
<button
:if={length(@failed_emails) > 0}
phx-click="select_all_failed"
class="btn btn-sm btn-outline"
>
{gettext("Select All")}
</button>
<% end %>
<.button phx-click="refresh" class="btn btn-outline btn-sm">
<.icon name="hero-arrow-path" class="w-4 h-4" />
<span class="hidden sm:inline ml-1">{gettext("Refresh")}</span>
</.button>
</:toolbar_actions>
<: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>
<%= if @failed_emails == [] do %>
<.table_default_row hover={false}>
<.table_default_cell colspan={6}>
<div class="text-center py-12">
<.icon
name="hero-check-circle"
class="h-16 w-16 mx-auto text-base-content/40 mb-4"
/>
<h3 class="text-lg font-medium text-base-content mb-2">
{gettext("No failed emails")}
</h3>
<p class="text-base-content/70">
{gettext("No failed emails in the last 24 hours")}
</p>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
<%= for email <- @failed_emails do %>
<.table_default_row
class="cursor-pointer"
phx-click="view_details"
phx-value-uuid={email.uuid}
>
<.table_default_cell>
<%!-- stopPropagation so selecting a row doesn't also trigger
the row's view_details navigation. --%>
<div onclick="event.stopPropagation()">
<input
type="checkbox"
class="checkbox checkbox-sm"
checked={email.uuid in @selected_emails}
phx-click="toggle_email_selection"
phx-value-email_uuid={email.uuid}
/>
</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-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>
<div onclick="event.stopPropagation()">
<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>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
</div>
<%!-- Recent Activity Section --%>
<div class="bg-base-100">
<%= 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
}
>
<:toolbar_title>
<h2 class="text-lg font-semibold">{gettext("Recent Activity (Last 20 Emails)")}</h2>
</:toolbar_title>
<:toolbar_actions>
<span class="hidden sm:inline text-xs text-base-content/60 mr-1">
{gettext("Last updated:")} {UtilsDate.format_time_with_user_format(@last_updated)}
</span>
<.button phx-click="refresh" class="btn btn-outline btn-sm">
<.icon name="hero-arrow-path" class="w-4 h-4" />
<span class="hidden sm:inline ml-1">{gettext("Refresh")}</span>
</.button>
</:toolbar_actions>
<.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>
<%= if @recent_activity == [] do %>
<.table_default_row hover={false}>
<.table_default_cell colspan={5}>
<div class="text-center py-12">
<.icon
name="hero-envelope"
class="h-16 w-16 mx-auto text-base-content/40 mb-4"
/>
<h3 class="text-lg font-medium text-base-content mb-2">
{gettext("No recent email activity")}
</h3>
<p class="text-base-content/70">
{gettext("Recently sent emails will appear here")}
</p>
</div>
</.table_default_cell>
</.table_default_row>
<% end %>
<%= for email <- @recent_activity do %>
<.table_default_row
class="cursor-pointer"
phx-click="view_details"
phx-value-uuid={email.uuid}
>
<.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 %>
</.table_default_body>
</.table_default>
<% end %>
</div>
</div>