Packages

phoenix_kit

1.5.2
1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib phoenix_kit admin simple_presence.ex
Raw

lib/phoenix_kit/admin/simple_presence.ex

defmodule PhoenixKit.Admin.SimplePresence do
@moduledoc """
Simple presence tracking system for PhoenixKit admin interface.
This is a lightweight alternative to Phoenix.Presence that works
without requiring a full OTP application supervision tree.
"""
use GenServer
require Logger
alias PhoenixKit.Admin.Events
alias PhoenixKit.PubSub.Manager
@table_name :phoenix_kit_sessions
@server_name __MODULE__
@presence_topic "phoenix_kit:presence"
## Public API
@doc """
Starts the simple presence system.
"""
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: @server_name)
end
@doc """
Tracks an anonymous session.
"""
def track_anonymous(session_id, metadata \\ %{}) do
key = "anonymous:#{session_id}"
metadata =
metadata
|> Map.put(:type, :anonymous)
|> Map.put(:session_id, session_id)
|> Map.put_new(:connected_at, DateTime.utc_now())
case GenServer.call(@server_name, {:track, key, metadata}) do
:ok ->
Events.broadcast_anonymous_session_connected(session_id, metadata)
broadcast_presence_stats()
:ok
error ->
error
end
rescue
error ->
Logger.error("Failed to track anonymous session: #{inspect(error)}")
{:error, error}
end
@doc """
Tracks an authenticated user session.
"""
def track_user(user, metadata \\ %{}) do
key = "user:#{user.id}"
metadata =
metadata
|> Map.put(:type, :authenticated)
|> Map.put(:user_id, user.id)
|> Map.put(:user_email, user.email)
|> Map.put_new(:connected_at, DateTime.utc_now())
case GenServer.call(@server_name, {:track, key, metadata}) do
:ok ->
Events.broadcast_user_session_connected(user.id, metadata)
broadcast_presence_stats()
:ok
error ->
error
end
rescue
error ->
Logger.error("Failed to track user session: #{inspect(error)}")
{:error, error}
end
@doc """
Updates metadata for an existing presence.
"""
def update_metadata(key, metadata_updates) do
case GenServer.call(@server_name, {:update, key, metadata_updates}) do
:ok ->
broadcast_presence_stats()
:ok
error ->
error
end
end
@doc """
Lists all active sessions.
"""
def list_active_sessions do
@table_name
|> :ets.tab2list()
|> Enum.map(fn {_key, metadata} -> metadata end)
|> Enum.sort_by(& &1.connected_at, {:desc, DateTime})
rescue
ArgumentError ->
[]
end
@doc """
Lists anonymous sessions only.
"""
def list_anonymous_sessions do
list_active_sessions()
|> Enum.filter(&(&1.type == :anonymous))
end
@doc """
Lists authenticated sessions only.
"""
def list_authenticated_sessions do
list_active_sessions()
|> Enum.filter(&(&1.type == :authenticated))
end
@doc """
Gets presence statistics.
"""
def get_presence_stats do
active_sessions = list_active_sessions()
anonymous_sessions = Enum.filter(active_sessions, &(&1.type == :anonymous))
authenticated_sessions = Enum.filter(active_sessions, &(&1.type == :authenticated))
# Calculate page statistics
page_stats =
active_sessions
|> Enum.filter(&(not is_nil(&1.current_page)))
|> Enum.group_by(& &1.current_page)
|> Enum.map(fn {page, sessions} -> {page, length(sessions)} end)
|> Enum.sort_by(&elem(&1, 1), :desc)
|> Enum.take(10)
%{
total_sessions: length(active_sessions),
anonymous_sessions: length(anonymous_sessions),
authenticated_sessions: length(authenticated_sessions),
unique_anonymous_visitors: length(Enum.uniq_by(anonymous_sessions, & &1.session_id)),
active_authenticated_users: length(Enum.uniq_by(authenticated_sessions, & &1.user_id)),
top_pages: page_stats,
last_updated: DateTime.utc_now()
}
end
@doc """
Subscribes to presence events.
"""
def subscribe do
Manager.subscribe(@presence_topic)
end
@doc """
Gets the presence topic name.
"""
def get_topic, do: @presence_topic
## GenServer Callbacks
@impl true
def init(_opts) do
# Create ETS table for session tracking
:ets.new(@table_name, [:named_table, :public, :set])
# Schedule cleanup of old sessions every 5 minutes
schedule_cleanup()
Logger.debug("PhoenixKit.Admin.SimplePresence started")
{:ok, %{}}
end
@impl true
def handle_call({:track, key, metadata}, {pid, _ref}, state) do
# Store session with process monitor
monitor_ref = Process.monitor(pid)
metadata = Map.put(metadata, :monitor_ref, monitor_ref)
:ets.insert(@table_name, {key, metadata})
{:reply, :ok, state}
end
@impl true
def handle_call({:update, key, metadata_updates}, _from, state) do
case :ets.lookup(@table_name, key) do
[{^key, existing_metadata}] ->
updated_metadata = Map.merge(existing_metadata, metadata_updates)
:ets.insert(@table_name, {key, updated_metadata})
{:reply, :ok, state}
[] ->
{:reply, {:error, :not_found}, state}
end
end
@impl true
def handle_info({:DOWN, monitor_ref, :process, _pid, _reason}, state) do
# Remove session when process dies
cleanup_session_by_monitor(monitor_ref)
broadcast_presence_stats()
{:noreply, state}
end
@impl true
def handle_info(:cleanup, state) do
cleanup_old_sessions()
schedule_cleanup()
{:noreply, state}
end
## Private Functions
defp broadcast_presence_stats do
stats = get_presence_stats()
Events.broadcast_presence_stats_updated(stats)
end
defp schedule_cleanup do
# 5 minutes
Process.send_after(self(), :cleanup, 5 * 60 * 1000)
end
defp cleanup_session_by_monitor(monitor_ref) do
# Find and remove session by monitor reference
@table_name
|> :ets.tab2list()
|> Enum.find(fn {_key, metadata} ->
Map.get(metadata, :monitor_ref) == monitor_ref
end)
|> case do
{key, metadata} ->
:ets.delete(@table_name, key)
# Broadcast disconnect event
case metadata.type do
:anonymous ->
Events.broadcast_anonymous_session_disconnected(metadata.session_id)
:authenticated ->
Events.broadcast_user_session_disconnected(metadata.user_id, metadata.session_id)
end
nil ->
:ok
end
end
defp cleanup_old_sessions do
# Remove sessions older than 1 hour
one_hour_ago = DateTime.add(DateTime.utc_now(), -3600, :second)
@table_name
|> :ets.tab2list()
|> Enum.filter(fn {_key, metadata} ->
DateTime.compare(metadata.connected_at, one_hour_ago) == :lt
end)
|> Enum.each(fn {key, metadata} ->
:ets.delete(@table_name, key)
# Broadcast disconnect event
case metadata.type do
:anonymous ->
Events.broadcast_anonymous_session_disconnected(metadata.session_id)
:authenticated ->
Events.broadcast_user_session_disconnected(metadata.user_id, metadata.session_id)
end
end)
if length(@table_name |> :ets.tab2list()) > 0 do
broadcast_presence_stats()
end
end
end