From 5bf697707349d4843de26116b2d04b30eeb94840 Mon Sep 17 00:00:00 2001 From: timujeen Date: Wed, 18 Feb 2026 06:35:34 +0000 Subject: [PATCH 1/3] Fix register_groups to convert plain maps to Group structs The handle_call for register_groups inserted groups directly into ETS without converting plain maps to Group structs. This could crash sidebar templates that access group fields with dot syntax when parent apps pass plain maps via the public register_groups/1 API. --- lib/phoenix_kit/dashboard/registry.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/phoenix_kit/dashboard/registry.ex b/lib/phoenix_kit/dashboard/registry.ex index 90756f89..ec6927b2 100644 --- a/lib/phoenix_kit/dashboard/registry.ex +++ b/lib/phoenix_kit/dashboard/registry.ex @@ -540,7 +540,13 @@ defmodule PhoenixKit.Dashboard.Registry do @impl true def handle_call({:register_groups, groups}, _from, state) do - :ets.insert(@ets_table, {:groups, groups}) + converted = + Enum.map(groups, fn + %Group{} = g -> g + map when is_map(map) -> Group.new(map) + end) + + :ets.insert(@ets_table, {:groups, converted}) broadcast_refresh() {:reply, :ok, state} end From 4e0243489ab7830f7c9568fce8785f0d929194cc Mon Sep 17 00:00:00 2001 From: timujeen Date: Wed, 18 Feb 2026 07:07:52 +0000 Subject: [PATCH 2/3] Fix DateTime.utc_now() microsecond crashes after utc_datetime migration PR #347 changed all schema fields from :utc_datetime_usec to :utc_datetime but did not add DateTime.truncate(:second) to all DateTime.utc_now() calls in contexts and schemas. This caused ArgumentError crashes on any DB write operation. Fixed 19 files across settings, billing, shop, emails, referrals, tickets, comments, scheduled jobs, auth, permissions, and roles. --- lib/modules/billing/schemas/invoice.ex | 6 +++--- lib/modules/billing/schemas/order.ex | 15 +++++++++++---- lib/modules/comments/comments.ex | 4 +++- lib/modules/emails/event.ex | 2 +- lib/modules/emails/log.ex | 2 +- lib/modules/emails/templates.ex | 2 +- lib/modules/referrals/referrals.ex | 2 +- .../referrals/schemas/referral_code_usage.ex | 2 +- lib/modules/shop/shop.ex | 12 +++++++++--- lib/modules/tickets/tickets.ex | 15 +++++++++++---- lib/phoenix_kit/scheduled_jobs.ex | 4 +++- lib/phoenix_kit/scheduled_jobs/scheduled_job.ex | 2 +- lib/phoenix_kit/settings/setting.ex | 6 +++--- lib/phoenix_kit/users/auth.ex | 16 ++++++++++------ lib/phoenix_kit/users/auth/user.ex | 2 +- lib/phoenix_kit/users/magic_link_registration.ex | 2 +- lib/phoenix_kit/users/permissions.ex | 2 +- lib/phoenix_kit/users/role_assignment.ex | 2 +- lib/phoenix_kit/users/roles.ex | 2 +- 19 files changed, 64 insertions(+), 36 deletions(-) diff --git a/lib/modules/billing/schemas/invoice.ex b/lib/modules/billing/schemas/invoice.ex index 12c6a5ca..f512a228 100644 --- a/lib/modules/billing/schemas/invoice.ex +++ b/lib/modules/billing/schemas/invoice.ex @@ -161,9 +161,9 @@ defmodule PhoenixKit.Modules.Billing.Invoice do |> validate_status_transition(invoice.status, new_status) case new_status do - "sent" -> put_change(changeset, :sent_at, DateTime.utc_now()) - "paid" -> put_change(changeset, :paid_at, DateTime.utc_now()) - "void" -> put_change(changeset, :voided_at, DateTime.utc_now()) + "sent" -> put_change(changeset, :sent_at, DateTime.truncate(DateTime.utc_now(), :second)) + "paid" -> put_change(changeset, :paid_at, DateTime.truncate(DateTime.utc_now(), :second)) + "void" -> put_change(changeset, :voided_at, DateTime.truncate(DateTime.utc_now(), :second)) _ -> changeset end end diff --git a/lib/modules/billing/schemas/order.ex b/lib/modules/billing/schemas/order.ex index 1a6681bd..55f267e7 100644 --- a/lib/modules/billing/schemas/order.ex +++ b/lib/modules/billing/schemas/order.ex @@ -214,10 +214,17 @@ defmodule PhoenixKit.Modules.Billing.Order do |> validate_status_transition(order.status, new_status) case new_status do - "confirmed" -> put_change(changeset, :confirmed_at, DateTime.utc_now()) - "paid" -> put_change(changeset, :paid_at, DateTime.utc_now()) - "cancelled" -> put_change(changeset, :cancelled_at, DateTime.utc_now()) - _ -> changeset + "confirmed" -> + put_change(changeset, :confirmed_at, DateTime.truncate(DateTime.utc_now(), :second)) + + "paid" -> + put_change(changeset, :paid_at, DateTime.truncate(DateTime.utc_now(), :second)) + + "cancelled" -> + put_change(changeset, :cancelled_at, DateTime.truncate(DateTime.utc_now(), :second)) + + _ -> + changeset end end diff --git a/lib/modules/comments/comments.ex b/lib/modules/comments/comments.ex index 28fc1ae9..ac58b962 100644 --- a/lib/modules/comments/comments.ex +++ b/lib/modules/comments/comments.ex @@ -304,7 +304,9 @@ defmodule PhoenixKit.Modules.Comments do def bulk_update_status(comment_ids, status) when is_list(comment_ids) and status in ["published", "hidden", "deleted", "pending"] do from(c in Comment, where: c.uuid in ^comment_ids) - |> repo().update_all(set: [status: status, updated_at: DateTime.utc_now()]) + |> repo().update_all( + set: [status: status, updated_at: DateTime.truncate(DateTime.utc_now(), :second)] + ) end @doc """ diff --git a/lib/modules/emails/event.ex b/lib/modules/emails/event.ex index daa33782..79e0e6ad 100644 --- a/lib/modules/emails/event.ex +++ b/lib/modules/emails/event.ex @@ -759,7 +759,7 @@ defmodule PhoenixKit.Modules.Emails.Event do # Set occurred_at if not provided defp maybe_set_occurred_at(changeset) do case get_field(changeset, :occurred_at) do - nil -> put_change(changeset, :occurred_at, DateTime.utc_now()) + nil -> put_change(changeset, :occurred_at, DateTime.truncate(DateTime.utc_now(), :second)) _ -> changeset end end diff --git a/lib/modules/emails/log.ex b/lib/modules/emails/log.ex index 7bca0ef1..1c934ddf 100644 --- a/lib/modules/emails/log.ex +++ b/lib/modules/emails/log.ex @@ -1217,7 +1217,7 @@ defmodule PhoenixKit.Modules.Emails.Log do # Set queued_at if not provided defp maybe_set_queued_at(changeset) do case get_field(changeset, :queued_at) do - nil -> put_change(changeset, :queued_at, DateTime.utc_now()) + nil -> put_change(changeset, :queued_at, DateTime.truncate(DateTime.utc_now(), :second)) _ -> changeset end end diff --git a/lib/modules/emails/templates.ex b/lib/modules/emails/templates.ex index 9431d6d6..c61ae8a4 100644 --- a/lib/modules/emails/templates.ex +++ b/lib/modules/emails/templates.ex @@ -480,7 +480,7 @@ defmodule PhoenixKit.Modules.Emails.Templates do template |> Template.usage_changeset(%{ usage_count: template.usage_count + 1, - last_used_at: DateTime.utc_now() + last_used_at: DateTime.truncate(DateTime.utc_now(), :second) }) |> repo().update() end diff --git a/lib/modules/referrals/referrals.ex b/lib/modules/referrals/referrals.ex index 8c7d6413..8e826ab1 100644 --- a/lib/modules/referrals/referrals.ex +++ b/lib/modules/referrals/referrals.ex @@ -757,7 +757,7 @@ defmodule PhoenixKit.Modules.Referrals do defp maybe_set_date_created(changeset) do if changeset.data.__meta__.state == :built do - put_change(changeset, :date_created, DateTime.utc_now()) + put_change(changeset, :date_created, DateTime.truncate(DateTime.utc_now(), :second)) else changeset end diff --git a/lib/modules/referrals/schemas/referral_code_usage.ex b/lib/modules/referrals/schemas/referral_code_usage.ex index 822ee424..db88e75e 100644 --- a/lib/modules/referrals/schemas/referral_code_usage.ex +++ b/lib/modules/referrals/schemas/referral_code_usage.ex @@ -217,7 +217,7 @@ defmodule PhoenixKit.Modules.Referrals.ReferralCodeUsage do # Private helper to set date_used on new records defp maybe_set_date_used(changeset) do if changeset.data.__meta__.state == :built do - put_change(changeset, :date_used, DateTime.utc_now()) + put_change(changeset, :date_used, DateTime.truncate(DateTime.utc_now(), :second)) else changeset end diff --git a/lib/modules/shop/shop.ex b/lib/modules/shop/shop.ex index d00543cf..ed1a4076 100644 --- a/lib/modules/shop/shop.ex +++ b/lib/modules/shop/shop.ex @@ -539,7 +539,9 @@ defmodule PhoenixKit.Modules.Shop do {count, _} = query - |> repo().update_all(set: [status: status, updated_at: DateTime.utc_now()]) + |> repo().update_all( + set: [status: status, updated_at: DateTime.truncate(DateTime.utc_now(), :second)] + ) if count > 0 do Events.broadcast_products_bulk_status_changed(ids, status) @@ -963,7 +965,9 @@ defmodule PhoenixKit.Modules.Shop do {count, _} = query - |> repo().update_all(set: [status: status, updated_at: DateTime.utc_now()]) + |> repo().update_all( + set: [status: status, updated_at: DateTime.truncate(DateTime.utc_now(), :second)] + ) if count > 0 do Events.broadcast_categories_bulk_status_changed(ids, status) @@ -2385,7 +2389,9 @@ defmodule PhoenixKit.Modules.Shop do {count, _} = Cart |> where([c], c.id == ^cart_id and c.status == "active") - |> repo().update_all(set: [status: "converting", updated_at: DateTime.utc_now()]) + |> repo().update_all( + set: [status: "converting", updated_at: DateTime.truncate(DateTime.utc_now(), :second)] + ) if count == 1 do # Successfully locked - reload cart with new status diff --git a/lib/modules/tickets/tickets.ex b/lib/modules/tickets/tickets.ex index f3cd2ac1..9b26076a 100644 --- a/lib/modules/tickets/tickets.ex +++ b/lib/modules/tickets/tickets.ex @@ -586,10 +586,17 @@ defmodule PhoenixKit.Modules.Tickets do # Set timestamps based on new status attrs = case new_status do - "resolved" -> Map.put(attrs, :resolved_at, DateTime.utc_now()) - "closed" -> Map.put(attrs, :closed_at, DateTime.utc_now()) - "open" -> Map.merge(attrs, %{resolved_at: nil, closed_at: nil}) - _ -> attrs + "resolved" -> + Map.put(attrs, :resolved_at, DateTime.truncate(DateTime.utc_now(), :second)) + + "closed" -> + Map.put(attrs, :closed_at, DateTime.truncate(DateTime.utc_now(), :second)) + + "open" -> + Map.merge(attrs, %{resolved_at: nil, closed_at: nil}) + + _ -> + attrs end # Use raw update to avoid double broadcast from update_ticket diff --git a/lib/phoenix_kit/scheduled_jobs.ex b/lib/phoenix_kit/scheduled_jobs.ex index f82d4cfa..84668327 100644 --- a/lib/phoenix_kit/scheduled_jobs.ex +++ b/lib/phoenix_kit/scheduled_jobs.ex @@ -130,7 +130,9 @@ defmodule PhoenixKit.ScheduledJobs do where: j.resource_id == ^resource_id, where: j.status == "pending" ) - |> repo().update_all(set: [status: "cancelled", updated_at: DateTime.utc_now()]) + |> repo().update_all( + set: [status: "cancelled", updated_at: DateTime.truncate(DateTime.utc_now(), :second)] + ) end @doc """ diff --git a/lib/phoenix_kit/scheduled_jobs/scheduled_job.ex b/lib/phoenix_kit/scheduled_jobs/scheduled_job.ex index ef6e77ff..4ff6a957 100644 --- a/lib/phoenix_kit/scheduled_jobs/scheduled_job.ex +++ b/lib/phoenix_kit/scheduled_jobs/scheduled_job.ex @@ -84,7 +84,7 @@ defmodule PhoenixKit.ScheduledJobs.ScheduledJob do scheduled_job |> change(%{ status: "executed", - executed_at: DateTime.utc_now() + executed_at: DateTime.truncate(DateTime.utc_now(), :second) }) end diff --git a/lib/phoenix_kit/settings/setting.ex b/lib/phoenix_kit/settings/setting.ex index f9f69d3c..f79fcf2d 100644 --- a/lib/phoenix_kit/settings/setting.ex +++ b/lib/phoenix_kit/settings/setting.ex @@ -122,21 +122,21 @@ defmodule PhoenixKit.Settings.Setting do |> validate_setting_value() |> validate_value_exclusivity() |> validate_length(:module, max: 255) - |> put_change(:date_updated, DateTime.utc_now()) + |> put_change(:date_updated, DateTime.truncate(DateTime.utc_now(), :second)) end # Private helper to set timestamps on new records defp maybe_set_timestamps(changeset) do case changeset.data.uuid do nil -> - now = DateTime.utc_now() + now = DateTime.truncate(DateTime.utc_now(), :second) changeset |> put_change(:date_added, now) |> put_change(:date_updated, now) _uuid -> - put_change(changeset, :date_updated, DateTime.utc_now()) + put_change(changeset, :date_updated, DateTime.truncate(DateTime.utc_now(), :second)) end end diff --git a/lib/phoenix_kit/users/auth.ex b/lib/phoenix_kit/users/auth.ex index d1e9b940..6df939d5 100644 --- a/lib/phoenix_kit/users/auth.ex +++ b/lib/phoenix_kit/users/auth.ex @@ -2447,7 +2447,11 @@ defmodule PhoenixKit.Users.Auth do from(o in module, where: ^dynamic_query) |> Repo.repo().update_all( - set: [user_id: nil, user_uuid: nil, anonymized_at: DateTime.utc_now()] + set: [ + user_id: nil, + user_uuid: nil, + anonymized_at: DateTime.truncate(DateTime.utc_now(), :second) + ] ) |> elem(0) else @@ -2470,7 +2474,7 @@ defmodule PhoenixKit.Users.Auth do user_id: nil, user_uuid: nil, author_deleted: true, - anonymized_at: DateTime.utc_now() + anonymized_at: DateTime.truncate(DateTime.utc_now(), :second) ] ) |> elem(0) @@ -2504,7 +2508,7 @@ defmodule PhoenixKit.Users.Auth do user_id: nil, user_uuid: nil, author_deleted: true, - anonymized_at: DateTime.utc_now() + anonymized_at: DateTime.truncate(DateTime.utc_now(), :second) ] ) |> elem(0) @@ -2543,7 +2547,7 @@ defmodule PhoenixKit.Users.Auth do set: [ user_id: nil, user_uuid: nil, - anonymized_at: DateTime.utc_now(), + anonymized_at: DateTime.truncate(DateTime.utc_now(), :second), original_user_email: nil ] ) @@ -2568,7 +2572,7 @@ defmodule PhoenixKit.Users.Auth do set: [ user_id: nil, user_uuid: nil, - anonymized_at: DateTime.utc_now() + anonymized_at: DateTime.truncate(DateTime.utc_now(), :second) ] ) |> elem(0) @@ -2591,7 +2595,7 @@ defmodule PhoenixKit.Users.Auth do set: [ user_id: nil, user_uuid: nil, - anonymized_at: DateTime.utc_now() + anonymized_at: DateTime.truncate(DateTime.utc_now(), :second) ] ) |> elem(0) diff --git a/lib/phoenix_kit/users/auth/user.ex b/lib/phoenix_kit/users/auth/user.ex index 9bbb2714..ca214cc7 100644 --- a/lib/phoenix_kit/users/auth/user.ex +++ b/lib/phoenix_kit/users/auth/user.ex @@ -319,7 +319,7 @@ defmodule PhoenixKit.Users.Auth.User do Confirms the account by setting `confirmed_at`. """ def confirm_changeset(user) do - now = DateTime.utc_now() + now = DateTime.truncate(DateTime.utc_now(), :second) change(user, confirmed_at: now) end diff --git a/lib/phoenix_kit/users/magic_link_registration.ex b/lib/phoenix_kit/users/magic_link_registration.ex index 5c697d17..69a28b06 100644 --- a/lib/phoenix_kit/users/magic_link_registration.ex +++ b/lib/phoenix_kit/users/magic_link_registration.ex @@ -163,7 +163,7 @@ defmodule PhoenixKit.Users.MagicLinkRegistration do attrs = attrs |> Map.put("email", email) - |> Map.put("confirmed_at", DateTime.utc_now()) + |> Map.put("confirmed_at", DateTime.truncate(DateTime.utc_now(), :second)) track_geolocation = Settings.get_boolean_setting("track_registration_geolocation", false) diff --git a/lib/phoenix_kit/users/permissions.ex b/lib/phoenix_kit/users/permissions.ex index e0d0e627..759aa1c4 100644 --- a/lib/phoenix_kit/users/permissions.ex +++ b/lib/phoenix_kit/users/permissions.ex @@ -735,7 +735,7 @@ defmodule PhoenixKit.Users.Permissions do # Bulk insert new permissions if MapSet.size(to_add) > 0 do - now = DateTime.utc_now() + now = DateTime.truncate(DateTime.utc_now(), :second) granted_by_int = resolve_user_id(granted_by_id) granted_by_uuid = resolve_user_uuid(granted_by_id) diff --git a/lib/phoenix_kit/users/role_assignment.ex b/lib/phoenix_kit/users/role_assignment.ex index 3ba07e9d..0fd2978f 100644 --- a/lib/phoenix_kit/users/role_assignment.ex +++ b/lib/phoenix_kit/users/role_assignment.ex @@ -99,7 +99,7 @@ defmodule PhoenixKit.Users.RoleAssignment do put_change( changeset, :assigned_at, - DateTime.utc_now() + DateTime.truncate(DateTime.utc_now(), :second) ) _ -> diff --git a/lib/phoenix_kit/users/roles.ex b/lib/phoenix_kit/users/roles.ex index 3953cce4..a56f3a4d 100644 --- a/lib/phoenix_kit/users/roles.ex +++ b/lib/phoenix_kit/users/roles.ex @@ -799,7 +799,7 @@ defmodule PhoenixKit.Users.Roles do # Add confirmed_at timestamp if user email is not confirmed defp maybe_add_confirmed_at(changes, nil) do - Map.put(changes, :confirmed_at, DateTime.utc_now()) + Map.put(changes, :confirmed_at, DateTime.truncate(DateTime.utc_now(), :second)) end defp maybe_add_confirmed_at(changes, _confirmed_at), do: changes From eabc6a32a70f90fcff8714983753d96d1bc20458 Mon Sep 17 00:00:00 2001 From: timujeen Date: Wed, 18 Feb 2026 07:39:12 +0000 Subject: [PATCH 3/3] Fix CastError in live sessions by using UUID lookup instead of integer id SimplePresence stores user.uuid in user_id field but preload_users_for_sessions passed these UUIDs to get_users_by_ids which queries by integer id column, causing Ecto.Query.CastError. Add get_users_by_uuids/1 and use it for presence user preloading. --- lib/phoenix_kit/users/auth.ex | 10 ++++++++++ lib/phoenix_kit_web/live/users/live_sessions.ex | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/phoenix_kit/users/auth.ex b/lib/phoenix_kit/users/auth.ex index 6df939d5..4c2cf23d 100644 --- a/lib/phoenix_kit/users/auth.ex +++ b/lib/phoenix_kit/users/auth.ex @@ -295,6 +295,16 @@ defmodule PhoenixKit.Users.Auth do |> Repo.all() end + @doc """ + Gets multiple users by their UUIDs. + """ + def get_users_by_uuids([]), do: [] + + def get_users_by_uuids(uuids) when is_list(uuids) do + from(u in User, where: u.uuid in ^uuids) + |> Repo.all() + end + @doc """ Gets the first admin user (Owner or Admin role). diff --git a/lib/phoenix_kit_web/live/users/live_sessions.ex b/lib/phoenix_kit_web/live/users/live_sessions.ex index 2f207fc8..26ddef85 100644 --- a/lib/phoenix_kit_web/live/users/live_sessions.ex +++ b/lib/phoenix_kit_web/live/users/live_sessions.ex @@ -226,15 +226,15 @@ defmodule PhoenixKitWeb.Live.Users.LiveSessions do end defp preload_users_for_sessions(sessions) do - user_ids = + user_uuids = sessions |> Enum.filter(&(&1.type == :authenticated)) |> Enum.map(& &1.user_id) |> Enum.uniq() - case user_ids do + case user_uuids do [] -> %{} - ids -> Auth.get_users_by_ids(ids) |> Map.new(&{&1.id, &1}) + uuids -> Auth.get_users_by_uuids(uuids) |> Map.new(&{&1.uuid, &1}) end end