diff --git a/lib/membership/membership.ex b/lib/membership/membership.ex
index 7fa35dc..ffe7703 100644
--- a/lib/membership/membership.ex
+++ b/lib/membership/membership.ex
@@ -178,6 +178,18 @@ defmodule Mv.Membership do
end
end
+ @doc """
+ Invalidates the global settings cache.
+
+ This should be used by callers that update settings through paths outside of
+ `update_settings/2` (for example, custom form submit flows) to keep reads via
+ `get_settings/0` consistent across views.
+ """
+ @spec invalidate_settings_cache() :: :ok
+ def invalidate_settings_cache do
+ SettingsCache.invalidate()
+ end
+
@doc """
Lists only required custom fields.
diff --git a/lib/mv/config.ex b/lib/mv/config.ex
index c807193..33c92cf 100644
--- a/lib/mv/config.ex
+++ b/lib/mv/config.ex
@@ -143,6 +143,27 @@ defmodule Mv.Config do
|> parse_and_validate_integer(default)
end
+ # ---------------------------------------------------------------------------
+ # Association name
+ # ENV variable takes priority; fallback to Settings from database.
+ # ---------------------------------------------------------------------------
+
+ @doc """
+ Returns the association name.
+
+ Reads from `ASSOCIATION_NAME` env first, then from Settings.
+ """
+ @spec association_name() :: String.t() | nil
+ def association_name do
+ env_or_setting("ASSOCIATION_NAME", :club_name)
+ end
+
+ @doc """
+ Returns true if ASSOCIATION_NAME is set (field is read-only in Settings).
+ """
+ @spec association_name_env_set?() :: boolean()
+ def association_name_env_set?, do: env_set?("ASSOCIATION_NAME")
+
# ---------------------------------------------------------------------------
# Vereinfacht accounting software integration
# ENV variables take priority; fallback to Settings from database.
diff --git a/lib/mv_web/components/layouts.ex b/lib/mv_web/components/layouts.ex
index 9aff23c..c6e3e01 100644
--- a/lib/mv_web/components/layouts.ex
+++ b/lib/mv_web/components/layouts.ex
@@ -135,8 +135,11 @@ defmodule MvWeb.Layouts do
slot :inner_block, required: true
def app(assigns) do
- # Single get_settings() for layout; derive club_name and join_form_enabled to avoid duplicate query.
- %{club_name: club_name, join_form_enabled: join_form_enabled} = get_layout_settings()
+ # Single settings read for layout defaults.
+ # Use an explicitly provided club_name as source of truth to avoid stale
+ # values from cache reads immediately after a settings update in LiveViews.
+ %{club_name: fallback_club_name, join_form_enabled: join_form_enabled} = get_layout_settings()
+ club_name = assigns[:club_name] || fallback_club_name
# NOTE: Unprocessed count runs on every page load when join form is enabled; consider
# loading only on navigation or caching briefly if performance becomes an issue.
diff --git a/lib/mv_web/live/global_settings_live.ex b/lib/mv_web/live/global_settings_live.ex
index 983f075..228970a 100644
--- a/lib/mv_web/live/global_settings_live.ex
+++ b/lib/mv_web/live/global_settings_live.ex
@@ -65,6 +65,7 @@ defmodule MvWeb.GlobalSettingsLive do
|> assign(:settings, settings)
|> assign(:locale, locale)
|> assign(:environment, environment)
+ |> assign(:association_name_env_set, Mv.Config.association_name_env_set?())
|> assign(:vereinfacht_env_configured, Mv.Config.vereinfacht_env_configured?())
|> assign(:vereinfacht_api_url_env_set, Mv.Config.vereinfacht_api_url_env_set?())
|> assign(:vereinfacht_api_key_env_set, Mv.Config.vereinfacht_api_key_env_set?())
@@ -125,6 +126,13 @@ defmodule MvWeb.GlobalSettingsLive do
<%!-- Club Settings Section --%>
<.form_section title={gettext("Club Settings")}>
+ <%= if @association_name_env_set do %>
+
+ {gettext(
+ "Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
+ )}
+
+ <% end %>
<.form for={@form} id="settings-form" phx-change="validate" phx-submit="save">
<.input
@@ -132,10 +140,18 @@ defmodule MvWeb.GlobalSettingsLive do
type="text"
label={gettext("Association Name")}
required
+ disabled={@association_name_env_set}
+ placeholder={
+ if(@association_name_env_set, do: gettext("From ASSOCIATION_NAME"), else: nil)
+ }
/>
- <.button phx-disable-with={gettext("Saving...")} variant="primary">
+ <.button
+ :if={not @association_name_env_set}
+ phx-disable-with={gettext("Saving...")}
+ variant="primary"
+ >
{gettext("Save Name")}
@@ -919,6 +935,7 @@ defmodule MvWeb.GlobalSettingsLive do
# Never send blank API key / client secret / smtp password so we do not overwrite stored secrets
setting_params_clean =
setting_params
+ |> drop_env_managed_association_name()
|> drop_blank_vereinfacht_api_key()
|> drop_blank_oidc_client_secret()
|> drop_blank_smtp_password()
@@ -927,6 +944,10 @@ defmodule MvWeb.GlobalSettingsLive do
case MvWeb.LiveHelpers.submit_form(socket.assigns.form, setting_params_clean, actor) do
{:ok, updated_settings} ->
+ # Keep cross-view reads consistent after settings updates (layouts/sidebar
+ # read via Membership.get_settings/0).
+ Membership.invalidate_settings_cache()
+
# Use the returned record for the form so saved values show immediately;
# get_settings() can return cached data without the new attribute until reload.
test_result =
@@ -1195,10 +1216,19 @@ defmodule MvWeb.GlobalSettingsLive do
end
end
+ defp drop_env_managed_association_name(params) when is_map(params) do
+ if Mv.Config.association_name_env_set?() do
+ Map.delete(params, "club_name")
+ else
+ params
+ end
+ end
+
defp assign_form(%{assigns: %{settings: settings}} = socket) do
- # Show ENV values in disabled fields (Vereinfacht, OIDC, SMTP); never expose secrets in form
+ # Show ENV values in disabled fields (Association Name, Vereinfacht, OIDC, SMTP); never expose secrets in form
settings_display =
settings
+ |> merge_association_env_values()
|> merge_vereinfacht_env_values()
|> merge_oidc_env_values()
|> merge_smtp_env_values()
@@ -1225,6 +1255,15 @@ defmodule MvWeb.GlobalSettingsLive do
defp put_if_env_set(map, _key, false, _value), do: map
defp put_if_env_set(map, key, true, value), do: Map.put(map, key, value)
+ defp merge_association_env_values(s) do
+ put_if_env_set(
+ s,
+ :club_name,
+ Mv.Config.association_name_env_set?(),
+ Mv.Config.association_name()
+ )
+ end
+
defp merge_vereinfacht_env_values(s) do
s
|> put_if_env_set(
diff --git a/priv/gettext/de/LC_MESSAGES/default.po b/priv/gettext/de/LC_MESSAGES/default.po
index 1482490..31830ce 100644
--- a/priv/gettext/de/LC_MESSAGES/default.po
+++ b/priv/gettext/de/LC_MESSAGES/default.po
@@ -3927,3 +3927,13 @@ msgstr "Die SMTP-Umgebungs-Konfiguration ist unvollständig. Fehlend: %{keys}"
#, elixir-autogen, elixir-format
msgid "SMTP is fully managed via environment variables. All SMTP fields are read-only."
msgstr "SMTP wird vollständig über Umgebungsvariablen verwaltet. Alle SMTP-Felder sind schreibgeschützt."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
+msgstr "Der Vereinsname wird über die Umgebungsvariable ASSOCIATION_NAME gesetzt. Dieses Feld ist schreibgeschützt."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From ASSOCIATION_NAME"
+msgstr "Aus ASSOCIATION_NAME"
diff --git a/priv/gettext/default.pot b/priv/gettext/default.pot
index 21e7b16..586d1f4 100644
--- a/priv/gettext/default.pot
+++ b/priv/gettext/default.pot
@@ -3927,3 +3927,13 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "SMTP is fully managed via environment variables. All SMTP fields are read-only."
msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
+msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From ASSOCIATION_NAME"
+msgstr ""
diff --git a/priv/gettext/en/LC_MESSAGES/default.po b/priv/gettext/en/LC_MESSAGES/default.po
index 7d94b7c..6b8db53 100644
--- a/priv/gettext/en/LC_MESSAGES/default.po
+++ b/priv/gettext/en/LC_MESSAGES/default.po
@@ -3927,3 +3927,13 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "SMTP is fully managed via environment variables. All SMTP fields are read-only."
msgstr ""
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
+msgstr "Association name is set via environment variable ASSOCIATION_NAME. This field is read-only."
+
+#: lib/mv_web/live/global_settings_live.ex
+#, elixir-autogen, elixir-format
+msgid "From ASSOCIATION_NAME"
+msgstr "From ASSOCIATION_NAME"
diff --git a/test/mv_web/components/layouts_test.exs b/test/mv_web/components/layouts_test.exs
new file mode 100644
index 0000000..411750b
--- /dev/null
+++ b/test/mv_web/components/layouts_test.exs
@@ -0,0 +1,26 @@
+defmodule MvWeb.LayoutsTest do
+ use MvWeb.ConnCase, async: false
+
+ import Phoenix.LiveViewTest
+
+ alias Mv.Membership
+ alias MvWeb.Layouts
+
+ describe "app/1" do
+ test "prefers provided club_name over settings fallback" do
+ {:ok, settings} = Membership.get_settings()
+ {:ok, _} = Membership.update_settings(settings, %{club_name: "Settings Club Name"})
+
+ html =
+ render_component(&Layouts.app/1, %{
+ flash: %{},
+ current_user: nil,
+ club_name: "Provided Club Name",
+ inner_block: [%{inner_block: fn _, _ -> "content" end}]
+ })
+
+ assert html =~ "Provided Club Name"
+ refute html =~ "Settings Club Name"
+ end
+ end
+end
diff --git a/test/mv_web/live/global_settings_live_test.exs b/test/mv_web/live/global_settings_live_test.exs
index 37c4e38..8d2963c 100644
--- a/test/mv_web/live/global_settings_live_test.exs
+++ b/test/mv_web/live/global_settings_live_test.exs
@@ -13,6 +13,7 @@ defmodule MvWeb.GlobalSettingsLiveTest do
"MAIL_FROM_NAME",
"MAIL_FROM_EMAIL"
]
+ @association_env_key "ASSOCIATION_NAME"
describe "Global Settings LiveView" do
setup %{conn: conn} do
@@ -51,6 +52,17 @@ defmodule MvWeb.GlobalSettingsLiveTest do
assert render(view) =~ "Updated Club Name"
end
+ test "updated club name is shown after remount", %{conn: conn} do
+ {:ok, view, _html} = live(conn, ~p"/settings")
+
+ assert view
+ |> form("#settings-form", %{setting: %{club_name: "Remount Club Name"}})
+ |> render_submit()
+
+ {:ok, _view_after_remount, html_after_remount} = live(conn, ~p"/settings")
+ assert html_after_remount =~ "Remount Club Name"
+ end
+
test "shows error when club_name is empty", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
@@ -90,6 +102,34 @@ defmodule MvWeb.GlobalSettingsLiveTest do
"Open"
)
end
+
+ @tag :ui
+ test "disables association name input when ASSOCIATION_NAME is set", %{conn: conn} do
+ clear_association_name_env()
+ System.put_env(@association_env_key, "Association Name from ENV")
+ on_exit(fn -> clear_association_name_env() end)
+
+ {:ok, view, _html} = live(conn, ~p"/settings")
+
+ assert has_element?(view, "#setting_club_name[disabled]")
+ assert has_element?(view, "#setting_club_name[placeholder='From ASSOCIATION_NAME']")
+ refute has_element?(view, "#settings-form button", "Save Name")
+ assert render(view) =~ "Association name is set via environment variable ASSOCIATION_NAME"
+ end
+
+ @tag :ui
+ test "keeps association name input editable when ASSOCIATION_NAME is not set", %{conn: conn} do
+ clear_association_name_env()
+ on_exit(fn -> clear_association_name_env() end)
+
+ {:ok, view, _html} = live(conn, ~p"/settings")
+
+ refute has_element?(view, "#setting_club_name[disabled]")
+ assert has_element?(view, "#settings-form button", "Save Name")
+
+ refute render(view) =~
+ "Association name is set via environment variable ASSOCIATION_NAME"
+ end
end
describe "SMTP / E-Mail section" do
@@ -270,4 +310,8 @@ defmodule MvWeb.GlobalSettingsLiveTest do
defp clear_smtp_env do
Enum.each(@smtp_env_keys, &System.delete_env/1)
end
+
+ defp clear_association_name_env do
+ System.delete_env(@association_env_key)
+ end
end