Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol terminal multiplexer session_manager.ex
Raw

lib/raxol/terminal/multiplexer/session_manager.ex

defmodule Raxol.Terminal.Multiplexer.SessionManager do
@moduledoc """
Terminal multiplexing system providing tmux-like session management for Raxol.
This module provides comprehensive session management with:
- Multiple persistent terminal sessions
- Window and pane management within sessions
- Session detaching and reattaching
- Cross-platform session persistence
- Real-time session sharing and collaboration
- Advanced layout management
- Session scripting and automation
- Resource isolation between sessions
## Features
### Session Management
- Create, list, attach, detach sessions
- Named sessions with unique identifiers
- Session persistence across application restarts
- Session templates and presets
- Automatic session recovery
### Window Management
- Multiple windows per session
- Window splitting (horizontal/vertical)
- Pane management with resize and navigation
- Custom layouts (even-horizontal, even-vertical, main-vertical, etc.)
- Window and pane synchronization
### Advanced Features
- Session sharing between users/devices
- Real-time collaboration with multiple cursors
- Session recording and playback
- Command history per session
- Environment variable isolation
- Custom key bindings per session
## Usage
# Create and manage sessions
{:ok, session_mgr} = SessionManager.start_link()
# Create a new session
{:ok, session_id} = SessionManager.create_session(session_mgr,
name: "dev-work",
windows: [
%{name: "editor", command: "nvim"},
%{name: "server", command: "mix phx.server"},
%{name: "logs", command: "tail -f log/dev.log"}
]
)
# Attach to session
SessionManager.attach_session(session_mgr, session_id)
# Create window with panes
{:ok, window_id} = SessionManager.create_window(session_mgr, session_id,
name: "development",
layout: :main_vertical,
panes: [
%{command: "nvim .", size: 70},
%{command: "iex -S mix", size: 30}
]
)
# Session sharing
share_token = SessionManager.share_session(session_mgr, session_id,
permissions: [:read, :write],
expires_in: :timer.hours(24)
)
"""
use GenServer
require Logger
# Multiplexer aliases will be added as needed
defstruct [
:sessions,
:active_session,
:session_storage,
:config,
:event_bus,
:collaboration_server
]
@type session_id :: String.t()
@type window_id :: String.t()
@type pane_id :: String.t()
@type layout_type ::
:even_horizontal
| :even_vertical
| :main_horizontal
| :main_vertical
| :tiled
| :custom
@type session :: %{
id: session_id(),
name: String.t(),
created_at: integer(),
last_accessed: integer(),
windows: [window_id()],
active_window: window_id() | nil,
environment: %{String.t() => String.t()},
working_directory: String.t(),
status: :active | :detached | :shared,
metadata: map()
}
@type window :: %{
id: window_id(),
session_id: session_id(),
name: String.t(),
layout: layout_type(),
panes: [pane_id()],
active_pane: pane_id() | nil,
created_at: integer(),
metadata: map()
}
@type pane :: %{
id: pane_id(),
window_id: window_id(),
session_id: session_id(),
command: String.t() | nil,
pid: pid() | nil,
size: %{width: integer(), height: integer()},
position: %{x: integer(), y: integer()},
status: :active | :inactive | :dead,
buffer: term(),
metadata: map()
}
@type collaboration_permission :: :read | :write | :admin
@type share_token :: String.t()
# Default configuration
@default_config %{
max_sessions: 100,
session_timeout_hours: 72,
auto_save_interval_ms: 30_000,
enable_collaboration: false,
storage_backend: :file,
storage_path: "~/.raxol/sessions"
}
# Default layouts would be defined here when needed
## Public API
@doc """
Starts the session manager.
## Options
- `:max_sessions` - Maximum number of concurrent sessions (default: 100)
- `:session_timeout_hours` - Hours before inactive sessions expire (default: 72)
- `:enable_collaboration` - Enable session sharing features (default: false)
- `:storage_backend` - Storage backend (:file, :ets, :database) (default: :file)
"""
def start_link(opts \\ []) do
config = opts |> Enum.into(%{}) |> then(&Map.merge(@default_config, &1))
GenServer.start_link(__MODULE__, config, name: __MODULE__)
end
@doc """
Creates a new terminal session.
## Options
- `:name` - Session name (default: auto-generated)
- `:windows` - Initial windows to create
- `:working_directory` - Initial working directory
- `:environment` - Environment variables
- `:template` - Session template to use
"""
def create_session(manager \\ __MODULE__, opts \\ []) do
GenServer.call(manager, {:create_session, opts})
end
@doc """
Lists all available sessions.
"""
def list_sessions(manager \\ __MODULE__) do
GenServer.call(manager, :list_sessions)
end
@doc """
Gets detailed information about a session.
"""
def get_session(manager \\ __MODULE__, session_id) do
GenServer.call(manager, {:get_session, session_id})
end
@doc """
Attaches to a session, making it the active session.
"""
def attach_session(manager \\ __MODULE__, session_id) do
GenServer.call(manager, {:attach_session, session_id})
end
@doc """
Detaches from the current session.
"""
def detach_session(manager \\ __MODULE__) do
GenServer.call(manager, :detach_session)
end
@doc """
Terminates a session and all its windows/panes.
"""
def kill_session(manager \\ __MODULE__, session_id) do
GenServer.call(manager, {:kill_session, session_id})
end
@doc """
Renames a session.
"""
def rename_session(manager \\ __MODULE__, session_id, new_name) do
GenServer.call(manager, {:rename_session, session_id, new_name})
end
@doc """
Creates a new window within a session.
## Options
- `:name` - Window name
- `:command` - Initial command to run
- `:layout` - Pane layout type
- `:panes` - Initial panes configuration
- `:working_directory` - Working directory for the window
"""
def create_window(manager \\ __MODULE__, session_id, opts \\ []) do
GenServer.call(manager, {:create_window, session_id, opts})
end
@doc """
Lists windows in a session.
"""
def list_windows(manager \\ __MODULE__, session_id) do
GenServer.call(manager, {:list_windows, session_id})
end
@doc """
Switches to a different window within the current session.
"""
def select_window(manager \\ __MODULE__, session_id, window_id) do
GenServer.call(manager, {:select_window, session_id, window_id})
end
@doc """
Kills a window and all its panes.
"""
def kill_window(manager \\ __MODULE__, session_id, window_id) do
GenServer.call(manager, {:kill_window, session_id, window_id})
end
@doc """
Splits a pane horizontally or vertically.
## Options
- `:direction` - :horizontal or :vertical
- `:command` - Command to run in new pane
- `:size` - Size percentage for the new pane
"""
def split_pane(
manager \\ __MODULE__,
session_id,
window_id,
pane_id,
opts \\ []
) do
GenServer.call(manager, {:split_pane, session_id, window_id, pane_id, opts})
end
@doc """
Kills a pane.
"""
def kill_pane(manager \\ __MODULE__, session_id, window_id, pane_id) do
GenServer.call(manager, {:kill_pane, session_id, window_id, pane_id})
end
@doc """
Resizes a pane.
"""
def resize_pane(
manager \\ __MODULE__,
session_id,
window_id,
pane_id,
size_change
) do
GenServer.call(
manager,
{:resize_pane, session_id, window_id, pane_id, size_change}
)
end
@doc """
Changes the layout of a window.
"""
def set_layout(manager \\ __MODULE__, session_id, window_id, layout) do
GenServer.call(manager, {:set_layout, session_id, window_id, layout})
end
@doc """
Shares a session with other users.
## Options
- `:permissions` - List of permissions (:read, :write, :admin)
- `:expires_in` - Expiration time in milliseconds
- `:max_users` - Maximum number of concurrent users
"""
def share_session(manager \\ __MODULE__, session_id, opts \\ []) do
GenServer.call(manager, {:share_session, session_id, opts})
end
@doc """
Joins a shared session using a share token.
"""
def join_shared_session(manager \\ __MODULE__, share_token) do
GenServer.call(manager, {:join_shared_session, share_token})
end
@doc """
Saves the current session state to storage.
"""
def save_session(manager \\ __MODULE__, session_id) do
GenServer.call(manager, {:save_session, session_id})
end
@doc """
Restores a session from storage.
"""
def restore_session(manager \\ __MODULE__, session_id) do
GenServer.call(manager, {:restore_session, session_id})
end
## GenServer Implementation
@impl GenServer
def init(config) do
# Initialize storage
{:ok, storage} = init_storage(config)
# Schedule periodic saves
schedule_auto_save(
config.auto_save_interval_ms > 0,
config.auto_save_interval_ms
)
# Load existing sessions
sessions = load_sessions(storage)
state = %__MODULE__{
sessions: sessions,
active_session: nil,
session_storage: storage,
config: config,
event_bus: init_event_bus(),
collaboration_server:
init_collaboration_if_enabled(config.enable_collaboration)
}
Logger.info(
"Session manager initialized with #{map_size(sessions)} sessions"
)
{:ok, state}
end
@impl GenServer
def handle_call({:create_session, opts}, _from, state) do
case create_session_impl(state, opts) do
{:ok, session, new_state} ->
Logger.info("Created session '#{session.name}' (#{session.id})")
{:reply, {:ok, session.id}, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl GenServer
def handle_call(:list_sessions, _from, state) do
sessions =
state.sessions
|> Enum.map(fn {id, session} ->
%{
id: id,
name: session.name,
status: session.status,
windows: length(session.windows),
created_at: session.created_at,
last_accessed: session.last_accessed
}
end)
{:reply, sessions, state}
end
@impl GenServer
def handle_call({:get_session, session_id}, _from, state) do
case Map.get(state.sessions, session_id) do
nil ->
{:reply, {:error, :session_not_found}, state}
session ->
detailed_session = enrich_session_details(session, state)
{:reply, {:ok, detailed_session}, state}
end
end
@impl GenServer
def handle_call({:attach_session, session_id}, _from, state) do
case Map.get(state.sessions, session_id) do
nil ->
{:reply, {:error, :session_not_found}, state}
session ->
# Update session status and last accessed time
updated_session = %{
session
| status: :active,
last_accessed: System.monotonic_time(:millisecond)
}
new_sessions = Map.put(state.sessions, session_id, updated_session)
new_state = %{
state
| sessions: new_sessions,
active_session: session_id
}
# Broadcast session attachment event
broadcast_event(state, :session_attached, %{session_id: session_id})
Logger.info("Attached to session '#{session.name}' (#{session_id})")
{:reply, :ok, new_state}
end
end
@impl GenServer
def handle_call(:detach_session, _from, state) do
process_detach_session(state.active_session != nil, state)
end
@impl GenServer
def handle_call({:create_window, session_id, opts}, _from, state) do
case Map.get(state.sessions, session_id) do
nil ->
{:reply, {:error, :session_not_found}, state}
session ->
{:ok, window, new_state} = create_window_impl(state, session, opts)
Logger.info("Created window '#{window.name}' in session #{session_id}")
{:reply, {:ok, window.id}, new_state}
end
end
@impl GenServer
def handle_call(
{:split_pane, session_id, window_id, pane_id, opts},
_from,
state
) do
case get_pane(state, session_id, window_id, pane_id) do
{:ok, pane} ->
{:ok, new_pane, new_state} = split_pane_impl(state, pane, opts)
Logger.info("Split pane #{pane_id} in window #{window_id}")
{:reply, {:ok, new_pane.id}, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl GenServer
def handle_call({:share_session, session_id, opts}, _from, state) do
handle_session_sharing(
state.config.enable_collaboration,
state,
session_id,
opts
)
end
@impl GenServer
def handle_info(:auto_save, state) do
# Auto-save all sessions
saved_count = save_all_sessions(state)
Logger.debug("Auto-saved #{saved_count} sessions")
{:noreply, state}
end
@impl GenServer
def handle_info(_msg, state) do
{:noreply, state}
end
## Private Implementation
defp create_session_impl(state, opts) do
# Check session limit
session_limit_reached =
map_size(state.sessions) >= state.config.max_sessions
create_session_if_allowed(session_limit_reached, state, opts)
end
defp create_session_if_allowed(true, _state, _opts),
do: {:error, :max_sessions_reached}
defp create_session_if_allowed(false, state, opts) do
session_id = generate_session_id()
session_name = Keyword.get(opts, :name, "session-#{session_id}")
# Create base session
session = %{
id: session_id,
name: session_name,
created_at: System.monotonic_time(:millisecond),
last_accessed: System.monotonic_time(:millisecond),
windows: [],
active_window: nil,
environment: Keyword.get(opts, :environment, System.get_env()),
working_directory: Keyword.get(opts, :working_directory, File.cwd!()),
status: :active,
metadata: %{}
}
# Create initial windows if specified
{updated_session, updated_state} =
case Keyword.get(opts, :windows, []) do
[] ->
# Create default window
{:ok, window, new_state} = create_default_window(state, session)
updated_sess = %{
session
| windows: [window.id],
active_window: window.id
}
{updated_sess, new_state}
window_configs ->
create_initial_windows(state, session, window_configs)
end
new_sessions =
Map.put(updated_state.sessions, session_id, updated_session)
final_state = %{
updated_state
| sessions: new_sessions,
active_session: session_id
}
{:ok, updated_session, final_state}
end
defp create_window_impl(state, session, opts) do
window_id = generate_window_id()
window_name = Keyword.get(opts, :name, "window-#{window_id}")
layout = Keyword.get(opts, :layout, :even_horizontal)
# Create window
window = %{
id: window_id,
session_id: session.id,
name: window_name,
layout: layout,
panes: [],
active_pane: nil,
created_at: System.monotonic_time(:millisecond),
metadata: %{}
}
# Create initial pane(s)
pane_configs = Keyword.get(opts, :panes, [%{}])
{updated_window, updated_state} =
create_window_panes(state, window, pane_configs)
# Update session
updated_session = %{
session
| windows: [window_id | session.windows],
active_window: window_id
}
new_sessions = Map.put(state.sessions, session.id, updated_session)
final_state = %{updated_state | sessions: new_sessions}
{:ok, updated_window, final_state}
end
defp create_default_window(state, session) do
create_window_impl(state, session, name: "main")
end
defp create_initial_windows(state, session, window_configs) do
{windows, final_state} =
Enum.reduce(window_configs, {[], state}, fn window_config,
{acc_windows, acc_state} ->
{:ok, window, new_state} =
create_window_impl(acc_state, session, window_config)
{[window | acc_windows], new_state}
end)
window_ids = Enum.map(windows, & &1.id)
active_window_id = List.first(window_ids)
updated_session = %{
session
| windows: window_ids,
active_window: active_window_id
}
{updated_session, final_state}
end
defp create_window_panes(state, window, pane_configs) do
{panes, final_state} =
Enum.reduce(pane_configs, {[], state}, fn pane_config,
{acc_panes, acc_state} ->
pane = create_pane(window, pane_config)
{[pane | acc_panes], acc_state}
end)
pane_ids = Enum.map(panes, & &1.id)
active_pane_id = List.first(pane_ids)
updated_window = %{window | panes: pane_ids, active_pane: active_pane_id}
{updated_window, final_state}
end
defp create_pane(window, config) do
pane_id = generate_pane_id()
%{
id: pane_id,
window_id: window.id,
session_id: window.session_id,
command: Map.get(config, :command),
# Will be set when command starts
pid: nil,
# Default size
size: %{width: 80, height: 24},
position: %{x: 0, y: 0},
status: :active,
buffer: init_pane_buffer(),
metadata: %{}
}
end
defp split_pane_impl(state, existing_pane, opts) do
direction = Keyword.get(opts, :direction, :vertical)
command = Keyword.get(opts, :command)
size_ratio = Keyword.get(opts, :size, 50)
# Create new pane
new_pane = %{
id: generate_pane_id(),
window_id: existing_pane.window_id,
session_id: existing_pane.session_id,
command: command,
pid: nil,
size: calculate_split_size(existing_pane.size, direction, size_ratio),
position:
calculate_split_position(existing_pane.position, direction, size_ratio),
status: :active,
buffer: init_pane_buffer(),
metadata: %{}
}
# Update existing pane size
updated_existing_pane = %{
existing_pane
| size:
calculate_remaining_size(existing_pane.size, direction, size_ratio)
}
# Update window and session state
updated_state = update_pane_layout(state, new_pane, updated_existing_pane)
{:ok, new_pane, updated_state}
end
defp get_pane(state, session_id, window_id, pane_id) do
# This is a simplified lookup - in practice would maintain proper data structures
case Map.get(state.sessions, session_id) do
nil ->
{:error, :session_not_found}
session ->
validate_window_and_get_pane(
window_id in session.windows,
pane_id,
window_id,
session_id
)
end
end
defp create_share_token(state, session_id, opts) do
case Map.get(state.sessions, session_id) do
nil ->
{:error, :session_not_found}
session ->
permissions = Keyword.get(opts, :permissions, [:read])
expires_in = Keyword.get(opts, :expires_in, :timer.hours(24))
max_users = Keyword.get(opts, :max_users, 10)
share_token = generate_share_token()
expires_at = System.monotonic_time(:millisecond) + expires_in
share_info = %{
token: share_token,
session_id: session_id,
permissions: permissions,
max_users: max_users,
current_users: 0,
expires_at: expires_at,
created_at: System.monotonic_time(:millisecond)
}
# Store share info (would use proper storage in practice)
updated_session = Map.put(session, :share_info, share_info)
new_sessions = Map.put(state.sessions, session_id, updated_session)
new_state = %{state | sessions: new_sessions}
{:ok, share_token, new_state}
end
end
## Helper Functions
defp generate_session_id do
:crypto.strong_rand_bytes(8) |> Base.url_encode64(padding: false)
end
defp generate_window_id do
:crypto.strong_rand_bytes(6) |> Base.url_encode64(padding: false)
end
defp generate_pane_id do
:crypto.strong_rand_bytes(4) |> Base.url_encode64(padding: false)
end
defp generate_share_token do
:crypto.strong_rand_bytes(16) |> Base.url_encode64(padding: false)
end
defp init_pane_buffer do
# Initialize terminal buffer for pane
%{
lines: [],
cursor: %{x: 0, y: 0},
scrollback: []
}
end
defp calculate_split_size(original_size, direction, ratio) do
case direction do
:horizontal ->
%{original_size | height: round(original_size.height * ratio / 100)}
:vertical ->
%{original_size | width: round(original_size.width * ratio / 100)}
end
end
defp calculate_split_position(original_position, direction, ratio) do
case direction do
:horizontal ->
%{original_position | y: original_position.y + round(ratio)}
:vertical ->
%{original_position | x: original_position.x + round(ratio)}
end
end
defp calculate_remaining_size(original_size, direction, ratio) do
case direction do
:horizontal ->
%{
original_size
| height: round(original_size.height * (100 - ratio) / 100)
}
:vertical ->
%{
original_size
| width: round(original_size.width * (100 - ratio) / 100)
}
end
end
defp update_pane_layout(state, _new_pane, _updated_existing_pane) do
# This would update the actual layout structures
# For now, just return the state
state
end
defp enrich_session_details(session, _state) do
# Add detailed information about windows and panes
windows =
Enum.map(session.windows, fn window_id ->
%{
id: window_id,
name: "window-#{window_id}",
panes: 1,
active: session.active_window == window_id
}
end)
%{session | windows: windows}
end
## Storage Functions
defp init_storage(config) do
case config.storage_backend do
:file ->
storage_path = Path.expand(config.storage_path)
File.mkdir_p!(storage_path)
{:ok, %{type: :file, path: storage_path}}
:ets ->
table = :ets.new(:raxol_sessions, [:set, :public, :named_table])
{:ok, %{type: :ets, table: table}}
:database ->
# Would initialize database connection
{:ok, %{type: :database, connection: nil}}
end
end
defp load_sessions(storage) do
case Raxol.Core.ErrorHandling.safe_call(fn ->
case storage.type do
:file ->
sessions_file = Path.join(storage.path, "sessions.json")
load_sessions_from_file(
File.exists?(sessions_file),
sessions_file
)
:ets ->
:ets.tab2list(storage.table) |> Map.new()
:database ->
# Would load from database
%{}
end
end) do
{:ok, sessions} -> sessions
{:error, _reason} -> %{}
end
end
defp save_all_sessions(state) do
sessions_to_save =
state.sessions
|> Enum.filter(fn {_id, session} -> session.status != :dead end)
case Raxol.Core.ErrorHandling.safe_call(fn ->
case state.session_storage.type do
:file ->
sessions_file =
Path.join(state.session_storage.path, "sessions.json")
json_data = Jason.encode!(sessions_to_save)
File.write!(sessions_file, json_data)
:ets ->
# Clear and repopulate ETS table
:ets.delete_all_objects(state.session_storage.table)
Enum.each(sessions_to_save, fn {id, session} ->
:ets.insert(state.session_storage.table, {id, session})
end)
:database ->
# Would save to database
:ok
end
length(sessions_to_save)
end) do
{:ok, count} ->
count
{:error, reason} ->
Logger.error("Failed to save sessions: #{inspect(reason)}")
0
end
end
## Event System
defp init_event_bus do
{:ok, pid} = GenServer.start_link(EventBus, [])
pid
end
defp init_collaboration_server do
# Would initialize collaboration server
nil
end
defp broadcast_event(state, event_type, event_data) do
broadcast_if_available(
state.event_bus != nil,
state.event_bus,
event_type,
event_data
)
end
## Utility Functions
defp atomize_keys(map) when is_map(map) do
Map.new(map, fn {k, v} -> {String.to_atom(k), atomize_keys(v)} end)
end
defp atomize_keys(list) when is_list(list) do
Enum.map(list, &atomize_keys/1)
end
defp atomize_keys(value), do: value
## Helper Functions for Pattern Matching
defp schedule_auto_save(false, _interval), do: :ok
defp schedule_auto_save(true, interval),
do: :timer.send_interval(interval, :auto_save)
defp init_collaboration_if_enabled(true), do: init_collaboration_server()
defp init_collaboration_if_enabled(false), do: nil
defp process_detach_session(false, state),
do: {:reply, {:error, :no_active_session}, state}
defp process_detach_session(true, state) do
session_id = state.active_session
session = Map.get(state.sessions, session_id)
# Update session status
updated_session = %{
session
| status: :detached,
last_accessed: System.monotonic_time(:millisecond)
}
new_sessions = Map.put(state.sessions, session_id, updated_session)
new_state = %{state | sessions: new_sessions, active_session: nil}
# Broadcast detachment event
broadcast_event(state, :session_detached, %{session_id: session_id})
Logger.info("Detached from session '#{session.name}' (#{session_id})")
{:reply, :ok, new_state}
end
defp handle_session_sharing(false, state, _session_id, _opts),
do: {:reply, {:error, :collaboration_disabled}, state}
defp handle_session_sharing(true, state, session_id, opts) do
case create_share_token(state, session_id, opts) do
{:ok, share_token, new_state} ->
Logger.info(
"Session #{session_id} shared with token #{String.slice(share_token, 0, 8)}..."
)
{:reply, {:ok, share_token}, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
defp validate_window_and_get_pane(false, _pane_id, _window_id, _session_id),
do: {:error, :window_not_found}
defp validate_window_and_get_pane(true, pane_id, window_id, session_id) do
# Mock pane lookup
pane = %{
id: pane_id,
window_id: window_id,
session_id: session_id,
command: nil,
pid: nil,
size: %{width: 80, height: 24},
position: %{x: 0, y: 0},
status: :active,
buffer: init_pane_buffer(),
metadata: %{}
}
{:ok, pane}
end
defp load_sessions_from_file(false, _sessions_file), do: %{}
defp load_sessions_from_file(true, sessions_file) do
sessions_file
|> File.read!()
|> Jason.decode!()
|> Enum.map(fn {id, session_data} ->
{id, atomize_keys(session_data)}
end)
|> Map.new()
end
defp broadcast_if_available(false, _event_bus, _event_type, _event_data),
do: :ok
defp broadcast_if_available(true, event_bus, event_type, event_data) do
GenServer.cast(event_bus, {:broadcast, event_type, event_data})
end
## Public Utility Functions
@doc """
Creates a session template for reuse.
"""
def create_template(name, session_config) do
template = %{
name: name,
config: session_config,
created_at: System.monotonic_time(:millisecond)
}
# Would save template to storage
{:ok, template}
end
@doc """
Lists available session templates.
"""
def list_templates do
# Would load from storage
[]
end
@doc """
Gets session statistics and usage information.
"""
def get_statistics(manager \\ __MODULE__) do
GenServer.call(manager, :get_statistics)
end
end
# Simple EventBus implementation
defmodule EventBus do
use GenServer
def init(_) do
{:ok, %{subscribers: []}}
end
def handle_cast({:broadcast, _event_type, _event_data}, state) do
# Would broadcast to subscribers
{:noreply, state}
end
end