Packages
phoenix_kit
1.7.34
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/settings.html.heex
<PhoenixKitWeb.Components.LayoutWrapper.app_layout
flash={@flash}
phoenix_kit_current_scope={assigns[:phoenix_kit_current_scope]}
page_title="Storage Settings"
current_path={@current_path}
current_locale={@current_locale}
project_title={@project_title}
>
<div class="container mx-auto px-4 py-8 max-w-6xl">
<%!-- Header Section --%>
<header class="w-full relative mb-6">
<%!-- Back Button (Left aligned) --%>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/modules")}
class="btn btn-outline mb-4"
>
<.icon name="hero-arrow-left" class="w-4 h-4 mr-2" /> Back to Modules
</.link>
<%!-- Title Section --%>
<div class="text-center mb-8">
<h1 class="text-4xl font-bold text-base-content mb-2">
Storage Settings
</h1>
<p class="text-base-content/70">
Configure storage buckets and file processing settings for your application
</p>
</div>
</header>
<%!-- Module Status Alert --%>
<div class="alert alert-info mb-6">
<.icon name="hero-information-circle" class="w-5 h-5" />
<div>
<h3 class="font-bold">Core System Module</h3>
<p class="text-sm">
Storage is a core system module that is always enabled and cannot be disabled.
It provides essential file management functionality across PhoenixKit.
</p>
</div>
</div>
<%!-- Repair Storage Module Section --%>
<div class="alert alert-warning mb-6">
<.icon name="hero-wrench-screwdriver" class="w-5 h-5" />
<div class="flex-1">
<h3 class="font-bold">Storage Module Repair</h3>
<p class="text-sm">
If your storage module isn't working correctly, use this to reset configuration to defaults.
This will NOT delete any uploaded files.
</p>
</div>
<button
phx-click="repair_storage_module"
data-confirm="This will reset dimensions and settings to defaults, and create a local bucket if none exist. Your uploaded files will NOT be deleted. Continue?"
class="btn btn-warning btn-sm"
>
<.icon name="hero-wrench-screwdriver" class="w-4 h-4 mr-1" /> Repair Storage Module
</button>
</div>
<%!-- System Dependencies Check --%>
<%= if match?({:error, :not_installed}, @imagemagick_status) do %>
<div class="alert alert-error mb-6">
<.icon name="hero-exclamation-circle" class="w-5 h-5" />
<div>
<h3 class="font-bold">ImageMagick Not Installed</h3>
<p class="text-sm">
ImageMagick is required for image processing and variant generation.
Without it, image resizing, cropping, and format conversion will not work.
</p>
<p class="text-sm mt-2">
<strong>Installation:</strong>
<br />macOS:
<code class="bg-base-200 px-2 py-1 rounded text-xs">brew install imagemagick</code>
<br />Ubuntu/Debian:
<code class="bg-base-200 px-2 py-1 rounded text-xs">
sudo apt-get install imagemagick
</code>
<br />CentOS/RHEL:
<code class="bg-base-200 px-2 py-1 rounded text-xs">
sudo yum install ImageMagick
</code>
</p>
</div>
</div>
<% end %>
<%= if match?({:error, :not_installed}, @ffmpeg_status) do %>
<div class="alert alert-warning mb-6">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<h3 class="font-bold">FFmpeg Not Installed</h3>
<p class="text-sm">
FFmpeg is required for video processing, transcoding, and thumbnail generation.
Without it, video file processing will not work.
</p>
<p class="text-sm mt-2">
<strong>Installation:</strong>
<br />macOS:
<code class="bg-base-200 px-2 py-1 rounded text-xs">brew install ffmpeg</code>
<br />Ubuntu/Debian:
<code class="bg-base-200 px-2 py-1 rounded text-xs">sudo apt-get install ffmpeg</code>
<br />CentOS/RHEL:
<code class="bg-base-200 px-2 py-1 rounded text-xs">sudo yum install ffmpeg</code>
</p>
</div>
</div>
<% end %>
<%!-- Storage Buckets --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<div class="flex justify-between items-center mb-4">
<h2 class="card-title text-2xl">
<.icon name="hero-inbox-stack" class="w-6 h-6 mr-2" /> Storage Buckets
</h2>
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/settings/media/buckets/new")}
class="btn btn-primary"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> Add Bucket
</.link>
</div>
<%= if Enum.empty?(@buckets) do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<div>
<h3 class="font-bold">No Storage Buckets Configured</h3>
<p class="text-sm">
You need to create at least one storage bucket before files can be uploaded.
Buckets can be local directories or cloud storage providers (AWS S3, Backblaze B2, Cloudflare R2).
</p>
<div class="mt-3">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/settings/media/buckets/new")}
class="btn btn-warning btn-sm"
>
<.icon name="hero-plus" class="w-4 h-4 mr-1" /> Create Your First Bucket
</.link>
</div>
</div>
</div>
<% else %>
<div class="overflow-x-auto">
<table class="table table-zebra">
<thead>
<tr>
<th>Name</th>
<th>Provider</th>
<th>Priority</th>
<th>Files</th>
<th>Status</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<%= for bucket <- @buckets do %>
<tr>
<%!-- Name --%>
<td>
<div class="flex items-center gap-2">
<%= case bucket.provider do %>
<% "local" -> %>
<.icon name="hero-folder" class="w-5 h-5 text-primary" />
<% "s3" -> %>
<.icon name="hero-cloud" class="w-5 h-5 text-warning" />
<% "b2" -> %>
<.icon name="hero-cube" class="w-5 h-5 text-info" />
<% "r2" -> %>
<.icon name="hero-cloud-arrow-up" class="w-5 h-5 text-accent" />
<% _ -> %>
<.icon name="hero-server" class="w-5 h-5 text-base-content/50" />
<% end %>
<div>
<div class="font-bold">{bucket.name}</div>
<div class="text-xs text-base-content/60 font-mono">
{get_bucket_full_path(bucket)}
</div>
</div>
</div>
</td>
<%!-- Provider --%>
<td>
<span class="badge badge-ghost badge-sm">
{String.upcase(bucket.provider)}
</span>
</td>
<%!-- Priority --%>
<td>
<%= if bucket.priority == 0 do %>
<span class="text-xs text-base-content/60">Random (0)</span>
<% else %>
<span class="font-semibold">{bucket.priority}</span>
<% end %>
</td>
<%!-- Files Count --%>
<td>
<span class="text-sm font-semibold">
{Map.get(@bucket_file_counts, bucket.id, 0)} files
</span>
</td>
<%!-- Status --%>
<td>
<%= if bucket.enabled do %>
<span class="badge badge-success badge-sm">Enabled</span>
<% else %>
<span class="badge badge-error badge-sm">Disabled</span>
<% end %>
</td>
<%!-- Actions --%>
<td>
<div class="flex justify-end gap-1">
<%!-- Edit Button --%>
<.link
navigate={
PhoenixKit.Utils.Routes.path(
"/admin/settings/media/buckets/#{bucket.id}/edit"
)
}
class="btn btn-xs btn-ghost"
title="Edit bucket"
>
<.icon name="hero-pencil" class="w-4 h-4" />
</.link>
<%!-- Toggle Enable/Disable --%>
<button
phx-click="toggle_bucket"
phx-value-id={bucket.id}
class="btn btn-xs btn-ghost"
title={if bucket.enabled, do: "Disable bucket", else: "Enable bucket"}
>
<%= if bucket.enabled do %>
<.icon name="hero-eye-slash" class="w-4 h-4" />
<% else %>
<.icon name="hero-eye" class="w-4 h-4" />
<% end %>
</button>
<%!-- Delete Button --%>
<button
phx-click="delete_bucket"
phx-value-id={bucket.id}
data-confirm="Are you sure you want to delete this bucket? This will remove all location records."
class="btn btn-xs btn-ghost text-error"
title="Delete bucket"
>
<.icon name="hero-trash" class="w-4 h-4" />
</button>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<div class="stats stats-vertical lg:stats-horizontal shadow mt-6">
<div class="stat">
<div class="stat-title">Total Buckets</div>
<div class="stat-value text-2xl">{length(@buckets)}</div>
<div class="stat-desc">Configured storage locations</div>
</div>
<div class="stat">
<div class="stat-title">Active Buckets</div>
<div class="stat-value text-2xl text-success">
{Enum.count(@buckets, & &1.enabled)}
</div>
<div class="stat-desc">Currently in use</div>
</div>
<div class="stat">
<div class="stat-title">Default Bucket</div>
<div class="stat-value text-2xl">
<%= if @default_bucket_id do %>
<span class="text-success">Set</span>
<% else %>
<span class="text-warning">None</span>
<% end %>
</div>
<div class="stat-desc">Primary storage location</div>
</div>
</div>
<% end %>
</div>
</div>
<%!-- Storage Configuration --%>
<div class="card bg-base-100 shadow-xl mb-6">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
<.icon name="hero-cog-6-tooth" class="w-6 h-6 mr-2" /> Storage Configuration
</h2>
<.form
for={%{"form_redundancy" => @form_redundancy}}
phx-change="update_storage_form"
class="grid grid-cols-1 md:grid-cols-2 gap-6"
>
<%!-- Redundancy Settings --%>
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Redundancy Copies</span>
<span class="label-text-alt">Number of copies to store</span>
</label>
<%= if @active_buckets_count == 0 do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-4 h-4" />
<span class="text-sm">
No active buckets available. Enable buckets to configure redundancy.
</span>
</div>
<% else %>
<%= if @form_redundancy > @max_redundancy do %>
<div class="alert alert-warning">
<.icon name="hero-exclamation-triangle" class="w-4 h-4" />
<span class="text-sm">
Current setting (#{@form_redundancy} copies) exceeds available buckets (#{@max_redundancy}).
Reduce redundancy or enable more buckets.
</span>
</div>
<% end %>
<input
type="number"
name="form_redundancy"
value={@form_redundancy}
min="1"
max={@max_redundancy}
class={"input input-bordered #{if @form_redundancy > @max_redundancy, do: "input-warning", else: ""}"}
/>
<label class="label">
<span class="label-text-alt">
Enter number of copies (1-#{@max_redundancy}).
<%= if @max_redundancy == 1 do %>
Only 1 active bucket available. Add more buckets to enable redundancy.
<% else %>
Maximum #{@max_redundancy} copies based on #{@active_buckets_count} active bucket(s).
<% end %>
</span>
</label>
<% end %>
</div>
<%!-- Auto-Variant Generation --%>
<div class="form-control">
<label class="label cursor-pointer">
<input
type="checkbox"
name="form_auto_generate_variants"
checked={@form_auto_generate_variants}
phx-click="toggle_form_variants"
value="true"
class="checkbox checkbox-primary"
/>
<span class="label-text ml-2">
<span class="font-semibold">Auto-Generate Variants</span>
<span class="block text-xs opacity-70">
Automatically resize images and videos to configured dimensions
</span>
</span>
</label>
</div>
</.form>
<%!-- Apply Button --%>
<div class="form-control mt-4">
<button
type="button"
phx-click="apply_storage_settings"
disabled={
@form_redundancy == @redundancy_copies and
@form_auto_generate_variants == @auto_generate_variants
}
phx-disable-with="Applying..."
class="btn btn-primary"
>
<.icon name="hero-check" class="w-4 h-4 mr-2" /> Apply Changes
</button>
<span class="label-text-alt text-xs ml-2">
<%= if @form_redundancy == @redundancy_copies and @form_auto_generate_variants == @auto_generate_variants do %>
No changes to apply
<% else %>
Changes will be saved immediately
<% end %>
</span>
</div>
<%!-- Quick Actions --%>
<div class="divider">Quick Actions</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<.link
navigate={PhoenixKit.Utils.Routes.path("/admin/settings/media/dimensions")}
class="btn btn-outline"
>
<.icon name="hero-photo" class="w-4 h-4 mr-2" /> Manage Dimensions
</.link>
<button class="btn btn-outline" disabled>
<.icon name="hero-chart-bar" class="w-4 h-4 mr-2" /> Storage Stats
<span class="badge badge-xs ml-1">Soon</span>
</button>
<button class="btn btn-outline" disabled>
<.icon name="hero-arrow-path" class="w-4 h-4 mr-2" /> Sync Files
<span class="badge badge-xs ml-1">Soon</span>
</button>
</div>
</div>
</div>
<%!-- Coming Soon Features --%>
<div class="card bg-base-100 shadow-xl border-2 border-dashed border-base-300">
<div class="card-body">
<h2 class="card-title text-2xl mb-4">
<.icon name="hero-rocket-launch" class="w-6 h-6 mr-2" /> Advanced Features Coming Soon
</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<%!-- Multi-Location Storage --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">🗂️</div>
<div>
<h3 class="font-bold mb-1">Multi-Location Storage</h3>
<p class="text-sm text-base-content/70">
Store files across 1-5 redundant locations with automatic failover
</p>
</div>
</div>
</div>
<%!-- S3 Integration --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">☁️</div>
<div>
<h3 class="font-bold mb-1">AWS S3 Integration</h3>
<p class="text-sm text-base-content/70">
Store files in Amazon S3 buckets for cloud redundancy
</p>
</div>
</div>
</div>
<%!-- Remote Servers --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">🖥️</div>
<div>
<h3 class="font-bold mb-1">Remote Servers</h3>
<p class="text-sm text-base-content/70">
Distribute files across multiple remote servers and datacenters
</p>
</div>
</div>
</div>
<%!-- Admin Dashboard --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">📊</div>
<div>
<h3 class="font-bold mb-1">Admin Dashboard</h3>
<p class="text-sm text-base-content/70">
Monitor storage health, locations, and file distribution
</p>
</div>
</div>
</div>
<%!-- File Tracking --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">📋</div>
<div>
<h3 class="font-bold mb-1">File Tracking</h3>
<p class="text-sm text-base-content/70">
PostgreSQL-based file registry with location tracking
</p>
</div>
</div>
</div>
<%!-- Automatic Failover --%>
<div class="border border-base-300 rounded-lg p-4">
<div class="flex items-start gap-3">
<div class="text-3xl">🔄</div>
<div>
<h3 class="font-bold mb-1">Automatic Failover</h3>
<p class="text-sm text-base-content/70">
Seamless file retrieval even when storage locations fail
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<%!-- Upload Test Section --%>
<%!-- Help Section --%>
<div class="alert mt-6">
<.icon name="hero-light-bulb" class="w-5 h-5" />
<div>
<h3 class="font-bold">Storage System Architecture</h3>
<p class="text-sm">
The storage system is designed for distributed file management with automatic redundancy.
Files are tracked in PostgreSQL with support for multiple storage locations (local, S3, remote servers).
When a file is requested, the system automatically tries each location until it successfully retrieves the file.
</p>
</div>
</div>
</div>
</PhoenixKitWeb.Components.LayoutWrapper.app_layout>
<%!-- Add CSS for drag and drop --%>
<style>
#upload-dropzone.dragover {
@apply border-primary bg-primary/5;
}
</style>
<script>
// Simple drag and drop styling
document.addEventListener('DOMContentLoaded', function() {
const dropzone = document.getElementById('upload-dropzone');
const fileInput = document.getElementById('file-upload');
if (dropzone && fileInput) {
dropzone.addEventListener('dragover', function(e) {
e.preventDefault();
dropzone.classList.add('dragover');
});
dropzone.addEventListener('dragleave', function(e) {
e.preventDefault();
dropzone.classList.remove('dragover');
});
dropzone.addEventListener('drop', function(e) {
e.preventDefault();
dropzone.classList.remove('dragover');
});
}
});
</script>