From ab03ae046732c33c36d4fa63c3d63d92b7bd4f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Komendarczuk?= Date: Tue, 9 Jun 2026 14:45:19 +0200 Subject: [PATCH 1/6] Moderation actions start expiring after acknowledgement --- .../coordinator/coordinator_server.ex | 26 +++++++++ lib/teiserver/moderation/libs/action_lib.ex | 5 ++ lib/teiserver/moderation/libs/test_lib.ex | 2 +- lib/teiserver/moderation/schemas/action.ex | 15 +++--- .../tasks/refresh_user_restrictions_task.ex | 20 ++++--- .../moderation/action_controller.ex | 26 +++++---- .../controllers/moderation/ban_controller.ex | 2 +- .../moderation/action/form.html.heex | 15 ++++-- .../moderation/action/index.html.heex | 20 +++---- .../moderation/action/tab_details.html.heex | 42 ++++++++------- .../views/moderation/action_view.ex | 13 +++++ ...854_add_duration_to_moderation_actions.exs | 10 ++++ test/teiserver/moderation/moderation_test.exs | 54 ++++++++++++++++++- .../moderation/action_controller_test.exs | 10 ++-- 14 files changed, 197 insertions(+), 63 deletions(-) create mode 100644 priv/repo/migrations/20260609103854_add_duration_to_moderation_actions.exs diff --git a/lib/teiserver/coordinator/coordinator_server.ex b/lib/teiserver/coordinator/coordinator_server.ex index 796d114294..796aeabe57 100644 --- a/lib/teiserver/coordinator/coordinator_server.ex +++ b/lib/teiserver/coordinator/coordinator_server.ex @@ -16,6 +16,7 @@ defmodule Teiserver.Coordinator.CoordinatorServer do alias Teiserver.Coordinator.Parser alias Teiserver.Lobby alias Teiserver.Moderation + alias Teiserver.Moderation.RefreshUserRestrictionsTask alias Teiserver.Room alias Teiserver.Telemetry @@ -226,6 +227,31 @@ defmodule Teiserver.Coordinator.CoordinatorServer do ) end + pending_actions = + Moderation.list_actions( + search: [ + target_id: userid, + expiry: "Pending only" + ] + ) + + db_user = + if Enum.empty?(pending_actions) do + db_user + else + now = DateTime.utc_now() + + Enum.each(pending_actions, fn action -> + if action.duration do + expires = DateTime.add(now, action.duration, :second) + Moderation.update_action(action, %{"expires" => expires}) + end + end) + + RefreshUserRestrictionsTask.refresh_user(userid) + Account.get_user(userid) + end + relevant_restrictions = db_user.restrictions |> Enum.filter(fn r -> not Enum.member?(["Bridging"], r) end) diff --git a/lib/teiserver/moderation/libs/action_lib.ex b/lib/teiserver/moderation/libs/action_lib.ex index 47a3302b93..56c20e80a3 100644 --- a/lib/teiserver/moderation/libs/action_lib.ex +++ b/lib/teiserver/moderation/libs/action_lib.ex @@ -97,6 +97,11 @@ defmodule Teiserver.Moderation.ActionLib do def _search(query, :expiry, "All"), do: query + def _search(query, :expiry, "Pending only") do + from actions in query, + where: is_nil(actions.expires) + end + def _search(query, :expiry, "Completed only") do from actions in query, where: actions.expires < ^DateTime.utc_now() diff --git a/lib/teiserver/moderation/libs/test_lib.ex b/lib/teiserver/moderation/libs/test_lib.ex index 2fe38b6f47..7faabc5d91 100644 --- a/lib/teiserver/moderation/libs/test_lib.ex +++ b/lib/teiserver/moderation/libs/test_lib.ex @@ -30,7 +30,7 @@ defmodule Teiserver.Moderation.ModerationTestLib do reason: "Reason", restrictions: ["Login"], score_modifier: 1000, - expires: DateTime.shift(DateTime.utc_now(), day: 5) + duration: 5 * 86_400 } |> Map.merge(attrs) |> Moderation.create_action() diff --git a/lib/teiserver/moderation/schemas/action.ex b/lib/teiserver/moderation/schemas/action.ex index 12c1adca0d..87f06b1325 100644 --- a/lib/teiserver/moderation/schemas/action.ex +++ b/lib/teiserver/moderation/schemas/action.ex @@ -2,7 +2,6 @@ defmodule Teiserver.Moderation.Action do @moduledoc false alias Ecto.Changeset - alias Teiserver.Helper.DateHelper use TeiserverWeb, :schema @@ -14,6 +13,7 @@ defmodule Teiserver.Moderation.Action do field :restrictions, {:array, :string} field :score_modifier, :integer field :expires, :naive_datetime + field :duration, :integer field :hidden, :boolean, default: false @@ -31,20 +31,23 @@ defmodule Teiserver.Moderation.Action do struct |> cast( params, - ~w(target_id reason restrictions score_modifier expires notes hidden discord_message_id appeal_status)a + ~w(target_id reason restrictions score_modifier expires duration notes hidden discord_message_id appeal_status)a ) - |> validate_required(~w(target_id reason restrictions expires score_modifier)a) + |> validate_required(~w(target_id reason restrictions duration score_modifier)a) |> adjust_restrictions() |> validate_length(:restrictions, min: 1) end + # 10 years in seconds + @permanent_threshold_seconds 10 * 365 * 24 * 3_600 + defp adjust_restrictions(%Ecto.Changeset{} = struct) do - years = DateTime.shift(DateTime.utc_now(), year: 10) - expires = Changeset.get_field(struct, :expires, []) + duration = Changeset.get_field(struct, :duration) inbound_restrictions = Changeset.get_field(struct, :restrictions, []) new_restrictions = - if DateHelper.greater_than(expires, years) and Enum.member?(inbound_restrictions, "Login") do + if duration && duration >= @permanent_threshold_seconds && + Enum.member?(inbound_restrictions, "Login") do ["Permanently banned" | inbound_restrictions] |> Enum.uniq() else (inbound_restrictions || []) |> List.delete("Permanently banned") diff --git a/lib/teiserver/moderation/tasks/refresh_user_restrictions_task.ex b/lib/teiserver/moderation/tasks/refresh_user_restrictions_task.ex index c3b0ada710..08fa464a35 100644 --- a/lib/teiserver/moderation/tasks/refresh_user_restrictions_task.ex +++ b/lib/teiserver/moderation/tasks/refresh_user_restrictions_task.ex @@ -66,17 +66,21 @@ defmodule Teiserver.Moderation.RefreshUserRestrictionsTask do |> List.flatten() |> Enum.uniq() - new_restricted_until = + expires_dates = actions |> Enum.map(fn a -> a.expires end) - |> List.flatten() - |> Enum.reduce(nil, fn - dt1, nil -> - dt1 + |> Enum.reject(&is_nil/1) - dt1, dt2 -> - if DateHelper.compare(dt1, dt2) == :lt, do: dt1, else: dt2 - end) + new_restricted_until = + case expires_dates do + [] -> + nil + + dates -> + Enum.reduce(dates, fn dt1, dt2 -> + if DateHelper.compare(dt1, dt2) == :lt, do: dt1, else: dt2 + end) + end expires_as_string = new_restricted_until |> Jason.encode!() |> Jason.decode!() diff --git a/lib/teiserver_web/controllers/moderation/action_controller.ex b/lib/teiserver_web/controllers/moderation/action_controller.ex index 63f09fe65b..49de177e66 100644 --- a/lib/teiserver_web/controllers/moderation/action_controller.ex +++ b/lib/teiserver_web/controllers/moderation/action_controller.ex @@ -246,15 +246,10 @@ defmodule TeiserverWeb.Moderation.ActionController do def create(conn, %{"action" => action_params}) do user = Account.get_user(action_params["target_id"]) - expires_string = action_params["expires"] || "" - - expires = - case DateHelper.human_input_to_datetime(expires_string) do - {:ok, expires_datetime} -> - expires_datetime - - nil -> - action_params["expires"] + duration_seconds = + case DateHelper.human_input_to_datetime(action_params["duration"] || "") do + {:ok, future_datetime} -> DateTime.diff(future_datetime, DateTime.utc_now()) + nil -> nil end restrictions = @@ -265,7 +260,8 @@ defmodule TeiserverWeb.Moderation.ActionController do action_params = Map.merge(action_params, %{ "restrictions" => restrictions, - "expires" => expires + "duration" => duration_seconds, + "expires" => nil }) report_ids = @@ -350,6 +346,12 @@ defmodule TeiserverWeb.Moderation.ActionController do def update(conn, %{"id" => id, "action" => action_params}) do action = Moderation.get_action!(id) + duration_seconds = + case DateHelper.human_input_to_datetime(action_params["duration"] || "") do + {:ok, future_datetime} -> DateTime.diff(future_datetime, DateTime.utc_now()) + nil -> nil + end + restrictions = action_params["restrictions"] |> Map.values() @@ -357,7 +359,9 @@ defmodule TeiserverWeb.Moderation.ActionController do action_params = Map.merge(action_params, %{ - "restrictions" => restrictions + "restrictions" => restrictions, + "duration" => duration_seconds, + "expires" => nil }) case Moderation.update_action(action, action_params) do diff --git a/lib/teiserver_web/controllers/moderation/ban_controller.ex b/lib/teiserver_web/controllers/moderation/ban_controller.ex index 438194f7d9..223f5dcdcb 100644 --- a/lib/teiserver_web/controllers/moderation/ban_controller.ex +++ b/lib/teiserver_web/controllers/moderation/ban_controller.ex @@ -203,7 +203,7 @@ defmodule TeiserverWeb.Moderation.BanController do reason: ban.reason, restrictions: ["Login"], score_modifier: 0, - expires: DateTime.shift(DateTime.utc_now(), year: 1000) + duration: 1_000 * 365 * 86_400 }) ActionLib.maybe_create_discord_post(action) diff --git a/lib/teiserver_web/templates/moderation/action/form.html.heex b/lib/teiserver_web/templates/moderation/action/form.html.heex index 859db81163..8c05350589 100644 --- a/lib/teiserver_web/templates/moderation/action/form.html.heex +++ b/lib/teiserver_web/templates/moderation/action/form.html.heex @@ -26,9 +26,18 @@
- {label(f, :expires, class: "control-label")} - {text_input(f, :expires, class: "form-control")} - {error_tag(f, :expires)} + {label(f, :duration, class: "control-label")} + {text_input(f, :duration, class: "form-control", placeholder: "e.g. 7d, 24h, 1m, 1y")} + + Timer starts when the user logs in and acknowledges the action. + Format: s + seconds, h + hours, d + days, m + months, y + years. + + {error_tag(f, :duration)}
diff --git a/lib/teiserver_web/templates/moderation/action/index.html.heex b/lib/teiserver_web/templates/moderation/action/index.html.heex index 6139dde9a9..616d90f920 100644 --- a/lib/teiserver_web/templates/moderation/action/index.html.heex +++ b/lib/teiserver_web/templates/moderation/action/index.html.heex @@ -54,18 +54,18 @@ {action.restrictions |> Enum.join(", ")} {action.inserted_at |> date_to_str(format: :hms_or_ymd)} - <%= if action.expires do %> - <%= if NaiveDateTime.compare(NaiveDateTime.utc_now(), action.expires) == :gt do %> + <%= cond do %> + <% action.expires != nil and NaiveDateTime.compare(NaiveDateTime.utc_now(), action.expires) == :gt -> %> Expired - <% else %> + {duration_to_str(action.inserted_at, action.expires)} + <% action.expires != nil -> %> {action.expires |> date_to_str(format: :hms_or_ymd)} - <% end %> - - {duration_to_str(action.inserted_at, action.expires)} - <% else %> - - Permanent - + {duration_to_str(action.inserted_at, action.expires)} + <% action.duration != nil -> %> + Pending acknowledgement + {format_duration(action.duration)} + <% true -> %> + Permanent <% end %> diff --git a/lib/teiserver_web/templates/moderation/action/tab_details.html.heex b/lib/teiserver_web/templates/moderation/action/tab_details.html.heex index 401ca74d4d..c948158662 100644 --- a/lib/teiserver_web/templates/moderation/action/tab_details.html.heex +++ b/lib/teiserver_web/templates/moderation/action/tab_details.html.heex @@ -30,26 +30,32 @@ {date_to_str(@action.inserted_at, format: :hms_or_ymd)} - <%= if @action.expires do %> - - Expires: + + Duration: + {format_duration(@action.duration)} + - <%= if NaiveDateTime.compare(NaiveDateTime.utc_now(), @action.expires) == :gt do %> - Expired - <% else %> + <%= cond do %> + <% @action.expires != nil and NaiveDateTime.compare(NaiveDateTime.utc_now(), @action.expires) == :gt -> %> + + Expires: + Expired ({duration_to_str(@action.inserted_at, @action.expires)}) + + <% @action.expires != nil -> %> + + Expires: {@action.expires |> date_to_str(format: :hms_or_ymd)} - <% end %> - - - - Duration: - {duration_to_str(@action.inserted_at, @action.expires)} - - <% else %> - - Expires: - Permanent - + + <% @action.duration != nil -> %> + + Expires: + Pending acknowledgement (timer starts on next login) + + <% true -> %> + + Expires: + Permanent + <% end %> diff --git a/lib/teiserver_web/views/moderation/action_view.ex b/lib/teiserver_web/views/moderation/action_view.ex index 57d03750c5..ae0defec59 100644 --- a/lib/teiserver_web/views/moderation/action_view.ex +++ b/lib/teiserver_web/views/moderation/action_view.ex @@ -12,4 +12,17 @@ defmodule TeiserverWeb.Moderation.ActionView do @spec icon() :: String.t() def icon, do: ActionLib.icon() + + @spec format_duration(integer() | nil) :: String.t() + def format_duration(nil), do: "—" + + def format_duration(seconds) do + cond do + seconds >= 86_400 * 365 -> "#{div(seconds, 86_400 * 365)} year(s)" + seconds >= 86_400 * 30 -> "#{div(seconds, 86_400 * 30)} month(s)" + seconds >= 86_400 -> "#{div(seconds, 86_400)} day(s)" + seconds >= 3_600 -> "#{div(seconds, 3_600)} hour(s)" + true -> "#{div(seconds, 60)} minute(s)" + end + end end diff --git a/priv/repo/migrations/20260609103854_add_duration_to_moderation_actions.exs b/priv/repo/migrations/20260609103854_add_duration_to_moderation_actions.exs new file mode 100644 index 0000000000..79193feb11 --- /dev/null +++ b/priv/repo/migrations/20260609103854_add_duration_to_moderation_actions.exs @@ -0,0 +1,10 @@ +defmodule Teiserver.Repo.Migrations.AddDurationToModerationActions do + use Ecto.Migration + + def change do + alter table(:moderation_actions) do + add :duration, :bigint, null: true + modify :expires, :naive_datetime, null: true, from: {:naive_datetime, null: false} + end + end +end diff --git a/test/teiserver/moderation/moderation_test.exs b/test/teiserver/moderation/moderation_test.exs index 1705dbfc4d..6a9724b14e 100644 --- a/test/teiserver/moderation/moderation_test.exs +++ b/test/teiserver/moderation/moderation_test.exs @@ -80,13 +80,13 @@ defmodule Teiserver.ModerationTest do "reason" => "some reason", "restrictions" => ["r1", "r2"], "score_modifier" => "1000", - "expires" => DateTime.utc_now() + "duration" => 7 * 86_400 } @update_attrs %{ "reason" => "some updated reason", "restrictions" => ["u1", "u2"], "score_modifier" => "1500", - "expires" => DateTime.utc_now() + "duration" => 14 * 86_400 } @invalid_attrs %{"reason" => nil} @@ -137,6 +137,56 @@ defmodule Teiserver.ModerationTest do action = ModerationTestLib.action_fixture() assert %Ecto.Changeset{} = Moderation.change_action(action) end + + test "expires is nil until user logs in" do + {:ok, action} = + @valid_attrs + |> Map.merge(%{"target_id" => GeneralTestLib.make_user().id}) + |> Moderation.create_action() + + assert action.duration == 7 * 86_400 + assert is_nil(action.expires) + + # Action should appear in "Pending only" query, not in "Unexpired only" + pending = + Moderation.list_actions(search: [target_id: action.target_id, expiry: "Pending only"]) + + unexpired = + Moderation.list_actions(search: [target_id: action.target_id, expiry: "Unexpired only"]) + + assert Enum.any?(pending, &(&1.id == action.id)) + refute Enum.any?(unexpired, &(&1.id == action.id)) + end + + test "expires is set after user logs in" do + {:ok, action} = + @valid_attrs + |> Map.merge(%{"target_id" => GeneralTestLib.make_user().id}) + |> Moderation.create_action() + + assert is_nil(action.expires) + + # Simulate coordinator login: set expires = now + duration + now = DateTime.utc_now() + expires = DateTime.add(now, action.duration, :second) + {:ok, updated} = Moderation.update_action(action, %{"expires" => expires}) + + refute is_nil(updated.expires) + assert NaiveDateTime.compare(updated.expires, NaiveDateTime.utc_now()) == :gt + + expected = NaiveDateTime.add(NaiveDateTime.utc_now(), action.duration, :second) + assert abs(NaiveDateTime.diff(updated.expires, expected, :second)) < 5 + + # Action should now appear in "Unexpired only", not in "Pending only" + pending = + Moderation.list_actions(search: [target_id: action.target_id, expiry: "Pending only"]) + + unexpired = + Moderation.list_actions(search: [target_id: action.target_id, expiry: "Unexpired only"]) + + refute Enum.any?(pending, &(&1.id == action.id)) + assert Enum.any?(unexpired, &(&1.id == action.id)) + end end describe "bans" do diff --git a/test/teiserver_web/controllers/moderation/action_controller_test.exs b/test/teiserver_web/controllers/moderation/action_controller_test.exs index 0e3868c8ec..45f71aca90 100644 --- a/test/teiserver_web/controllers/moderation/action_controller_test.exs +++ b/test/teiserver_web/controllers/moderation/action_controller_test.exs @@ -16,10 +16,14 @@ defmodule TeiserverWeb.Moderation.ActionControllerTest do @create_attrs %{ reason: "some name", restrictions: %{"Login" => "Login"}, - expires: "1 day", + duration: "1d", score_modifier: "10000" } - @update_attrs %{reason: "some updated name", restrictions: %{"Warning" => "Warning"}} + @update_attrs %{ + reason: "some updated name", + restrictions: %{"Warning" => "Warning"}, + duration: "7d" + } @invalid_attrs %{reason: nil, restrictions: %{}} describe "index" do @@ -160,7 +164,7 @@ defmodule TeiserverWeb.Moderation.ActionControllerTest do describe "halt action" do test "halts chosen action", %{conn: conn} do action = ModerationTestLib.action_fixture() - assert NaiveDateTime.compare(action.expires, NaiveDateTime.utc_now()) == :gt + assert is_nil(action.expires) conn = put(conn, Routes.moderation_action_path(conn, :halt, action.id)) assert redirected_to(conn) == Routes.moderation_action_path(conn, :show, action) From ec3e9c0beb65aa65d5432e5475c124e0db17bbb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Komendarczuk?= Date: Thu, 11 Jun 2026 19:13:37 +0200 Subject: [PATCH 2/6] Fixed expire date after edit action --- lib/teiserver/helpers/date_helper.ex | 3 ++ .../moderation/action_controller.ex | 21 +++++++--- .../moderation/action/form.html.heex | 6 ++- .../moderation/action/index.html.heex | 2 +- .../moderation/action/new_with_user.html.heex | 42 ++++++++++--------- .../moderation/action/tab_details.html.heex | 4 +- .../views/moderation/action_view.ex | 13 ++++++ test/teiserver/helpers/date_helper_test.exs | 18 ++++++++ 8 files changed, 81 insertions(+), 28 deletions(-) diff --git a/lib/teiserver/helpers/date_helper.ex b/lib/teiserver/helpers/date_helper.ex index ab9a9002d5..2a0a9cf8b1 100644 --- a/lib/teiserver/helpers/date_helper.ex +++ b/lib/teiserver/helpers/date_helper.ex @@ -99,6 +99,9 @@ defmodule Teiserver.Helper.DateHelper do :ymd_hms -> Calendar.strftime(the_time, "%Y-%m-%d %I:%M:%S") + :ymd_hms24 -> + Calendar.strftime(the_time, "%Y-%m-%d %H:%M:%S") + :hms -> Calendar.strftime(the_time, "%I:%M:%S") diff --git a/lib/teiserver_web/controllers/moderation/action_controller.ex b/lib/teiserver_web/controllers/moderation/action_controller.ex index 49de177e66..51d631a610 100644 --- a/lib/teiserver_web/controllers/moderation/action_controller.ex +++ b/lib/teiserver_web/controllers/moderation/action_controller.ex @@ -246,9 +246,11 @@ defmodule TeiserverWeb.Moderation.ActionController do def create(conn, %{"action" => action_params}) do user = Account.get_user(action_params["target_id"]) + now = DateTime.utc_now() + duration_seconds = - case DateHelper.human_input_to_datetime(action_params["duration"] || "") do - {:ok, future_datetime} -> DateTime.diff(future_datetime, DateTime.utc_now()) + case DateHelper.human_input_to_datetime(action_params["duration"] || "", now) do + {:ok, future_datetime} -> DateTime.diff(future_datetime, now) nil -> nil end @@ -346,9 +348,11 @@ defmodule TeiserverWeb.Moderation.ActionController do def update(conn, %{"id" => id, "action" => action_params}) do action = Moderation.get_action!(id) + now = DateTime.utc_now() + duration_seconds = - case DateHelper.human_input_to_datetime(action_params["duration"] || "") do - {:ok, future_datetime} -> DateTime.diff(future_datetime, DateTime.utc_now()) + case DateHelper.human_input_to_datetime(action_params["duration"] || "", now) do + {:ok, future_datetime} -> DateTime.diff(future_datetime, now) nil -> nil end @@ -357,11 +361,18 @@ defmodule TeiserverWeb.Moderation.ActionController do |> Map.values() |> Enum.reject(fn v -> v == "false" end) + expires = + if duration_seconds != nil and action.expires != nil do + NaiveDateTime.add(action.inserted_at, duration_seconds, :second) + else + nil + end + action_params = Map.merge(action_params, %{ "restrictions" => restrictions, "duration" => duration_seconds, - "expires" => nil + "expires" => expires }) case Moderation.update_action(action, action_params) do diff --git a/lib/teiserver_web/templates/moderation/action/form.html.heex b/lib/teiserver_web/templates/moderation/action/form.html.heex index 8c05350589..3ca828949d 100644 --- a/lib/teiserver_web/templates/moderation/action/form.html.heex +++ b/lib/teiserver_web/templates/moderation/action/form.html.heex @@ -27,7 +27,11 @@
{label(f, :duration, class: "control-label")} - {text_input(f, :duration, class: "form-control", placeholder: "e.g. 7d, 24h, 1m, 1y")} + {text_input(f, :duration, + class: "form-control", + placeholder: "e.g. 7d, 24h, 1m, 1y", + value: seconds_to_duration_input(Ecto.Changeset.get_field(@changeset, :duration)) + )} Timer starts when the user logs in and acknowledges the action. Format: s diff --git a/lib/teiserver_web/templates/moderation/action/index.html.heex b/lib/teiserver_web/templates/moderation/action/index.html.heex index 616d90f920..6286f839e6 100644 --- a/lib/teiserver_web/templates/moderation/action/index.html.heex +++ b/lib/teiserver_web/templates/moderation/action/index.html.heex @@ -59,7 +59,7 @@ Expired {duration_to_str(action.inserted_at, action.expires)} <% action.expires != nil -> %> - {action.expires |> date_to_str(format: :hms_or_ymd)} + {action.expires |> date_to_str(format: :ymd_hms24)} {duration_to_str(action.inserted_at, action.expires)} <% action.duration != nil -> %> Pending acknowledgement diff --git a/lib/teiserver_web/templates/moderation/action/new_with_user.html.heex b/lib/teiserver_web/templates/moderation/action/new_with_user.html.heex index b4cbdf89ba..cd000894a9 100644 --- a/lib/teiserver_web/templates/moderation/action/new_with_user.html.heex +++ b/lib/teiserver_web/templates/moderation/action/new_with_user.html.heex @@ -83,9 +83,13 @@ bsname = view_colour() %>
- {label(f, :expires, class: "control-label")} - {text_input(f, :expires, class: "form-control", required: "required")} - {error_tag(f, :expires)} + {label(f, :duration, class: "control-label")} + {text_input(f, :duration, + class: "form-control", + required: "required", + placeholder: "e.g. 7d, 24h, 1m, 1y" + )} + {error_tag(f, :duration)}
@@ -231,7 +235,7 @@ bsname = view_colour() %> $('#action_reason').val('[CoC]() B6 - Spamming the main chat channel.'); $('#action_restriction_Room_chat').prop('checked', true); $('#action_restriction_Warning_reminder').prop('checked', true); - $('#action_expires').val('7 days'); + $('#action_duration').val('7 days'); " > #main spam @@ -245,7 +249,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_All_lobbies').prop('checked', true); $('#action_restriction_All_chat').prop('checked', true); - $('#action_expires').val('5 days'); + $('#action_duration').val('5 days'); $('#action_reason').val('[CoC]() C2 - Hate speech, calls for a person to harm themselves, real-life threats, celebration of horrific events are all regarded as unacceptable.'); " > @@ -258,7 +262,7 @@ bsname = view_colour() %> $('#action_restriction_All_lobbies').prop('checked', true); $('#action_restriction_All_chat').prop('checked', true); $('#action_reason').val('[CoC]() B5 - Spectator cheating is completely unacceptable. Given the severity of this, any future attempts at cheating will result in a permanent ban, as will evasion of the suspension.') - $('#action_expires').val('21 days'); + $('#action_duration').val('21 days'); " > Spec cheating @@ -269,7 +273,7 @@ bsname = view_colour() %> $('#action_restriction_All_lobbies').prop('checked', true); $('#action_restriction_All_chat').prop('checked', true); $('#action_reason').val('[CoC]() B5 - Purposefully passing on consequential information to a game in progress via things like spectator chat (we understand mistakes happen occasionally) is obviously a form of cheating.') - $('#action_expires').val('5 days'); + $('#action_duration').val('5 days'); " > Passing Info @@ -279,7 +283,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_Login').prop('checked', true); $('#action_reason').val('Permanent ban.'); - $('#action_expires').val('1100 years'); + $('#action_duration').val('1100 years'); " > One-off account permaban @@ -292,7 +296,7 @@ bsname = view_colour() %> $('#action_restriction_All_chat').prop('checked', true); $('#action_restriction_Note').prop('checked', true); $('#action_reason').val('Permanent ban, possibly a previously banned user.'); - $('#action_expires').val('1100 years'); + $('#action_duration').val('1100 years'); " > One-off account permaban (hidden, for ban evaders) @@ -307,7 +311,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_Warning_reminder').prop('checked', true); $('#action_reason').val('[CoC]() C5 - Evading moderation by creating new accounts is not okay. Continued evasion will lead to a permanent ban.'); - $('#action_expires').val('1 day'); + $('#action_duration').val('1 day'); " > Moderation evasion - 1st offense @@ -318,7 +322,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_Login').prop('checked', true); $('#action_reason').val('[CoC]() C5 - Evading moderation, 2nd offense. Continued evasion will lead to a permanent ban.'); - $('#action_expires').val('1 day'); + $('#action_duration').val('1 day'); " > Moderation evasion - 2nd offense @@ -329,7 +333,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_Login').prop('checked', true); $('#action_reason').val('[CoC]() C5 - Evading moderation, 3rd offense. We may permanently ban the account if you do not stop trying to evade moderation.'); - $('#action_expires').val('3 days'); + $('#action_duration').val('3 days'); " > Moderation evasion - 3rd offense @@ -339,7 +343,7 @@ bsname = view_colour() %> onclick={" $('#action_restriction_Warning_reminder').prop('checked', true); $('#action_reason').val('[CoC]() B5 - Smurf or alt-accounts are not allowed, if you need an exemption or you think there has been an error please use the #open-ticket discord channel to talk to the moderation team. The discord can be accessed at #{discord_link}'); - $('#action_expires').val('1 day'); + $('#action_duration').val('1 day'); "} > Smurfing - 1st offense @@ -350,7 +354,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_Login').prop('checked', true); $('#action_reason').val('[CoC]() B5 - You are continuing to create smurf/alt-accounts. This is against the code of conduct. We may permanently ban you if this continues.'); - $('#action_expires').val('1 day'); + $('#action_duration').val('1 day'); " > Smurfing - 2nd offense @@ -361,7 +365,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_Login').prop('checked', true); $('#action_reason').val('[CoC]() B5 - Smurfing/alt-accounting. 3rd offense, if you do it again we may permanently ban your account.'); - $('#action_expires').val('3 days'); + $('#action_duration').val('3 days'); " > Smurfing - 3rd offense @@ -414,7 +418,7 @@ bsname = view_colour() %> $('#action_restriction_Matchmaking').prop('checked', true); $('#action_restriction_Community').prop('checked', true); $('#action_reason').val('[CoC]() B3 - **Nicknames and clan tags must not be offensive, inappropriate or contain politically charged phrases. Impersonation of other players and real-life figures is also forbidden. Player usernames must not contain references to modern political figures, contemporary movements, or historically controversial figures.** Please rename from `#{@user.name}` (use the Account button in the top right, then go to Recover/Change) to a name not in breach of the Code of Conduct and use the #open-ticket discord channel to notify the moderation team and have the restriction lifted. The discord can be accessed at #{discord_link} '); - $('#action_expires').val('1100 years'); + $('#action_duration').val('1100 years'); "} > Rename @@ -425,7 +429,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_Bridging').prop('checked', true); $('#action_reason').val('Manual unbridging.'); - $('#action_expires').val('1100 years'); + $('#action_duration').val('1100 years'); " > Unbridge @@ -434,7 +438,7 @@ bsname = view_colour() %> class={"btn btn-outline-#{bsname} btn-block"} onclick=" $('#action_restriction_Note').prop('checked', true); - $('#action_expires').val('1s'); + $('#action_duration').val('1s'); " > + internal note @@ -535,7 +539,7 @@ bsname = view_colour() %> <%= if NaiveDateTime.compare(NaiveDateTime.utc_now(), action.expires) == :gt do %> Expired <% else %> - {action.expires |> date_to_str(format: :hms_or_ymd)} + {action.expires |> date_to_str(format: :ymd_hms24)} <% end %> {duration_to_str(action.inserted_at, action.expires)} diff --git a/lib/teiserver_web/templates/moderation/action/tab_details.html.heex b/lib/teiserver_web/templates/moderation/action/tab_details.html.heex index c948158662..9c2c4ceb16 100644 --- a/lib/teiserver_web/templates/moderation/action/tab_details.html.heex +++ b/lib/teiserver_web/templates/moderation/action/tab_details.html.heex @@ -27,7 +27,7 @@ Started: - {date_to_str(@action.inserted_at, format: :hms_or_ymd)} + {date_to_str(@action.inserted_at, format: :ymd_hms24)} @@ -44,7 +44,7 @@ <% @action.expires != nil -> %> Expires: - {@action.expires |> date_to_str(format: :hms_or_ymd)} + {@action.expires |> date_to_str(format: :ymd_hms24)} <% @action.duration != nil -> %> diff --git a/lib/teiserver_web/views/moderation/action_view.ex b/lib/teiserver_web/views/moderation/action_view.ex index ae0defec59..ffe75189f1 100644 --- a/lib/teiserver_web/views/moderation/action_view.ex +++ b/lib/teiserver_web/views/moderation/action_view.ex @@ -25,4 +25,17 @@ defmodule TeiserverWeb.Moderation.ActionView do true -> "#{div(seconds, 60)} minute(s)" end end + + @spec seconds_to_duration_input(integer() | nil) :: String.t() + def seconds_to_duration_input(nil), do: "" + + def seconds_to_duration_input(seconds) do + cond do + rem(seconds, 86_400 * 365) == 0 -> "#{div(seconds, 86_400 * 365)}y" + rem(seconds, 86_400 * 30) == 0 -> "#{div(seconds, 86_400 * 30)}m" + rem(seconds, 86_400) == 0 -> "#{div(seconds, 86_400)}d" + rem(seconds, 3_600) == 0 -> "#{div(seconds, 3_600)}h" + true -> "#{seconds}s" + end + end end diff --git a/test/teiserver/helpers/date_helper_test.exs b/test/teiserver/helpers/date_helper_test.exs index 65a7b7e178..35be9e0f7f 100644 --- a/test/teiserver/helpers/date_helper_test.exs +++ b/test/teiserver/helpers/date_helper_test.exs @@ -305,6 +305,24 @@ defmodule Teiserver.Helpers.DateHelperTest do end end + describe "date_to_str :ymd_hms24" do + test "formats date and time in 24-hour notation" do + dt = ~U[2026-06-18 15:45:40Z] + assert DateHelper.date_to_str(dt, format: :ymd_hms24) == "2026-06-18 15:45:40" + end + + test "midnight is 00, not 12" do + dt = ~U[2026-06-18 00:00:00Z] + assert DateHelper.date_to_str(dt, format: :ymd_hms24) == "2026-06-18 00:00:00" + end + + test "does not use 12-hour AM/PM format" do + dt = ~U[2026-06-18 15:45:40Z] + result = DateHelper.date_to_str(dt, format: :ymd_hms24) + refute result == "2026-06-18 03:45:40" + end + end + describe "human_input_to_datetime" do @now ~U[2026-05-19 09:00:00Z] From b8bce268e09fed4e39794ac3646b91336c641297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Komendarczuk?= Date: Thu, 11 Jun 2026 19:56:30 +0200 Subject: [PATCH 3/6] Fix credo --- test/teiserver/moderation/moderation_test.exs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/teiserver/moderation/moderation_test.exs b/test/teiserver/moderation/moderation_test.exs index 6a9724b14e..8a0e8abe84 100644 --- a/test/teiserver/moderation/moderation_test.exs +++ b/test/teiserver/moderation/moderation_test.exs @@ -171,11 +171,8 @@ defmodule Teiserver.ModerationTest do expires = DateTime.add(now, action.duration, :second) {:ok, updated} = Moderation.update_action(action, %{"expires" => expires}) - refute is_nil(updated.expires) - assert NaiveDateTime.compare(updated.expires, NaiveDateTime.utc_now()) == :gt - - expected = NaiveDateTime.add(NaiveDateTime.utc_now(), action.duration, :second) - assert abs(NaiveDateTime.diff(updated.expires, expected, :second)) < 5 + expected_expires = expires |> DateTime.to_naive() |> NaiveDateTime.truncate(:second) + assert updated.expires == expected_expires # Action should now appear in "Unexpired only", not in "Pending only" pending = From ecbf392dd1336de063f2e42ecc540f99888c3164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Komendarczuk?= Date: Thu, 11 Jun 2026 20:11:10 +0200 Subject: [PATCH 4/6] fix test --- lib/teiserver/moderation/schemas/action.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/teiserver/moderation/schemas/action.ex b/lib/teiserver/moderation/schemas/action.ex index 87f06b1325..0cef629d9f 100644 --- a/lib/teiserver/moderation/schemas/action.ex +++ b/lib/teiserver/moderation/schemas/action.ex @@ -33,7 +33,7 @@ defmodule Teiserver.Moderation.Action do params, ~w(target_id reason restrictions score_modifier expires duration notes hidden discord_message_id appeal_status)a ) - |> validate_required(~w(target_id reason restrictions duration score_modifier)a) + |> validate_required(~w(target_id reason restrictions score_modifier)a) |> adjust_restrictions() |> validate_length(:restrictions, min: 1) end From 22bc8118583eb055ee0a54c085efba8eee1c8a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Komendarczuk?= Date: Mon, 15 Jun 2026 11:04:30 +0200 Subject: [PATCH 5/6] Fixes from review. Refactor --- .../coordinator/coordinator_server.ex | 46 +++++++++++++++---- lib/teiserver/helpers/date_helper.ex | 34 +++++++++++++- lib/teiserver/moderation/schemas/action.ex | 10 ++-- .../moderation/action_controller.ex | 18 ++------ .../controllers/moderation/ban_controller.ex | 2 +- .../moderation/action/form.html.heex | 1 + .../moderation/action/index.html.heex | 16 +------ .../moderation/action/new_with_user.html.heex | 35 +++++++------- .../views/moderation/action_view.ex | 20 +++++++- 9 files changed, 114 insertions(+), 68 deletions(-) diff --git a/lib/teiserver/coordinator/coordinator_server.ex b/lib/teiserver/coordinator/coordinator_server.ex index de826ce83e..229f094270 100644 --- a/lib/teiserver/coordinator/coordinator_server.ex +++ b/lib/teiserver/coordinator/coordinator_server.ex @@ -159,6 +159,7 @@ defmodule Teiserver.Coordinator.CoordinatorServer do case converted_message do ^warning_response -> Client.clear_awaiting_warn_ack(userid) + activate_warning_actions(userid) CacheUser.send_direct_message(state.userid, userid, "Thank you") _other_message -> @@ -235,19 +236,16 @@ defmodule Teiserver.Coordinator.CoordinatorServer do ] ) + {login_activated, _warning_pending} = + Enum.split_with(pending_actions, fn action -> + not Enum.member?(action.restrictions, "Warning reminder") + end) + db_user = - if Enum.empty?(pending_actions) do + if Enum.empty?(login_activated) do db_user else - now = DateTime.utc_now() - - Enum.each(pending_actions, fn action -> - if action.duration do - expires = DateTime.add(now, action.duration, :second) - Moderation.update_action(action, %{"expires" => expires}) - end - end) - + activate_actions(login_activated) RefreshUserRestrictionsTask.refresh_user(userid) Account.get_user(userid) end @@ -330,6 +328,34 @@ defmodule Teiserver.Coordinator.CoordinatorServer do {:noreply, state} end + defp activate_warning_actions(userid) do + pending_warnings = + Moderation.list_actions( + search: [ + target_id: userid, + expiry: "Pending only", + in_restrictions: "Warning reminder" + ] + ) + + activate_actions(pending_warnings) + + if not Enum.empty?(pending_warnings) do + RefreshUserRestrictionsTask.refresh_user(userid) + end + end + + defp activate_actions(actions) do + now = DateTime.utc_now() + + Enum.each(actions, fn action -> + if action.duration do + expires = DateTime.add(now, action.duration, :second) + Moderation.update_action(action, %{"expires" => expires}) + end + end) + end + # Special debugging to see what is being sent def handle_info({:timeout, duration}, state) do :timer.sleep(duration) diff --git a/lib/teiserver/helpers/date_helper.ex b/lib/teiserver/helpers/date_helper.ex index 2a0a9cf8b1..e937abb52c 100644 --- a/lib/teiserver/helpers/date_helper.ex +++ b/lib/teiserver/helpers/date_helper.ex @@ -406,7 +406,11 @@ defmodule Teiserver.Helper.DateHelper do @human_input_regex ~r/([1-9][0-9]*?)\s?(s|seconds?|h|hours?|d|days?|m|months?|y|years?)/ @spec human_input_to_datetime(String.t(), DateTime.t() | nil) :: {:ok, DateTime.t()} | nil - def human_input_to_datetime(human_input, now \\ nil) do + def human_input_to_datetime(human_input, now \\ nil) + def human_input_to_datetime(nil, _now), do: nil + def human_input_to_datetime("", _now), do: nil + + def human_input_to_datetime(human_input, now) do now = now || DateTime.utc_now() human_input = String.downcase(human_input) @@ -430,4 +434,32 @@ defmodule Teiserver.Helper.DateHelper do nil end end + + @doc """ + Converts human input like "7d" directly to seconds without going through DateTime. + """ + @spec human_input_to_seconds(String.t() | nil) :: integer() | nil + def human_input_to_seconds(nil), do: nil + def human_input_to_seconds(""), do: nil + + def human_input_to_seconds(human_input) do + human_input = String.downcase(human_input) + + case Regex.run(@human_input_regex, human_input) do + [_full, count, unit] -> + count = String.to_integer(count) + unit = String.first(unit) + + case unit do + "s" -> count + "h" -> count * 3_600 + "d" -> count * 86_400 + "m" -> count * 30 * 86_400 + "y" -> count * 365 * 86_400 + end + + _any_other -> + nil + end + end end diff --git a/lib/teiserver/moderation/schemas/action.ex b/lib/teiserver/moderation/schemas/action.ex index 0cef629d9f..2ce4f2d3fe 100644 --- a/lib/teiserver/moderation/schemas/action.ex +++ b/lib/teiserver/moderation/schemas/action.ex @@ -38,19 +38,15 @@ defmodule Teiserver.Moderation.Action do |> validate_length(:restrictions, min: 1) end - # 10 years in seconds - @permanent_threshold_seconds 10 * 365 * 24 * 3_600 - defp adjust_restrictions(%Ecto.Changeset{} = struct) do duration = Changeset.get_field(struct, :duration) - inbound_restrictions = Changeset.get_field(struct, :restrictions, []) + inbound_restrictions = Changeset.get_field(struct, :restrictions) || [] new_restrictions = - if duration && duration >= @permanent_threshold_seconds && - Enum.member?(inbound_restrictions, "Login") do + if is_nil(duration) and Enum.member?(inbound_restrictions, "Login") do ["Permanently banned" | inbound_restrictions] |> Enum.uniq() else - (inbound_restrictions || []) |> List.delete("Permanently banned") + List.delete(inbound_restrictions, "Permanently banned") end Changeset.put_change(struct, :restrictions, new_restrictions) diff --git a/lib/teiserver_web/controllers/moderation/action_controller.ex b/lib/teiserver_web/controllers/moderation/action_controller.ex index 51d631a610..41d168bc73 100644 --- a/lib/teiserver_web/controllers/moderation/action_controller.ex +++ b/lib/teiserver_web/controllers/moderation/action_controller.ex @@ -246,13 +246,7 @@ defmodule TeiserverWeb.Moderation.ActionController do def create(conn, %{"action" => action_params}) do user = Account.get_user(action_params["target_id"]) - now = DateTime.utc_now() - - duration_seconds = - case DateHelper.human_input_to_datetime(action_params["duration"] || "", now) do - {:ok, future_datetime} -> DateTime.diff(future_datetime, now) - nil -> nil - end + duration_seconds = DateHelper.human_input_to_seconds(action_params["duration"]) restrictions = action_params["restrictions"] @@ -348,13 +342,7 @@ defmodule TeiserverWeb.Moderation.ActionController do def update(conn, %{"id" => id, "action" => action_params}) do action = Moderation.get_action!(id) - now = DateTime.utc_now() - - duration_seconds = - case DateHelper.human_input_to_datetime(action_params["duration"] || "", now) do - {:ok, future_datetime} -> DateTime.diff(future_datetime, now) - nil -> nil - end + duration_seconds = DateHelper.human_input_to_seconds(action_params["duration"]) restrictions = action_params["restrictions"] @@ -363,7 +351,7 @@ defmodule TeiserverWeb.Moderation.ActionController do expires = if duration_seconds != nil and action.expires != nil do - NaiveDateTime.add(action.inserted_at, duration_seconds, :second) + NaiveDateTime.add(NaiveDateTime.utc_now(), duration_seconds, :second) else nil end diff --git a/lib/teiserver_web/controllers/moderation/ban_controller.ex b/lib/teiserver_web/controllers/moderation/ban_controller.ex index 223f5dcdcb..ff4e213368 100644 --- a/lib/teiserver_web/controllers/moderation/ban_controller.ex +++ b/lib/teiserver_web/controllers/moderation/ban_controller.ex @@ -203,7 +203,7 @@ defmodule TeiserverWeb.Moderation.BanController do reason: ban.reason, restrictions: ["Login"], score_modifier: 0, - duration: 1_000 * 365 * 86_400 + duration: nil }) ActionLib.maybe_create_discord_post(action) diff --git a/lib/teiserver_web/templates/moderation/action/form.html.heex b/lib/teiserver_web/templates/moderation/action/form.html.heex index 3ca828949d..c1ffe1e83c 100644 --- a/lib/teiserver_web/templates/moderation/action/form.html.heex +++ b/lib/teiserver_web/templates/moderation/action/form.html.heex @@ -34,6 +34,7 @@ )} Timer starts when the user logs in and acknowledges the action. + Leave empty for a permanent ban. Format: s seconds, h hours, d diff --git a/lib/teiserver_web/templates/moderation/action/index.html.heex b/lib/teiserver_web/templates/moderation/action/index.html.heex index 6286f839e6..dd7c2d7bd4 100644 --- a/lib/teiserver_web/templates/moderation/action/index.html.heex +++ b/lib/teiserver_web/templates/moderation/action/index.html.heex @@ -53,20 +53,8 @@ {action.reason |> String.slice(0..100)} {action.restrictions |> Enum.join(", ")} - {action.inserted_at |> date_to_str(format: :hms_or_ymd)} - <%= cond do %> - <% action.expires != nil and NaiveDateTime.compare(NaiveDateTime.utc_now(), action.expires) == :gt -> %> - Expired - {duration_to_str(action.inserted_at, action.expires)} - <% action.expires != nil -> %> - {action.expires |> date_to_str(format: :ymd_hms24)} - {duration_to_str(action.inserted_at, action.expires)} - <% action.duration != nil -> %> - Pending acknowledgement - {format_duration(action.duration)} - <% true -> %> - Permanent - <% end %> + {action.inserted_at |> date_to_str(format: :ymd_hms24)} + <.action_status action={action} /> diff --git a/lib/teiserver_web/templates/moderation/action/new_with_user.html.heex b/lib/teiserver_web/templates/moderation/action/new_with_user.html.heex index cd000894a9..58a1099929 100644 --- a/lib/teiserver_web/templates/moderation/action/new_with_user.html.heex +++ b/lib/teiserver_web/templates/moderation/action/new_with_user.html.heex @@ -86,9 +86,18 @@ bsname = view_colour() %> {label(f, :duration, class: "control-label")} {text_input(f, :duration, class: "form-control", - required: "required", placeholder: "e.g. 7d, 24h, 1m, 1y" )} + + Timer starts when the user logs in and acknowledges the action. + Leave empty for a permanent ban. + Format: s + seconds, h + hours, d + days, m + months, y + years. + {error_tag(f, :duration)}
@@ -283,7 +292,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_Login').prop('checked', true); $('#action_reason').val('Permanent ban.'); - $('#action_duration').val('1100 years'); + $('#action_duration').val(''); " > One-off account permaban @@ -296,7 +305,7 @@ bsname = view_colour() %> $('#action_restriction_All_chat').prop('checked', true); $('#action_restriction_Note').prop('checked', true); $('#action_reason').val('Permanent ban, possibly a previously banned user.'); - $('#action_duration').val('1100 years'); + $('#action_duration').val(''); " > One-off account permaban (hidden, for ban evaders) @@ -418,7 +427,7 @@ bsname = view_colour() %> $('#action_restriction_Matchmaking').prop('checked', true); $('#action_restriction_Community').prop('checked', true); $('#action_reason').val('[CoC]() B3 - **Nicknames and clan tags must not be offensive, inappropriate or contain politically charged phrases. Impersonation of other players and real-life figures is also forbidden. Player usernames must not contain references to modern political figures, contemporary movements, or historically controversial figures.** Please rename from `#{@user.name}` (use the Account button in the top right, then go to Recover/Change) to a name not in breach of the Code of Conduct and use the #open-ticket discord channel to notify the moderation team and have the restriction lifted. The discord can be accessed at #{discord_link} '); - $('#action_duration').val('1100 years'); + $('#action_duration').val(''); "} > Rename @@ -429,7 +438,7 @@ bsname = view_colour() %> onclick=" $('#action_restriction_Bridging').prop('checked', true); $('#action_reason').val('Manual unbridging.'); - $('#action_duration').val('1100 years'); + $('#action_duration').val(''); " > Unbridge @@ -534,20 +543,8 @@ bsname = view_colour() %> {action.notes && action.notes |> String.slice(0..100)} {action.restrictions |> Enum.join(", ")} - {action.inserted_at |> date_to_str(format: :hms_or_ymd)} - <%= if action.expires do %> - <%= if NaiveDateTime.compare(NaiveDateTime.utc_now(), action.expires) == :gt do %> - Expired - <% else %> - {action.expires |> date_to_str(format: :ymd_hms24)} - <% end %> - - {duration_to_str(action.inserted_at, action.expires)} - <% else %> - - Permanent - - <% end %> + {action.inserted_at |> date_to_str(format: :ymd_hms24)} + <.action_status action={action} /> + <% @action.expires != nil and NaiveDateTime.compare(NaiveDateTime.utc_now(), @action.expires) == :gt -> %> + Expired + {duration_to_str(@action.inserted_at, @action.expires)} + <% @action.expires != nil -> %> + {@action.expires |> date_to_str(format: :ymd_hms24)} + {duration_to_str(@action.inserted_at, @action.expires)} + <% @action.duration != nil -> %> + Pending acknowledgement + {format_duration(@action.duration)} + <% true -> %> + Permanent + <% end %> + """ + end + @spec format_duration(integer() | nil) :: String.t() - def format_duration(nil), do: "—" + def format_duration(nil), do: "Permanent" def format_duration(seconds) do cond do From 17e12a81cb0f8d1ade1764572d991e98c4034dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Komendarczuk?= Date: Mon, 15 Jun 2026 11:08:36 +0200 Subject: [PATCH 6/6] Fix compile --- .../coordinator/coordinator_server.ex | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/teiserver/coordinator/coordinator_server.ex b/lib/teiserver/coordinator/coordinator_server.ex index 229f094270..6a44a40689 100644 --- a/lib/teiserver/coordinator/coordinator_server.ex +++ b/lib/teiserver/coordinator/coordinator_server.ex @@ -328,6 +328,21 @@ defmodule Teiserver.Coordinator.CoordinatorServer do {:noreply, state} end + # Special debugging to see what is being sent + def handle_info({:timeout, duration}, state) do + :timer.sleep(duration) + {:noreply, state} + end + + # Catchall handle_info + def handle_info(msg, state) do + Logger.error( + "CoordinatorServer handle_info error. No handler for msg of #{Kernel.inspect(msg)}" + ) + + {:noreply, state} + end + defp activate_warning_actions(userid) do pending_warnings = Moderation.list_actions( @@ -356,21 +371,6 @@ defmodule Teiserver.Coordinator.CoordinatorServer do end) end - # Special debugging to see what is being sent - def handle_info({:timeout, duration}, state) do - :timer.sleep(duration) - {:noreply, state} - end - - # Catchall handle_info - def handle_info(msg, state) do - Logger.error( - "CoordinatorServer handle_info error. No handler for msg of #{Kernel.inspect(msg)}" - ) - - {:noreply, state} - end - def make_and_cache_coordinator_account do account = get_coordinator_account() Teiserver.cache_put(:application_metadata_cache, "teiserver_coordinator_userid", account.id)