diff --git a/config/config.exs b/config/config.exs index f9cf6e44..90686f56 100644 --- a/config/config.exs +++ b/config/config.exs @@ -37,10 +37,14 @@ config :ueberauth, Ueberauth, providers: %{} # Configure Oban (if using job processing) config :phoenix_kit, Oban, repo: PhoenixKit.Repo, - queues: [default: 10, emails: 50, file_processing: 20, posts: 10], + queues: [default: 10, emails: 50, file_processing: 20, posts: 10, scheduled_jobs: 1], plugins: [ - # Keep completed/cancelled/discarded jobs for 30 days for dashboard visibility - {Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 30}, + # Main pruner: 30 days for most queues + {Oban.Plugins.Pruner, + max_age: 60 * 60 * 24 * 30, + queue: [:default, :emails, :file_processing, :posts, :sitemap, :sqs_polling, :sync]}, + # Dedicated pruner: 1 day only for scheduled_jobs (cron runs every minute) + {Oban.Plugins.Pruner, max_age: 60 * 60 * 24, queue: [:scheduled_jobs]}, {Oban.Plugins.Cron, crontab: [ {"* * * * *", PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker}, diff --git a/lib/phoenix_kit/install/oauth_config.ex b/lib/phoenix_kit/install/oauth_config.ex index 8ccc1731..d794779a 100644 --- a/lib/phoenix_kit/install/oauth_config.ex +++ b/lib/phoenix_kit/install/oauth_config.ex @@ -17,7 +17,7 @@ defmodule PhoenixKit.Install.OAuthConfig do ``` This approach allows: - - **Minimal compile-time configuration** - Only `config :ueberauth, Ueberauth, providers: []` + - **Minimal compile-time configuration** - Only `config :ueberauth, Ueberauth, providers: %{}` - **Database-driven credentials** - Credentials loaded from Settings table at runtime - **Dynamic provider management** - Add/remove/modify providers without app restart @@ -70,7 +70,7 @@ defmodule PhoenixKit.Install.OAuthConfig do # Configure Ueberauth (minimal configuration for compilation) # OAuth providers are configured dynamically at runtime from database - config :ueberauth, Ueberauth, providers: [] + config :ueberauth, Ueberauth, providers: %{} """ try do @@ -184,7 +184,7 @@ defmodule PhoenixKit.Install.OAuthConfig do Please add the following to config/config.exs: - config :ueberauth, Ueberauth, providers: [] + config :ueberauth, Ueberauth, providers: %{} This minimal configuration is required for compilation. OAuth providers are configured dynamically at runtime from the database. diff --git a/lib/phoenix_kit/install/oban_config.ex b/lib/phoenix_kit/install/oban_config.ex index 0b9d01ab..e42165ff 100644 --- a/lib/phoenix_kit/install/oban_config.ex +++ b/lib/phoenix_kit/install/oban_config.ex @@ -118,12 +118,16 @@ defmodule PhoenixKit.Install.ObanConfig do emails: 50, # Email processing file_processing: 20, # File variant generation (storage system) posts: 10, # Posts scheduled publishing + scheduled_jobs: 1, # Scheduled jobs cron (1-day retention) sitemap: 5, # Sitemap generation sqs_polling: 1, # SQS polling for email events (only one concurrent job) sync: 5 # Sync data import ], plugins: [ - {Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 30}, # Keep jobs for 30 days + # Main pruner: 30 days for most queues + {Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 30, queue: [:default, :emails, :file_processing, :posts, :sitemap, :sqs_polling, :sync]}, + # Dedicated pruner: 1 day only for scheduled_jobs (cron runs every minute) + {Oban.Plugins.Pruner, max_age: 60 * 60 * 24, queue: [:scheduled_jobs]}, {Oban.Plugins.Cron, crontab: [ {"* * * * *", PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker} @@ -767,12 +771,16 @@ defmodule PhoenixKit.Install.ObanConfig do emails: 50, file_processing: 20, posts: 10, + scheduled_jobs: 1, # Scheduled jobs cron (1-day retention) sitemap: 5, sqs_polling: 1, sync: 5 ], plugins: [ - {Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 30}, # Keep jobs for 30 days + # Main pruner: 30 days for most queues + {Oban.Plugins.Pruner, max_age: 60 * 60 * 24 * 30, queue: [:default, :emails, :file_processing, :posts, :sitemap, :sqs_polling, :sync]}, + # Dedicated pruner: 1 day only for scheduled_jobs (cron runs every minute) + {Oban.Plugins.Pruner, max_age: 60 * 60 * 24, queue: [:scheduled_jobs]}, {Oban.Plugins.Cron, crontab: [ {"* * * * *", PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker} diff --git a/lib/phoenix_kit/scheduled_jobs/workers/process_scheduled_jobs_worker.ex b/lib/phoenix_kit/scheduled_jobs/workers/process_scheduled_jobs_worker.ex index 2af089ea..7a1a7605 100644 --- a/lib/phoenix_kit/scheduled_jobs/workers/process_scheduled_jobs_worker.ex +++ b/lib/phoenix_kit/scheduled_jobs/workers/process_scheduled_jobs_worker.ex @@ -35,7 +35,7 @@ defmodule PhoenixKit.ScheduledJobs.Workers.ProcessScheduledJobsWorker do - Worker itself always returns :ok to prevent Oban retries """ - use Oban.Worker, queue: :default, max_attempts: 1 + use Oban.Worker, queue: :scheduled_jobs, max_attempts: 1 require Logger diff --git a/lib/phoenix_kit/settings/queries.ex b/lib/phoenix_kit/settings/queries.ex new file mode 100644 index 00000000..876a0fb6 --- /dev/null +++ b/lib/phoenix_kit/settings/queries.ex @@ -0,0 +1,160 @@ +defmodule PhoenixKit.Settings.Queries do + @moduledoc """ + Ecto queries for Settings context. + + This module encapsulates all database queries for settings management, + providing a centralized location for query logic. + """ + + import Ecto.Query + + alias PhoenixKit.RepoHelper + alias PhoenixKit.Settings.Setting + + # Single record queries + + @doc """ + Gets a setting record by key. + + ## Examples + + iex> PhoenixKit.Settings.Queries.get_setting_by_key("time_zone") + %Setting{key: "time_zone", value: "0"} + + iex> PhoenixKit.Settings.Queries.get_setting_by_key("non_existent") + nil + """ + def get_setting_by_key(key) when is_binary(key) do + repo().get_by(Setting, key: key) + end + + # Multiple records queries + + @doc """ + Lists all settings ordered by key. + + ## Examples + + iex> PhoenixKit.Settings.Queries.list_settings() + [%Setting{key: "date_format", value: "Y-m-d"}, %Setting{key: "time_zone", value: "0"}, ...] + """ + def list_settings do + Setting + |> order_by([s], s.key) + |> repo().all() + end + + @doc """ + Gets all settings as a list of {key, value} tuples. + + ## Examples + + iex> PhoenixKit.Settings.Queries.list_settings_key_values() + [{"time_zone", "0"}, {"date_format", "Y-m-d"}] + """ + def list_settings_key_values do + Setting + |> select([s], {s.key, s.value}) + |> repo().all() + end + + @doc """ + Lists settings for specific keys as a list of {key, value} tuples. + + ## Examples + + iex> PhoenixKit.Settings.Queries.list_settings_key_values_by_keys(["time_zone", "date_format"]) + [{"time_zone", "0"}, {"date_format", "Y-m-d"}] + """ + def list_settings_key_values_by_keys(keys) when is_list(keys) do + Setting + |> where([s], s.key in ^keys) + |> select([s], {s.key, s.value}) + |> repo().all() + end + + @doc """ + Lists setting records for specific keys. + + ## Examples + + iex> PhoenixKit.Settings.Queries.list_settings_by_keys(["time_zone"]) + [%Setting{key: "time_zone", value: "0"}] + """ + def list_settings_by_keys(keys) when is_list(keys) do + Setting + |> where([s], s.key in ^keys) + |> repo().all() + end + + @doc """ + Lists settings by keys with JSON priority as a list of {key, value} tuples. + + Returns a list where value_json is used if present, otherwise falls back to + the string value. + + ## Examples + + iex> PhoenixKit.Settings.Queries.list_settings_with_json_priority_by_keys(["theme"]) + [{"theme", %{"primary" => "#3b82f6"}}] + """ + def list_settings_with_json_priority_by_keys(keys) when is_list(keys) do + Setting + |> where([s], s.key in ^keys) + |> repo().all() + |> Enum.map(fn setting -> + value = if setting.value_json, do: setting.value_json, else: setting.value + {setting.key, value} + end) + end + + # Write operations + + @doc """ + Inserts a new setting. + + ## Examples + + iex> %Setting{} |> Setting.changeset(%{key: "theme", value: "dark"}) + ...> |> PhoenixKit.Settings.Queries.insert_setting() + {:ok, %Setting{}} + """ + def insert_setting(changeset) do + repo().insert(changeset) + end + + @doc """ + Updates an existing setting. + + ## Examples + + iex> setting |> Setting.update_changeset(%{value: "light"}) + ...> |> PhoenixKit.Settings.Queries.update_setting() + {:ok, %Setting{}} + """ + def update_setting(changeset) do + repo().update(changeset) + end + + # Transaction + + @doc """ + Executes a transaction with multiple operations. + + ## Examples + + iex> Ecto.Multi.new() + ...> |> multi_operation() + ...> |> PhoenixKit.Settings.Queries.transaction() + {:ok, result} + """ + def transaction(multi) do + repo().transaction(multi) + end + + # Private functions + + defp repo do + RepoHelper.repo() + end +end diff --git a/lib/phoenix_kit/settings/settings.ex b/lib/phoenix_kit/settings/settings.ex index 0dbe5de4..f53de553 100644 --- a/lib/phoenix_kit/settings/settings.ex +++ b/lib/phoenix_kit/settings/settings.ex @@ -68,6 +68,7 @@ defmodule PhoenixKit.Settings do alias PhoenixKit.Config.AWS alias PhoenixKit.Modules.Languages + alias PhoenixKit.Settings.Queries alias PhoenixKit.Settings.Setting alias PhoenixKit.Settings.Setting.SettingsForm alias PhoenixKit.Users.Role @@ -77,12 +78,6 @@ defmodule PhoenixKit.Settings do @default_locale PhoenixKit.Config.default_locale() @cache_name :settings - # Gets the configured repository for database operations. - # Uses PhoenixKit.RepoHelper to get the configured repo with proper prefix support. - defp repo do - PhoenixKit.RepoHelper.repo() - end - @doc """ Gets default values for all settings. @@ -182,7 +177,7 @@ defmodule PhoenixKit.Settings do if Application.get_env(:phoenix_kit, :update_mode, false) do nil else - setting_record = repo().get_by(Setting, key: key) + setting_record = Queries.get_setting_by_key(key) case setting_record do %Setting{value: value} -> value @@ -402,7 +397,7 @@ defmodule PhoenixKit.Settings do nil """ def get_json_setting(key) when is_binary(key) do - setting_record = repo().get_by(Setting, key: key) + setting_record = Queries.get_setting_by_key(key) case setting_record do %Setting{value_json: value_json} when not is_nil(value_json) -> value_json @@ -487,16 +482,16 @@ defmodule PhoenixKit.Settings do """ def update_json_setting(key, json_value) when is_binary(key) do result = - case repo().get_by(Setting, key: key) do + case Queries.get_setting_by_key(key) do %Setting{} = setting -> setting |> Setting.update_changeset(%{value_json: json_value, value: nil}) - |> repo().update() + |> Queries.update_setting() nil -> %Setting{} |> Setting.changeset(%{key: key, value_json: json_value, value: nil}) - |> repo().insert() + |> Queries.insert_setting() end # Invalidate cache on successful update @@ -522,19 +517,19 @@ defmodule PhoenixKit.Settings do """ def update_json_setting_with_module(key, json_value, module) when is_binary(key) and is_binary(module) do - existing_setting = repo().get_by(Setting, key: key) + existing_setting = Queries.get_setting_by_key(key) result = case existing_setting do %Setting{} = setting -> setting |> Setting.update_changeset(%{value_json: json_value, value: nil, module: module}) - |> repo().update() + |> Queries.update_setting() nil -> %Setting{} |> Setting.changeset(%{key: key, value_json: json_value, value: nil, module: module}) - |> repo().insert() + |> Queries.insert_setting() end # Invalidate cache on successful update @@ -724,10 +719,7 @@ defmodule PhoenixKit.Settings do %{} else if repo_available?() do - Setting - |> where([s], s.key in ^keys) - |> select([s], {s.key, s.value}) - |> repo().all() + Queries.list_settings_key_values_by_keys(keys) |> Map.new() else %{} @@ -882,9 +874,7 @@ defmodule PhoenixKit.Settings do } """ def list_all_settings do - Setting - |> select([s], {s.key, s.value}) - |> repo().all() + Queries.list_settings_key_values() |> Map.new() end @@ -903,9 +893,7 @@ defmodule PhoenixKit.Settings do ] """ def list_settings do - Setting - |> order_by([s], s.key) - |> repo().all() + Queries.list_settings() end @doc """ @@ -1033,16 +1021,16 @@ defmodule PhoenixKit.Settings do stored_value = value || "" result = - case repo().get_by(Setting, key: key) do + case Queries.get_setting_by_key(key) do %Setting{} = setting -> setting |> Setting.update_changeset(%{value: stored_value}) - |> repo().update() + |> Queries.update_setting() nil -> %Setting{} |> Setting.changeset(%{key: key, value: stored_value}) - |> repo().insert() + |> Queries.insert_setting() end # Invalidate cache on successful update @@ -1078,16 +1066,14 @@ defmodule PhoenixKit.Settings do # Load all existing settings in a single query existing_settings = - Setting - |> where([s], s.key in ^keys) - |> repo().all() + Queries.list_settings_by_keys(keys) |> Map.new(fn setting -> {setting.key, setting} end) # Perform all updates/inserts in a transaction result = Ecto.Multi.new() |> add_batch_operations(settings_map, existing_settings) - |> repo().transaction() + |> Queries.transaction() case result do {:ok, _changes} -> @@ -1155,19 +1141,19 @@ defmodule PhoenixKit.Settings do {:ok, %Setting{key: "codes_enabled", value: "true", module: "referral_codes"}} """ def update_setting_with_module(key, value, module) when is_binary(key) and is_binary(value) do - existing_setting = repo().get_by(Setting, key: key) + existing_setting = Queries.get_setting_by_key(key) result = case existing_setting do %Setting{} = setting -> setting |> Setting.update_changeset(%{value: value, module: module}) - |> repo().update() + |> Queries.update_setting() nil -> %Setting{} |> Setting.changeset(%{key: key, value: value, module: module}) - |> repo().insert() + |> Queries.insert_setting() end # Invalidate cache on successful update @@ -1485,7 +1471,7 @@ defmodule PhoenixKit.Settings do # Check if repository is available before attempting to warm cache # This prevents errors during Mix tasks when repo might not be started yet if repo_available?() do - settings = repo().all(Setting) + settings = Queries.list_settings() settings |> Enum.map(fn setting -> @@ -1537,23 +1523,7 @@ defmodule PhoenixKit.Settings do # Check if repository is available if repo_available?() do - settings = - Setting - |> where([s], s.key in ^critical_keys) - |> repo().all() - - settings - |> Enum.map(fn setting -> - # Prioritize JSON value over string value for cache storage - value = - if setting.value_json do - setting.value_json - else - setting.value - end - - {setting.key, value} - end) + Queries.list_settings_with_json_priority_by_keys(critical_keys) |> Map.new() else # Repo not available - return empty map @@ -1574,10 +1544,7 @@ defmodule PhoenixKit.Settings do # Batch query multiple string settings from database in a single operation defp query_settings_batch(keys) do - Setting - |> where([s], s.key in ^keys) - |> select([s], {s.key, s.value}) - |> repo().all() + Queries.list_settings_key_values_by_keys(keys) |> Map.new() rescue _error -> @@ -1587,10 +1554,9 @@ defmodule PhoenixKit.Settings do # Batch query multiple JSON settings from database in a single operation defp query_json_settings_batch(keys) do - Setting - |> where([s], s.key in ^keys) - |> repo().all() - |> Enum.reduce(%{}, fn setting, acc -> + settings = Queries.list_settings_by_keys(keys) + + Enum.reduce(settings, %{}, fn setting, acc -> # Prioritize JSON value over string value (same logic as warm_cache_data) value = if setting.value_json, do: setting.value_json, else: nil Map.put(acc, setting.key, value) @@ -1611,7 +1577,7 @@ defmodule PhoenixKit.Settings do else # Check if repository is available before attempting query if repo_available?() do - case repo().get_by(Setting, key: key) do + case Queries.get_setting_by_key(key) do %Setting{value: value} -> PhoenixKit.Cache.put(@cache_name, key, value) value @@ -1642,7 +1608,7 @@ defmodule PhoenixKit.Settings do defp query_and_cache_json_setting(key) do # Check if repository is available before attempting query if repo_available?() do - case repo().get_by(Setting, key: key) do + case Queries.get_setting_by_key(key) do %Setting{value_json: value_json} when not is_nil(value_json) -> PhoenixKit.Cache.put(@cache_name, key, value_json) value_json diff --git a/lib/phoenix_kit_web/components/user_dashboard_nav.ex b/lib/phoenix_kit_web/components/user_dashboard_nav.ex index 57b7b757..47d24aaf 100644 --- a/lib/phoenix_kit_web/components/user_dashboard_nav.ex +++ b/lib/phoenix_kit_web/components/user_dashboard_nav.ex @@ -59,13 +59,13 @@ defmodule PhoenixKitWeb.Components.UserDashboardNav do <%= if PhoenixKit.Users.Auth.Scope.admin?(@scope) do %>
  • - if(active_path?(assigns[:current_path], "/admin"), do: " bg-primary text-primary-content", else: "")} > <.icon name="hero-shield-check" class="w-4 h-4" /> Admin Panel - +
  • <%= if @admin_edit_url do %>
  • @@ -81,23 +81,23 @@ defmodule PhoenixKitWeb.Components.UserDashboardNav do <% end %>
  • - if(active_path?(assigns[:current_path], "/dashboard"), do: " bg-primary text-primary-content", else: "")} > <.icon name="hero-home" class="w-4 h-4" /> Dashboard - +
  • - if(active_path?(assigns[:current_path], "/dashboard/settings"), do: " bg-primary text-primary-content", else: "")} > <.icon name="hero-cog-6-tooth" class="w-4 h-4" /> Settings - +
  • <% user_languages = get_user_languages() %>