Packages

phoenix_kit

1.7.203
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
phoenix_kit lib modules sitemap web settings.ex
Raw

lib/modules/sitemap/web/settings.ex

defmodule PhoenixKit.Modules.Sitemap.Web.Settings do
@moduledoc """
LiveView for sitemap configuration and management.
Provides admin interface for:
- Enabling/disabling sitemap module
- Per-module sitemap cards with stats and regeneration
- Auth pages configuration (login excluded, registration toggle)
- Publishing split by group toggle
- XSL styling configuration
- Scheduled generation
"""
use PhoenixKitWeb, :live_view
use Gettext, backend: PhoenixKitWeb.Gettext
require Logger
alias PhoenixKit.Modules.Sitemap
alias PhoenixKit.Modules.Sitemap.FileStorage
alias PhoenixKit.Modules.Sitemap.Generator
alias PhoenixKit.Modules.Sitemap.SchedulerWorker
alias PhoenixKit.Modules.Sitemap.Sources.RouterDiscovery
alias PhoenixKit.Modules.Sitemap.Sources.Source
alias PhoenixKit.Modules.Sitemap.Sources.Static
alias PhoenixKit.PubSub.Manager, as: PubSubManager
alias PhoenixKit.Settings
alias PhoenixKit.Utils.Date, as: UtilsDate
alias PhoenixKit.Utils.Json
alias PhoenixKit.Utils.Routes
@impl true
def mount(params, _session, socket) do
if connected?(socket) do
PubSubManager.subscribe("sitemap:updates")
PubSubManager.subscribe("sitemap:settings")
end
locale = params["locale"] || "en"
project_title = Settings.get_project_title()
site_url = Settings.get_setting("site_url", "")
config = Sitemap.get_config()
sitemap_version = get_sitemap_version(config)
module_stats = Sitemap.get_module_stats()
module_files = FileStorage.list_module_files()
socket =
socket
|> assign(:page_title, "Sitemap Settings")
|> assign(:project_title, project_title)
|> assign(:current_locale, locale)
|> assign(:current_path, Routes.path("/admin/settings/sitemap", locale: locale))
|> assign(:config, config)
|> assign(:site_url, site_url)
|> assign(:generating, false)
|> assign(:preview_mode, nil)
|> assign(:preview_content, nil)
|> assign(:show_preview, false)
|> assign(:sitemap_version, sitemap_version)
|> assign(:module_stats, module_stats)
|> assign(:module_files, module_files)
|> assign(:include_registration, Sitemap.include_registration?())
|> assign(:publishing_split_by_group, Sitemap.publishing_split_by_group?())
|> assign(:module_enabled, get_module_enabled_status())
|> assign(:exclude_patterns_text, exclude_patterns_text())
|> assign(:exclude_patterns_error, nil)
|> assign(:protected_pipelines_text, protected_pipelines_text())
|> assign(:protected_pipelines_defaults_text, protected_pipelines_defaults_text())
|> assign(:protected_pipelines_error, nil)
|> assign(:custom_urls_text, custom_urls_text())
|> assign(:custom_urls_error, nil)
|> assign(:static_routes_text, static_routes_text())
|> assign(:static_routes_error, nil)
|> assign(:extension_sources, build_extension_sources(Generator.get_sources()))
{:ok, socket}
end
@impl true
def handle_params(_params, _url, socket) do
{:noreply, socket}
end
@impl true
def handle_event("toggle_sitemap", _params, socket) do
new_enabled = !socket.assigns.config.enabled
result =
if new_enabled do
Sitemap.enable_system()
else
Sitemap.disable_system()
end
case result do
{:ok, _} ->
config = Sitemap.get_config()
message = if new_enabled, do: "Sitemap enabled", else: "Sitemap disabled"
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
broadcast_settings_change(:sitemap_toggled, %{enabled: new_enabled, version: new_version})
{:noreply,
socket
|> assign(:config, config)
|> assign(:sitemap_version, new_version)
|> put_flash(:info, message)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update sitemap status")}
end
end
# Router Discovery toggle = mode switch (flat vs index)
@impl true
def handle_event("toggle_source", %{"source" => "router_discovery"}, socket) do
key = "sitemap_router_discovery_enabled"
current = Settings.get_boolean_setting(key, false)
new_value = !current
case Settings.update_boolean_setting(key, new_value) do
{:ok, _} ->
Generator.invalidate_cache()
# Transitioning to flat mode: clean up per-module files
if new_value, do: FileStorage.delete_all_modules()
config = Sitemap.get_config()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
broadcast_settings_change(:source_changed, %{
source: "router_discovery",
version: new_version
})
module_stats = if new_value, do: [], else: Sitemap.get_module_stats()
module_files = if new_value, do: [], else: FileStorage.list_module_files()
message =
if new_value,
do: "Router Discovery enabled — flat sitemap mode active",
else: "Router Discovery disabled — per-module sitemap mode"
{:noreply,
socket
|> assign(:config, config)
|> assign(:sitemap_version, new_version)
|> assign(:module_stats, module_stats)
|> assign(:module_files, module_files)
|> put_flash(:info, message)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update source setting")}
end
end
@impl true
def handle_event("toggle_source", %{"source" => source}, socket) do
key =
if source == "router_discovery" do
"sitemap_router_discovery_enabled"
else
"sitemap_include_#{source}"
end
current = Settings.get_boolean_setting(key, true)
case Settings.update_boolean_setting(key, !current) do
{:ok, _} ->
Generator.invalidate_cache()
config = Sitemap.get_config()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
broadcast_settings_change(:source_changed, %{source: source, version: new_version})
{:noreply,
socket
|> assign(:config, config)
|> assign(:sitemap_version, new_version)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update source setting")}
end
end
@impl true
def handle_event("toggle_registration", _params, socket) do
new_value = !socket.assigns.include_registration
case Settings.update_boolean_setting("sitemap_include_registration", new_value) do
{:ok, _} ->
Generator.invalidate_cache()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
{:noreply,
socket
|> assign(:include_registration, new_value)
|> assign(:sitemap_version, new_version)
|> put_flash(
:info,
"Registration page #{if new_value, do: "included", else: "excluded"}"
)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update registration setting")}
end
end
@impl true
def handle_event("toggle_publishing_split", _params, socket) do
new_value = !socket.assigns.publishing_split_by_group
case Settings.update_boolean_setting("sitemap_publishing_split_by_group", new_value) do
{:ok, _} ->
Generator.invalidate_cache()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
{:noreply,
socket
|> assign(:publishing_split_by_group, new_value)
|> assign(:sitemap_version, new_version)
|> put_flash(
:info,
"Publishing #{if new_value, do: "split by blog", else: "combined into one file"}"
)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update publishing split setting")}
end
end
@impl true
def handle_event("toggle_html", _params, socket) do
current = socket.assigns.config.html_enabled
case Settings.update_boolean_setting("sitemap_html_enabled", !current) do
{:ok, _} ->
Generator.invalidate_cache()
config = Sitemap.get_config()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
broadcast_settings_change(:html_changed, %{enabled: !current, version: new_version})
{:noreply,
socket
|> assign(:config, config)
|> assign(:sitemap_version, new_version)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update HTML sitemap setting")}
end
end
@impl true
def handle_event("toggle_schedule", _params, socket) do
current = socket.assigns.config.schedule_enabled
case Settings.update_boolean_setting("sitemap_schedule_enabled", !current) do
{:ok, _} ->
if current do
SchedulerWorker.cancel_scheduled()
else
SchedulerWorker.schedule()
end
config = Sitemap.get_config()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
broadcast_settings_change(:schedule_changed, %{enabled: !current, version: new_version})
{:noreply,
socket
|> assign(:config, config)
|> assign(:sitemap_version, new_version)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update schedule setting")}
end
end
@impl true
def handle_event("update_style", %{"style" => style}, socket) do
case Settings.update_setting("sitemap_html_style", style) do
{:ok, _} ->
Generator.invalidate_cache()
config = Sitemap.get_config()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
broadcast_settings_change(:style_changed, %{style: style, version: new_version})
{:noreply,
socket
|> assign(:config, config)
|> assign(:sitemap_version, new_version)
|> put_flash(:info, "Style updated. Sitemap will use new style on next load.")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update style")}
end
end
@impl true
def handle_event("update_interval", %{"interval" => interval_str}, socket) do
case Integer.parse(interval_str) do
{interval, _} when interval > 0 ->
case Settings.update_setting("sitemap_schedule_interval_hours", interval_str) do
{:ok, _} ->
config = Sitemap.get_config()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
broadcast_settings_change(:interval_changed, %{
interval: interval,
version: new_version
})
{:noreply,
socket
|> assign(:config, config)
|> assign(:sitemap_version, new_version)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update interval")}
end
_ ->
{:noreply, put_flash(socket, :error, "Invalid interval value")}
end
end
@impl true
def handle_event("regenerate", _params, socket) do
base_url = Sitemap.get_base_url()
if base_url != "" do
case SchedulerWorker.regenerate_now() do
{:ok, _job} ->
{:noreply,
socket
|> assign(:generating, true)
|> put_flash(:info, "Sitemap generation queued. Stats will update automatically.")}
{:error, reason} ->
{:noreply,
socket
|> put_flash(:error, "Failed to queue generation: #{inspect(reason)}")}
end
else
{:noreply,
socket
|> put_flash(:error, "Please configure Base URL before generating")}
end
end
@impl true
def handle_event("preview", %{"type" => _type}, socket) do
base_url = Sitemap.get_base_url()
xsl_style = get_xsl_style(socket.assigns.config.html_style)
opts = [
base_url: base_url,
xsl_style: xsl_style,
xsl_enabled: socket.assigns.config.html_enabled
]
content =
case Generator.generate_all(opts) do
{:ok, %{index_xml: xml}} -> xml
_ -> "Error generating XML preview"
end
{:noreply,
socket
|> assign(:preview_mode, "xml")
|> assign(:preview_content, content)
|> assign(:show_preview, true)}
end
@impl true
def handle_event("close_preview", _params, socket) do
{:noreply,
socket
|> assign(:show_preview, false)
|> assign(:preview_mode, nil)
|> assign(:preview_content, nil)}
end
@impl true
def handle_event("invalidate_cache", _params, socket) do
case Generator.invalidate_and_regenerate() do
{:ok, _job} ->
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
{:noreply,
socket
|> assign(:sitemap_version, new_version)
|> assign(:generating, true)
|> assign(:module_stats, [])
|> assign(:module_files, [])
|> put_flash(:info, "All sitemaps cleared. Regeneration started...")}
{:error, reason} ->
{:noreply, put_flash(socket, :error, "Failed to regenerate: #{inspect(reason)}")}
end
end
@impl true
def handle_event("save_exclude_patterns", %{"patterns" => text}, socket) do
patterns = parse_lines(text)
case RouterDiscovery.invalid_patterns(patterns) do
[] ->
case Settings.update_setting(
"sitemap_router_discovery_exclude_patterns",
JSON.encode!(patterns)
) do
{:ok, _} ->
{:noreply,
socket
|> assign(:exclude_patterns_text, Enum.join(patterns, "\n"))
|> assign(:exclude_patterns_error, nil)
|> bump_and_broadcast("router_discovery_exclude_patterns")
|> put_flash(:info, "Exclude patterns updated")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update exclude patterns")}
end
invalid ->
{:noreply,
socket
|> assign(:exclude_patterns_text, text)
|> assign(
:exclude_patterns_error,
"Invalid regex pattern(s), not saved: #{Enum.join(invalid, ", ")}"
)}
end
end
@impl true
def handle_event("save_protected_pipelines", %{"pipelines" => text}, socket) do
names = parse_lines(text)
case invalid_pipeline_names(names) do
[] ->
case Settings.update_setting("sitemap_protected_pipelines", JSON.encode!(names)) do
{:ok, _} ->
{:noreply,
socket
|> assign(:protected_pipelines_text, Enum.join(names, "\n"))
|> assign(:protected_pipelines_error, nil)
|> bump_and_broadcast("protected_pipelines")
|> put_flash(:info, "Protected pipelines updated")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update protected pipelines")}
end
invalid ->
{:noreply,
socket
|> assign(:protected_pipelines_text, text)
|> assign(
:protected_pipelines_error,
"Invalid pipeline name(s) — use letters, digits and underscores only, not saved: " <>
Enum.join(invalid, ", ")
)}
end
end
@impl true
def handle_event("save_custom_urls", %{"json" => text}, socket) do
case decode_object_list(text) do
{:ok, list} ->
case Settings.update_setting("sitemap_custom_urls", JSON.encode!(list)) do
{:ok, _} ->
{:noreply,
socket
|> assign(:custom_urls_text, Json.encode_pretty!(list))
|> assign(:custom_urls_error, nil)
|> bump_and_broadcast("custom_urls")
|> put_flash(:info, "Custom URLs updated")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update custom URLs")}
end
{:error, message} ->
{:noreply,
socket
|> assign(:custom_urls_text, text)
|> assign(:custom_urls_error, message)}
end
end
@impl true
def handle_event("save_static_routes", %{"json" => text}, socket) do
case decode_object_list(text) do
{:ok, list} ->
case Settings.update_setting("sitemap_static_routes", JSON.encode!(list)) do
{:ok, _} ->
{:noreply,
socket
|> assign(:static_routes_text, Json.encode_pretty!(list))
|> assign(:static_routes_error, nil)
|> bump_and_broadcast("static_routes")
|> put_flash(:info, "Static routes updated")}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update static routes")}
end
{:error, message} ->
{:noreply,
socket
|> assign(:static_routes_text, text)
|> assign(:static_routes_error, message)}
end
end
@impl true
def handle_event("toggle_extension_setting", %{"key" => key}, socket) do
case find_extension_field(socket, key) do
%{type: :boolean} = field ->
# Read the current value through the rescue-protected helper (same as
# the render path) so a source that declares a boolean field with a
# non-boolean default can't crash the toggle: `get_boolean_setting/2`
# guards on `is_boolean(default)` and would otherwise raise here.
current = read_extension_field(field) == true
case Settings.update_boolean_setting(key, !current) do
{:ok, _} ->
{:noreply, socket |> refresh_extension_sources() |> bump_and_broadcast(key)}
{:error, _} ->
{:noreply, put_flash(socket, :error, "Failed to update setting")}
end
_ ->
{:noreply, put_flash(socket, :error, "Unknown setting")}
end
end
@impl true
def handle_event("save_extension_setting", %{"key" => key, "value" => raw_value}, socket) do
case find_extension_field(socket, key) do
%{type: :integer} ->
case Integer.parse(raw_value) do
{int, _} -> {:noreply, save_extension_value(socket, key, Integer.to_string(int))}
:error -> {:noreply, put_flash(socket, :error, "Invalid number")}
end
%{type: :string} ->
{:noreply, save_extension_value(socket, key, raw_value)}
_ ->
{:noreply, put_flash(socket, :error, "Unknown setting")}
end
end
# Handle PubSub message when sitemap generation completes
@impl true
def handle_info({:sitemap_generated, %{url_count: count}}, socket) do
config = Sitemap.get_config()
sitemap_version = get_sitemap_version(config)
module_stats = Sitemap.get_module_stats()
module_files = FileStorage.list_module_files()
{:noreply,
socket
|> assign(:generating, false)
|> assign(:config, config)
|> assign(:sitemap_version, sitemap_version)
|> assign(:module_stats, module_stats)
|> assign(:module_files, module_files)
|> put_flash(:info, "Sitemap generated successfully (#{count} URLs)")}
end
@impl true
def handle_info({:sitemap_settings_changed, %{type: :style_changed, version: version}}, socket) do
config = Sitemap.get_config()
{:noreply,
socket
|> assign(:config, config)
|> assign(:sitemap_version, version)}
end
@impl true
def handle_info({:sitemap_settings_changed, %{type: type}}, socket) do
config = Sitemap.get_config()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
{:noreply,
socket
|> assign(:config, config)
|> assign(:sitemap_version, new_version)
|> maybe_flash_for_setting(type)}
end
defp maybe_flash_for_setting(socket, :source_changed), do: socket
defp maybe_flash_for_setting(socket, :schedule_changed), do: socket
defp maybe_flash_for_setting(socket, :interval_changed), do: socket
defp maybe_flash_for_setting(socket, :html_changed), do: socket
defp maybe_flash_for_setting(socket, :sitemap_toggled), do: socket
defp maybe_flash_for_setting(socket, _), do: socket
defp broadcast_settings_change(type, data) do
PubSubManager.broadcast(
"sitemap:settings",
{:sitemap_settings_changed, Map.put(data, :type, type)}
)
rescue
_ -> :ok
end
# Maps old HTML style names to new XSL style names
def get_xsl_style(html_style) do
case html_style do
"grouped" -> "table"
"flat" -> "minimal"
style when style in ["table", "minimal"] -> style
_ -> "table"
end
end
# Helper: find stats for a specific module filename
def find_module_stat(module_stats, filename) do
Enum.find(module_stats, fn stat ->
stat["filename"] == filename or
String.starts_with?(stat["filename"] || "", filename)
end)
end
# Helper: count URLs for a source prefix across all module stats
def count_source_urls(module_stats, source_prefix) do
module_stats
|> Enum.filter(fn stat ->
String.starts_with?(stat["filename"] || "", source_prefix)
end)
|> Enum.reduce(0, fn stat, acc -> acc + (stat["url_count"] || 0) end)
end
# Helper: list files for a source prefix
def source_files(module_files, source_prefix) do
Enum.filter(module_files, &String.starts_with?(&1, source_prefix))
end
def format_timestamp(nil), do: "Never"
def format_timestamp(iso_string) when is_binary(iso_string) do
case DateTime.from_iso8601(iso_string) do
{:ok, dt, _} ->
fake_user = %{user_timezone: nil}
date_str = UtilsDate.format_date_with_user_timezone(dt, fake_user)
time_str = UtilsDate.format_time_with_user_timezone(dt, fake_user)
"#{date_str} #{time_str}"
_ ->
iso_string
end
end
def format_timestamp(_), do: "Never"
# ============================================================================
# Advanced settings (Router Discovery exclude patterns, protected pipelines,
# custom URLs, static routes) — raw text <-> stored JSON conversions.
# ============================================================================
# Helper: read a JSON-encoded setting, falling back to `default_term` when
# unset or invalid — mirrors the exact fallback each source module already
# applies when it reads the same key, so what's shown here matches what's
# actually in effect.
defp decoded_setting(key, default_term) do
case Settings.get_setting(key) do
nil ->
default_term
json when is_binary(json) ->
case JSON.decode(json) do
{:ok, term} -> term
_ -> default_term
end
end
end
def exclude_patterns_text do
decoded_setting(
"sitemap_router_discovery_exclude_patterns",
RouterDiscovery.default_exclude_patterns()
)
|> Enum.join("\n")
end
def protected_pipelines_text do
decoded_setting("sitemap_protected_pipelines", [])
|> Enum.map_join("\n", &to_string/1)
end
def protected_pipelines_defaults_text do
RouterDiscovery.default_protected_pipelines()
|> Enum.map_join(", ", &to_string/1)
end
def custom_urls_text do
decoded_setting("sitemap_custom_urls", [])
|> Json.encode_pretty!()
end
def static_routes_text do
decoded_setting("sitemap_static_routes", Static.default_static_routes())
|> Json.encode_pretty!()
end
# Helper: textarea -> trimmed, non-blank lines (one setting value per line)
defp parse_lines(text) do
text
|> String.split("\n")
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
@pipeline_name_pattern ~r/^[a-zA-Z_][a-zA-Z0-9_]*$/
# Pipeline names become atoms via String.to_atom/1 at collection time
# (see RouterDiscovery.safe_to_atom/1) — restrict to identifier-safe
# characters so a stray typo can't create an arbitrary atom.
defp invalid_pipeline_names(names) do
Enum.reject(names, &Regex.match?(@pipeline_name_pattern, &1))
end
# Helper: decode a JSON textarea value expected to hold an array of
# objects (sitemap_custom_urls / sitemap_static_routes shape).
defp decode_object_list(text) do
case JSON.decode!(text) do
list when is_list(list) ->
if Enum.all?(list, &is_map/1) do
{:ok, list}
else
{:error, "Must be a JSON array of objects"}
end
_other ->
{:error, "Must be a JSON array of objects"}
end
rescue
e in JSON.DecodeError -> {:error, "Invalid JSON: #{e.message}"}
end
# Shared tail for every advanced/extension setting save: bust the generated
# sitemap cache, bump the version so connected clients refresh, and notify
# other open tabs — the same three steps every other handler in this module
# performs inline after a successful Settings update.
defp bump_and_broadcast(socket, source_key) do
Generator.invalidate_cache()
new_version = UtilsDate.utc_now() |> DateTime.to_unix()
broadcast_settings_change(:source_changed, %{source: source_key, version: new_version})
assign(socket, :sitemap_version, new_version)
end
# ============================================================================
# Extension point: source-contributed settings
# (PhoenixKit.Modules.Sitemap.Sources.Source.sitemap_settings_schema/0)
# ============================================================================
# Builds the render-ready list of `{source_module, fields}` for every
# source that declares a settings schema, with each field's current value
# attached. Public (not just used by `mount/3`) so it can be unit tested
# directly against a mock source module, without needing a live DB-backed
# LiveView mount.
def build_extension_sources(source_modules) do
source_modules
|> Enum.map(fn mod -> {mod, safe_get_settings_schema(mod)} end)
|> Enum.reject(fn {_mod, schema} -> schema == [] end)
|> Enum.map(fn {mod, schema} ->
{mod, Enum.map(schema, &Map.put(&1, :value, read_extension_field(&1)))}
end)
end
# A source's schema is arbitrary data from another module — never let a
# malformed one crash the settings page for every admin (mirrors the same
# safety net `Source.safe_collect/2` applies to sitemap generation).
defp safe_get_settings_schema(mod) do
Source.get_settings_schema(mod)
rescue
error ->
Logger.warning(
"Sitemap source #{inspect(mod)} returned an invalid settings schema: #{inspect(error)}"
)
[]
end
defp read_extension_field(field) do
case field.type do
:boolean -> Settings.get_boolean_setting(field.key, field.default)
:integer -> Settings.get_integer_setting(field.key, field.default)
:string -> Settings.get_setting(field.key, field.default)
end
rescue
_ -> field.default
end
# Helper: human-readable card title for a source module in the extension
# section (e.g. `:router_discovery` -> "Router discovery").
def source_display_name(mod) do
mod.source_name() |> to_string() |> Phoenix.Naming.humanize()
rescue
_ -> mod |> to_string() |> String.trim_leading("Elixir.") |> Phoenix.Naming.humanize()
end
defp find_extension_field(socket, key) do
Enum.find_value(socket.assigns.extension_sources, fn {_mod, fields} ->
Enum.find(fields, &(&1.key == key))
end)
end
defp refresh_extension_sources(socket) do
assign(socket, :extension_sources, build_extension_sources(Generator.get_sources()))
end
defp save_extension_value(socket, key, value) do
case Settings.update_setting(key, value) do
{:ok, _} ->
socket
|> refresh_extension_sources()
|> bump_and_broadcast(key)
|> put_flash(:info, "Setting updated")
{:error, _} ->
put_flash(socket, :error, "Failed to update setting")
end
end
# Check which parent modules are actually enabled (not just sitemap toggles)
defp get_module_enabled_status do
%{
entities:
Code.ensure_loaded?(PhoenixKitEntities) and safe_module_enabled?(PhoenixKitEntities),
publishing: safe_module_available_and_enabled?(PhoenixKit.Modules.Publishing),
shop: safe_module_enabled?(PhoenixKitEcommerce),
posts: Code.ensure_loaded?(PhoenixKitPosts) and safe_module_enabled?(PhoenixKitPosts)
}
end
defp safe_module_enabled?(module) do
module.enabled?()
rescue
_ -> false
end
defp safe_module_available_and_enabled?(module) do
Code.ensure_loaded?(module) and safe_module_enabled?(module)
end
defp get_sitemap_version(config) do
case config.last_generated do
nil ->
UtilsDate.utc_now() |> DateTime.to_unix()
iso_string when is_binary(iso_string) ->
case DateTime.from_iso8601(iso_string) do
{:ok, dt, _} -> DateTime.to_unix(dt)
_ -> UtilsDate.utc_now() |> DateTime.to_unix()
end
end
end
end