Packages
phoenix_kit
1.7.99
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/modules/storage/web/health.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title={@page_title}
current_path={@current_path}
current_locale={@current_locale}
project_title={@project_title}
>
<div class="container mx-auto px-4 py-8 max-w-5xl">
<.admin_page_header
back={PhoenixKit.Utils.Routes.path("/admin/settings/media")}
title={@page_title}
subtitle={gettext("Check file redundancy against configured target")}
/>
<%!-- Stats --%>
<div class="stats stats-vertical lg:stats-horizontal shadow w-full mb-6">
<div class="stat">
<div class="stat-figure text-primary">
<.icon name="hero-document-duplicate" class="w-8 h-8" />
</div>
<div class="stat-title">{gettext("Total Files")}</div>
<div class="stat-value text-2xl">{@report.total}</div>
<div class="stat-desc">{gettext("Files with completed variants")}</div>
</div>
<div class="stat">
<div class="stat-figure text-success">
<.icon name="hero-check-circle" class="w-8 h-8" />
</div>
<div class="stat-title">{gettext("Healthy")}</div>
<div class="stat-value text-2xl text-success">{@report.healthy}</div>
<div class="stat-desc">
{gettext("%{count}+ locations each", count: @report.redundancy_target)}
</div>
</div>
<div class="stat">
<div class="stat-figure text-error">
<.icon name="hero-exclamation-triangle" class="w-8 h-8" />
</div>
<div class="stat-title">{gettext("Under-replicated")}</div>
<div class="stat-value text-2xl text-error">{length(@report.under_replicated)}</div>
<div class="stat-desc">{gettext("Below redundancy target")}</div>
</div>
<div class="stat">
<div class={"stat-figure #{if @report.health_percentage == 100.0, do: "text-success", else: "text-warning"}"}>
<.icon name="hero-heart" class="w-8 h-8" />
</div>
<div class="stat-title">{gettext("Health")}</div>
<div class={"stat-value text-2xl #{if @report.health_percentage == 100.0, do: "text-success", else: "text-warning"}"}>
{@report.health_percentage}%
</div>
<div class="stat-desc">
{gettext("Redundancy target: %{n} copies", n: @report.redundancy_target)}
</div>
</div>
</div>
<%!-- Actions --%>
<div class="flex justify-end gap-2 mb-4">
<%= if @report.under_replicated != [] do %>
<button phx-click="sync" class="btn btn-primary btn-sm" disabled={@syncing}>
<%= if @syncing do %>
<span class="loading loading-spinner loading-xs"></span>
{gettext("Syncing...")}
<% else %>
<.icon name="hero-arrow-path-rounded-square" class="w-4 h-4" />
{gettext("Sync All")}
<% end %>
</button>
<% end %>
<button phx-click="refresh" class="btn btn-outline btn-sm">
<.icon name="hero-arrow-path" class="w-4 h-4" /> {gettext("Refresh")}
</button>
</div>
<%!-- Sync Progress --%>
<%= if @syncing do %>
<div class="card bg-base-100 shadow-sm mb-6">
<div class="card-body">
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="text-sm font-semibold">
<%= if @sync_paused do %>
{gettext("Sync paused")}
<% else %>
{gettext("Syncing files...")}
<% end %>
</span>
<%= if @sync_paused do %>
<button phx-click="resume_sync" class="btn btn-xs btn-success">
<.icon name="hero-play" class="w-3 h-3" /> {gettext("Resume")}
</button>
<% else %>
<button phx-click="pause_sync" class="btn btn-xs btn-warning">
<.icon name="hero-pause" class="w-3 h-3" /> {gettext("Pause")}
</button>
<% end %>
<button
phx-click="stop_sync"
data-confirm={gettext("Stop the sync? Files already synced will be kept.")}
class="btn btn-xs btn-error"
>
<.icon name="hero-stop" class="w-3 h-3" /> {gettext("Stop")}
</button>
</div>
<span class="text-sm text-base-content/60">
{@sync_progress} / {@sync_total}
</span>
</div>
<progress
class="progress progress-primary w-full"
value={@sync_progress}
max={@sync_total}
>
</progress>
<div class="flex gap-4 mt-2 text-xs text-base-content/60">
<span class="text-success">{gettext("%{n} synced", n: @sync_synced)}</span>
<%= if @sync_failed > 0 do %>
<span class="text-error">{gettext("%{n} failed", n: @sync_failed)}</span>
<% end %>
</div>
<%!-- Sync Log --%>
<%= if @sync_log != [] do %>
<div class="mt-4 bg-base-200 rounded-lg p-3 max-h-48 overflow-y-auto font-mono text-xs">
<%= for entry <- @sync_log do %>
<div class={[
"flex gap-2 py-0.5",
entry.status == :error && "text-error",
entry.status == :ok && "text-base-content/60"
]}>
<%= if entry.status == :ok do %>
<span>✓</span>
<% else %>
<span>✗</span>
<% end %>
<span class="truncate font-semibold">{entry.file}</span>
<span class="truncate">{entry.message}</span>
</div>
<% end %>
</div>
<% end %>
</div>
</div>
<% end %>
<%!-- Show log after sync completes if there were failures --%>
<%= if !@syncing && @sync_log != [] && @sync_failed > 0 do %>
<div class="card bg-base-100 shadow-sm mb-6">
<div class="card-body">
<h3 class="card-title text-sm">
<.icon name="hero-document-text" class="w-4 h-4" />
{gettext("Sync Log")}
<span class="badge badge-error badge-sm">
{gettext("%{n} errors", n: @sync_failed)}
</span>
</h3>
<div class="bg-base-200 rounded-lg p-3 max-h-64 overflow-y-auto font-mono text-xs">
<%= for entry <- Enum.filter(@sync_log, & &1.status == :error) do %>
<div class="flex gap-2 py-0.5 text-error">
<span>✗</span>
<span class="font-semibold">{entry.file}</span>
<span class="truncate">{entry.message}</span>
</div>
<% end %>
</div>
</div>
</div>
<% end %>
<%!-- Results --%>
<%= if @report.under_replicated == [] do %>
<div class="card bg-base-100 shadow-sm">
<div class="card-body items-center text-center py-12">
<.icon name="hero-check-circle" class="w-16 h-16 text-success mb-4" />
<h3 class="text-xl font-bold text-success">{gettext("All Healthy")}</h3>
<p class="text-base-content/60">
{gettext("All files meet the redundancy target of %{n} copies.",
n: @report.redundancy_target
)}
</p>
</div>
</div>
<% else %>
<.table_default
id="health-table"
variant="zebra"
toggleable={true}
items={@report.under_replicated}
card_title={fn item -> item.original_file_name end}
card_fields={
fn item ->
[
%{label: gettext("Type"), value: item.file_type},
%{
label: gettext("Locations"),
value: "#{item.min_location_count} / #{@report.redundancy_target}"
},
%{
label: gettext("Status"),
value:
if(item.min_location_count == 0,
do: gettext("No copies"),
else:
gettext("Missing %{n} copies",
n: @report.redundancy_target - item.min_location_count
)
)
}
]
end
}
>
<.table_default_header>
<.table_default_row>
<.table_default_header_cell>{gettext("File")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Type")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Locations")}</.table_default_header_cell>
<.table_default_header_cell>{gettext("Status")}</.table_default_header_cell>
</.table_default_row>
</.table_default_header>
<.table_default_body>
<%= for item <- @report.under_replicated do %>
<.table_default_row>
<.table_default_cell>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/media/#{item.file_uuid}")}
class="font-bold link link-hover link-primary"
>
{item.original_file_name}
</.link>
</.table_default_cell>
<.table_default_cell>
<span class="badge badge-ghost badge-sm h-auto">{item.file_type}</span>
</.table_default_cell>
<.table_default_cell>
<span class={"font-semibold #{if item.min_location_count == 0, do: "text-error", else: "text-warning"}"}>
{item.min_location_count}
</span>
<span class="text-base-content/50">/ {@report.redundancy_target}</span>
</.table_default_cell>
<.table_default_cell>
<%= if item.min_location_count == 0 do %>
<span class="badge badge-error badge-sm h-auto">{gettext("No copies")}</span>
<% else %>
<span class="badge badge-warning badge-sm h-auto">
{gettext("Missing %{n}",
n: @report.redundancy_target - item.min_location_count
)}
</span>
<% end %>
</.table_default_cell>
</.table_default_row>
<% end %>
</.table_default_body>
</.table_default>
<% end %>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>