Packages
phoenix_kit
1.7.62
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/publishing/web/settings.ex
defmodule PhoenixKit.Modules.Publishing.Web.Settings do
@moduledoc """
Admin configuration for publishing groups.
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
alias PhoenixKit.Modules.Languages
alias PhoenixKit.Modules.Publishing
alias PhoenixKit.Modules.Publishing.DBStorage
alias PhoenixKit.Modules.Publishing.ListingCache
alias PhoenixKit.Modules.Publishing.PubSub, as: PublishingPubSub
alias PhoenixKit.Modules.Publishing.Renderer
alias PhoenixKit.Modules.Publishing.Storage
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Routes
# New settings keys (write to these)
@file_cache_key "publishing_file_cache_enabled"
@memory_cache_key "publishing_memory_cache_enabled"
@render_cache_key "publishing_render_cache_enabled"
# Legacy settings keys (read from these as fallback)
@legacy_file_cache_key "blogging_file_cache_enabled"
@legacy_memory_cache_key "blogging_memory_cache_enabled"
@legacy_render_cache_key "blogging_render_cache_enabled"
def mount(_params, _session, socket) do
# Subscribe to group changes for live updates
if connected?(socket) do
PublishingPubSub.subscribe_to_groups()
end
# Admin side reads from database only — groups appear after import
groups = db_groups_to_maps()
fs_groups = fs_groups_to_maps()
# Cache management uses DB groups if imported, else FS groups (caches serve public pages)
cache_groups = if groups != [], do: groups, else: fs_groups
languages_enabled = Languages.enabled?()
socket =
socket
|> assign(:project_title, Settings.get_project_title())
|> assign(:page_title, gettext("Manage Publishing"))
|> assign(
:current_path,
Routes.path("/admin/settings/publishing")
)
|> assign(:module_enabled, Publishing.enabled?())
|> assign(:publishing, groups)
|> assign(:cache_groups, cache_groups)
|> assign(:languages_enabled, languages_enabled)
|> assign(:global_primary_language, Storage.get_primary_language())
|> assign(:file_cache_enabled, get_cache_setting(@file_cache_key, @legacy_file_cache_key))
|> assign(
:memory_cache_enabled,
get_cache_setting(@memory_cache_key, @legacy_memory_cache_key)
)
|> assign(
:render_cache_enabled,
get_cache_setting(@render_cache_key, @legacy_render_cache_key)
)
|> assign(:cache_status, build_cache_status(cache_groups))
|> assign(:render_cache_stats, get_render_cache_stats())
|> assign(:render_cache_per_group, build_render_cache_per_group(cache_groups))
{:ok, socket}
end
def handle_params(_params, _uri, socket), do: {:noreply, socket}
def handle_event("remove_group", %{"slug" => slug}, socket) do
case Publishing.trash_group(slug) do
{:ok, trashed_name} ->
# The `Publishing.trash_group` call triggers `remove_group`, which handles
# the broadcast. This LiveView will catch the event and update its state.
{:noreply,
put_flash(
socket,
:info,
gettext("Group moved to trash as: %{name}", name: trashed_name)
)}
{:error, :not_found} ->
# Group directory doesn't exist, just remove from config
case Publishing.remove_group(slug) do
{:ok, _} ->
# The `Publishing.remove_group` call handles the broadcast. This
# LiveView will catch the event and update its state.
{:noreply, put_flash(socket, :info, gettext("Group removed from configuration"))}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, gettext("Failed to remove group"))}
end
{:error, _reason} ->
{:noreply, put_flash(socket, :error, gettext("Failed to move group to trash"))}
end
end
def handle_event("regenerate_cache", %{"slug" => slug}, socket) do
case ListingCache.regenerate(slug) do
:ok ->
{:noreply,
socket
|> assign(:cache_status, build_cache_status(socket.assigns.cache_groups))
|> put_flash(:info, gettext("Cache regenerated for %{group}", group: slug))}
{:error, _reason} ->
{:noreply, put_flash(socket, :error, gettext("Failed to regenerate cache"))}
end
end
def handle_event("invalidate_cache", %{"slug" => slug}, socket) do
ListingCache.invalidate(slug)
{:noreply,
socket
|> assign(:cache_status, build_cache_status(socket.assigns.cache_groups))
|> put_flash(:info, gettext("Cache cleared for %{group}", group: slug))}
end
def handle_event("migrate_primary_language", %{"slug" => slug}, socket) do
primary_lang = socket.assigns.global_primary_language
{:ok, count} = DBStorage.migrate_primary_language(slug, primary_lang)
{:noreply,
socket
|> refresh_groups()
|> put_flash(
:info,
gettext("Updated %{count} posts to use primary language: %{lang}",
count: count,
lang: primary_lang
)
)}
end
def handle_event("regenerate_all_caches", _params, socket) do
results =
Enum.map(socket.assigns.cache_groups, fn group ->
{group["slug"], ListingCache.regenerate(group["slug"])}
end)
success_count = Enum.count(results, fn {_, result} -> result == :ok end)
{:noreply,
socket
|> assign(:cache_status, build_cache_status(socket.assigns.cache_groups))
|> put_flash(:info, gettext("Regenerated %{count} caches", count: success_count))}
end
def handle_event("toggle_file_cache", _params, socket) do
new_value = !socket.assigns.file_cache_enabled
Settings.update_setting(@file_cache_key, to_string(new_value))
{:noreply,
socket
|> assign(:file_cache_enabled, new_value)
|> put_flash(:info, cache_toggle_message("File cache", new_value))}
end
def handle_event("toggle_memory_cache", _params, socket) do
new_value = !socket.assigns.memory_cache_enabled
Settings.update_setting(@memory_cache_key, to_string(new_value))
# If disabling memory cache, clear all :persistent_term entries
if !new_value do
Enum.each(socket.assigns.cache_groups, fn group ->
try do
:persistent_term.erase(ListingCache.persistent_term_key(group["slug"]))
rescue
ArgumentError -> :ok
end
end)
end
{:noreply,
socket
|> assign(:memory_cache_enabled, new_value)
|> assign(:cache_status, build_cache_status(socket.assigns.cache_groups))
|> put_flash(:info, cache_toggle_message("Memory cache", new_value))}
end
def handle_event("clear_render_cache", _params, socket) do
Renderer.clear_all_cache()
{:noreply,
socket
|> assign(:render_cache_stats, get_render_cache_stats())
|> put_flash(:info, gettext("Render cache cleared"))}
end
def handle_event("clear_group_render_cache", %{"slug" => slug}, socket) do
case Renderer.clear_group_cache(slug) do
{:ok, count} ->
{:noreply,
socket
|> assign(:render_cache_stats, get_render_cache_stats())
|> put_flash(
:info,
gettext("Cleared %{count} cached posts for %{group}", count: count, group: slug)
)}
{:error, _} ->
{:noreply, put_flash(socket, :error, gettext("Failed to clear cache"))}
end
end
def handle_event("toggle_render_cache", _params, socket) do
new_value = !socket.assigns.render_cache_enabled
Settings.update_setting(@render_cache_key, to_string(new_value))
{:noreply,
socket
|> assign(:render_cache_enabled, new_value)
|> put_flash(:info, cache_toggle_message("Render cache", new_value))}
end
def handle_event("toggle_group_render_cache", %{"slug" => slug}, socket) do
# Use Renderer helper to get the new key for writes
per_group_key = Renderer.per_group_cache_key(slug)
current_value = Renderer.group_render_cache_enabled?(slug)
new_value = !current_value
Settings.update_setting(per_group_key, to_string(new_value))
{:noreply,
socket
|> assign(:render_cache_per_group, build_render_cache_per_group(socket.assigns.cache_groups))
|> put_flash(:info, cache_toggle_message("Render cache for #{slug}", new_value))}
end
# ============================================================================
# PubSub Handlers - Live updates when groups change elsewhere
# ============================================================================
def handle_info({:group_created, _group}, socket) do
{:noreply, refresh_groups(socket)}
end
def handle_info({:group_deleted, _slug}, socket) do
{:noreply, refresh_groups(socket)}
end
def handle_info({:group_updated, _group}, socket) do
{:noreply, refresh_groups(socket)}
end
defp refresh_groups(socket) do
groups = db_groups_to_maps()
fs_groups = fs_groups_to_maps()
cache_groups = if groups != [], do: groups, else: fs_groups
socket
|> assign(:publishing, groups)
|> assign(:cache_groups, cache_groups)
|> assign(:fs_group_count, length(fs_groups))
|> assign(:cache_status, build_cache_status(cache_groups))
|> assign(:render_cache_per_group, build_render_cache_per_group(cache_groups))
end
defp db_groups_to_maps do
global_primary = Storage.get_primary_language()
DBStorage.list_groups()
|> Enum.map(fn g ->
# Check DB records for primary language issues (not filesystem)
primary_lang_status = DBStorage.count_primary_language_status(g.slug, global_primary)
needs_lang_migration =
primary_lang_status.needs_backfill + primary_lang_status.needs_migration > 0
%{
"name" => g.name,
"slug" => g.slug,
"mode" => g.mode,
"position" => g.position,
"needs_primary_lang_migration" => needs_lang_migration,
"primary_language_status" => primary_lang_status
}
end)
end
defp fs_groups_to_maps do
Publishing.list_groups()
|> Enum.map(fn g ->
%{
"name" => g["name"],
"slug" => g["slug"],
"mode" => g["mode"],
"position" => g["position"]
}
end)
end
# Helper for dual-key cache setting reads
defp get_cache_setting(new_key, legacy_key) do
case Settings.get_setting(new_key, nil) do
nil -> Settings.get_setting(legacy_key, "true") == "true"
value -> value == "true"
end
end
defp cache_toggle_message(cache_type, enabled) do
if enabled do
gettext("%{type} enabled", type: cache_type)
else
gettext("%{type} disabled", type: cache_type)
end
end
# Build cache status for all groups
defp build_cache_status(groups) do
Map.new(groups, fn group ->
slug = group["slug"]
{slug, get_cache_info(slug)}
end)
end
defp get_cache_info(group_slug) do
if Publishing.db_storage?() do
get_cache_info_db(group_slug)
else
get_cache_info_fs(group_slug)
end
end
defp get_cache_info_db(group_slug) do
in_memory =
case :persistent_term.get(ListingCache.persistent_term_key(group_slug), :not_found) do
:not_found -> false
_ -> true
end
post_count =
case :persistent_term.get(ListingCache.persistent_term_key(group_slug), :not_found) do
:not_found -> length(DBStorage.list_posts(group_slug))
posts -> length(posts)
end
%{
exists: in_memory,
file_size: 0,
modified_at: nil,
post_count: post_count,
in_memory: in_memory
}
end
defp get_cache_info_fs(group_slug) do
cache_path = ListingCache.cache_path(group_slug)
case File.stat(cache_path) do
{:ok, stat} ->
# Read cache to get post count
post_count =
case File.read(cache_path) do
{:ok, content} ->
case Jason.decode(content) do
{:ok, %{"post_count" => count}} -> count
_ -> nil
end
_ ->
nil
end
# Check if in :persistent_term
in_memory =
case :persistent_term.get(ListingCache.persistent_term_key(group_slug), :not_found) do
:not_found -> false
_ -> true
end
%{
exists: true,
file_size: stat.size,
modified_at: stat.mtime,
post_count: post_count,
in_memory: in_memory
}
{:error, :enoent} ->
%{exists: false, file_size: 0, modified_at: nil, post_count: nil, in_memory: false}
end
end
defp get_render_cache_stats do
PhoenixKit.Cache.stats(:publishing_posts)
rescue
_ -> %{hits: 0, misses: 0, puts: 0, invalidations: 0, hit_rate: 0.0}
end
defp build_render_cache_per_group(groups) do
Map.new(groups, fn group ->
slug = group["slug"]
{slug, Renderer.group_render_cache_enabled?(slug)}
end)
end
end