diff --git a/lib/wanderer_app/api/map_system_structure.ex b/lib/wanderer_app/api/map_system_structure.ex index 4a2baf98..f5a70a1f 100644 --- a/lib/wanderer_app/api/map_system_structure.ex +++ b/lib/wanderer_app/api/map_system_structure.ex @@ -5,25 +5,24 @@ defmodule WandererApp.Api.MapSystemStructure do """ @derive {Jason.Encoder, - only: [ - :id, - :system_id, - :solar_system_id, - :solar_system_name, - :structure_type_id, - :structure_type, - :character_eve_id, - :name, - :notes, - :owner_name, - :owner_ticker, - :owner_id, - :status, - :end_time, - :inserted_at, - :updated_at - ] - } + only: [ + :id, + :system_id, + :solar_system_id, + :solar_system_name, + :structure_type_id, + :structure_type, + :character_eve_id, + :name, + :notes, + :owner_name, + :owner_ticker, + :owner_id, + :status, + :end_time, + :inserted_at, + :updated_at + ]} use Ash.Resource, domain: WandererApp.Api, @@ -100,10 +99,9 @@ defmodule WandererApp.Api.MapSystemStructure do argument :system_id, :uuid, allow_nil?: false change manage_relationship(:system_id, :system, - on_lookup: :relate, - on_no_match: nil - ) - + on_lookup: :relate, + on_no_match: nil + ) end update :update do @@ -125,9 +123,7 @@ defmodule WandererApp.Api.MapSystemStructure do :status, :end_time ] - end - end attributes do diff --git a/lib/wanderer_app/application.ex b/lib/wanderer_app/application.ex index aa32ef63..763eecdd 100644 --- a/lib/wanderer_app/application.ex +++ b/lib/wanderer_app/application.ex @@ -93,7 +93,7 @@ defmodule WandererApp.Application do wanderer_kills_enabled = Application.get_env(:wanderer_app, :wanderer_kills_service_enabled, false) - if wanderer_kills_enabled in [true, :true, "true"] do + if wanderer_kills_enabled in [true, true, "true"] do Logger.info("Starting WandererKills service integration...") [ diff --git a/lib/wanderer_app/character/activity.ex b/lib/wanderer_app/character/activity.ex index f73095fb..8565fe3b 100644 --- a/lib/wanderer_app/character/activity.ex +++ b/lib/wanderer_app/character/activity.ex @@ -50,8 +50,8 @@ defmodule WandererApp.Character.Activity do def process_character_activity(map_id, current_user) do with {:ok, map_user_settings} <- get_map_user_settings(map_id, current_user.id), raw_activity <- WandererApp.Map.get_character_activity(map_id), - {:ok, user_characters} <- WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do - + {:ok, user_characters} <- + WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do result = process_activity_data(raw_activity, map_user_settings, user_characters) result end @@ -61,6 +61,7 @@ defmodule WandererApp.Character.Activity do case WandererApp.MapUserSettingsRepo.get(map_id, user_id) do {:ok, settings} when not is_nil(settings) -> {:ok, settings} + _ -> {:ok, %{main_character_eve_id: nil}} end @@ -98,17 +99,24 @@ defmodule WandererApp.Character.Activity do |> sort_by_timestamp() end - defp process_user_activity(user_id, user_activities, %{user_id: user_id, main_character_eve_id: main_id} = _map_user_settings, all_characters) + defp process_user_activity( + user_id, + user_activities, + %{user_id: user_id, main_character_eve_id: main_id} = _map_user_settings, + all_characters + ) when not is_nil(main_id) do # Group activities by character activities_by_character = group_activities_by_character(user_activities) main_id_str = to_string(main_id) - display_character = case Enum.find(all_characters, &(to_string(&1.eve_id) == main_id_str)) do - nil -> find_most_active_character_details(activities_by_character) # Fall back to most active - main_char -> main_char - end + display_character = + case Enum.find(all_characters, &(to_string(&1.eve_id) == main_id_str)) do + # Fall back to most active + nil -> find_most_active_character_details(activities_by_character) + main_char -> main_char + end build_activity_entry_if_valid(display_character, activities_by_character, user_id) end @@ -147,7 +155,8 @@ defmodule WandererApp.Character.Activity do # Find the details of the most active character defp find_most_active_character_details(activities_by_character) do - with most_active_id when not is_nil(most_active_id) <- find_most_active_character(activities_by_character), + with most_active_id when not is_nil(most_active_id) <- + find_most_active_character(activities_by_character), most_active_activities <- Map.get(activities_by_character, most_active_id, []), [first_activity | _] <- most_active_activities, character when not is_nil(character) <- Map.get(first_activity, :character) do @@ -168,13 +177,15 @@ defmodule WandererApp.Character.Activity do # Only create entry if there's at least some activity if all_passages + all_connections + all_signatures > 0 do - [%{ - character: character, - passages: all_passages, - connections: all_connections, - signatures: all_signatures, - timestamp: get_latest_timestamp(activities_by_character) - }] + [ + %{ + character: character, + passages: all_passages, + connections: all_connections, + signatures: all_signatures, + timestamp: get_latest_timestamp(activities_by_character) + } + ] else Logger.warning("Character has no activity, not creating entry") [] diff --git a/lib/wanderer_app/character/transactions_tracker.ex b/lib/wanderer_app/character/transactions_tracker.ex index 0df0ae8c..529743d1 100644 --- a/lib/wanderer_app/character/transactions_tracker.ex +++ b/lib/wanderer_app/character/transactions_tracker.ex @@ -71,7 +71,10 @@ defmodule WandererApp.Character.TransactionsTracker do @impl true def handle_info(:shutdown, %Impl{} = state) do - Logger.debug(fn -> "Shutting down character transaction tracker: #{inspect(state.character_id)}" end) + Logger.debug(fn -> + "Shutting down character transaction tracker: #{inspect(state.character_id)}" + end) + {:stop, :normal, state} end diff --git a/lib/wanderer_app/esi.ex b/lib/wanderer_app/esi.ex index c013f2a8..788c42e0 100644 --- a/lib/wanderer_app/esi.ex +++ b/lib/wanderer_app/esi.ex @@ -26,7 +26,6 @@ defmodule WandererApp.Esi do defdelegate get_killmail(killmail_id, killmail_hash, opts \\ []), to: WandererApp.Esi.ApiClient - defdelegate set_autopilot_waypoint( add_to_beginning, clear_other_waypoints, diff --git a/lib/wanderer_app/kills.ex b/lib/wanderer_app/kills.ex index e0f47f70..ccb6103d 100644 --- a/lib/wanderer_app/kills.ex +++ b/lib/wanderer_app/kills.ex @@ -1,55 +1,61 @@ defmodule WandererApp.Kills do @moduledoc """ Main interface for the WandererKills integration subsystem. - + Provides high-level functions for monitoring and managing the kills data pipeline, including connection status, health metrics, and system subscriptions. """ - + alias WandererApp.Kills.{Client, Storage} - + @doc """ Gets comprehensive status of the kills subsystem. """ @spec get_status() :: {:ok, map()} | {:error, term()} def get_status do with {:ok, client_status} <- Client.get_status() do - {:ok, %{ - enabled: Application.get_env(:wanderer_app, :wanderer_kills_service_enabled, false), - client: client_status, - websocket_url: Application.get_env(:wanderer_app, :wanderer_kills_base_url, "ws://wanderer-kills:4004") - }} + {:ok, + %{ + enabled: Application.get_env(:wanderer_app, :wanderer_kills_service_enabled, false), + client: client_status, + websocket_url: + Application.get_env( + :wanderer_app, + :wanderer_kills_base_url, + "ws://wanderer-kills:4004" + ) + }} end end - + @doc """ Subscribes to killmail updates for specified systems. """ @spec subscribe_systems([integer()]) :: :ok | {:error, term()} defdelegate subscribe_systems(system_ids), to: Client, as: :subscribe_to_systems - + @doc """ Unsubscribes from killmail updates for specified systems. """ @spec unsubscribe_systems([integer()]) :: :ok | {:error, term()} defdelegate unsubscribe_systems(system_ids), to: Client, as: :unsubscribe_from_systems - + @doc """ Gets kill count for a specific system. """ @spec get_system_kill_count(integer()) :: {:ok, non_neg_integer()} | {:error, :not_found} defdelegate get_system_kill_count(system_id), to: Storage, as: :get_kill_count - + @doc """ Gets recent kills for a specific system. """ @spec get_system_kills(integer()) :: {:ok, list(map())} | {:error, :not_found} defdelegate get_system_kills(system_id), to: Storage - + @doc """ Manually triggers a reconnection attempt. """ @spec reconnect() :: :ok | {:error, term()} defdelegate reconnect(), to: Client -end \ No newline at end of file +end diff --git a/lib/wanderer_app/kills/client.ex b/lib/wanderer_app/kills/client.ex index d4420072..3c0c7e49 100644 --- a/lib/wanderer_app/kills/client.ex +++ b/lib/wanderer_app/kills/client.ex @@ -15,8 +15,10 @@ defmodule WandererApp.Kills.Client do # Simple retry configuration - inline like character module @retry_delays [5_000, 10_000, 30_000, 60_000] @max_retries 10 - @health_check_interval :timer.seconds(30) # Check every 30 seconds - @message_timeout :timer.minutes(15) # No messages timeout + # Check every 30 seconds + @health_check_interval :timer.seconds(30) + # No messages timeout + @message_timeout :timer.minutes(15) defstruct [ :socket_pid, @@ -165,7 +167,8 @@ defmodule WandererApp.Kills.Client do | connected: true, connecting: false, socket_pid: socket_pid, - retry_count: 0, # Reset retry count only on successful connection + # Reset retry count only on successful connection + retry_count: 0, last_error: nil, last_message_time: System.system_time(:millisecond) } @@ -181,68 +184,78 @@ defmodule WandererApp.Kills.Client do end def handle_info({:disconnected, reason}, state) do - Logger.warning("[Client] WebSocket disconnected: #{inspect(reason)} (was connected: #{state.connected}, was connecting: #{state.connecting})") + Logger.warning( + "[Client] WebSocket disconnected: #{inspect(reason)} (was connected: #{state.connected}, was connecting: #{state.connecting})" + ) # Cancel connection timeout if pending state = cancel_connection_timeout(state) state = - %{state | - connected: false, - connecting: false, - socket_pid: nil, - last_error: reason - } + %{state | connected: false, connecting: false, socket_pid: nil, last_error: reason} if should_retry?(state) do {:noreply, schedule_retry(state)} else - Logger.error("[Client] Max retry attempts (#{@max_retries}) reached. Will not retry automatically.") + Logger.error( + "[Client] Max retry attempts (#{@max_retries}) reached. Will not retry automatically." + ) + {:noreply, state} end end def handle_info(:health_check, state) do health_status = check_health(state) - new_state = case health_status do - :healthy -> - state - :needs_reconnect -> - Logger.warning("[Client] Connection unhealthy, triggering reconnect (retry count: #{state.retry_count})") - # Don't reset retry count during health check failures - if state.connected or state.connecting do - send(self(), {:disconnected, :health_check_failed}) - %{state | connected: false, connecting: false, socket_pid: nil} - else - # Already disconnected, just maintain state + new_state = + case health_status do + :healthy -> state - end - :needs_reconnect_with_timestamp -> - Logger.warning("[Client] Health check triggering reconnect (retry count: #{state.retry_count})") - new_state = %{state | last_health_reconnect_attempt: System.system_time(:millisecond)} - if state.connected or state.connecting do - send(self(), {:disconnected, :health_check_failed}) - %{new_state | connected: false, connecting: false, socket_pid: nil} - else - # Already disconnected, trigger reconnect - send(self(), :connect) - new_state - end + :needs_reconnect -> + Logger.warning( + "[Client] Connection unhealthy, triggering reconnect (retry count: #{state.retry_count})" + ) - :needs_reconnect_reset_retries -> - Logger.warning("[Client] Health check resetting retry count and triggering reconnect") - new_state = %{state | retry_count: 0, last_retry_cycle_end: nil} - if state.connected or state.connecting do - send(self(), {:disconnected, :health_check_failed}) - %{new_state | connected: false, connecting: false, socket_pid: nil} - else - # Already disconnected, trigger immediate reconnect with reset count - send(self(), :connect) - new_state - end - end + # Don't reset retry count during health check failures + if state.connected or state.connecting do + send(self(), {:disconnected, :health_check_failed}) + %{state | connected: false, connecting: false, socket_pid: nil} + else + # Already disconnected, just maintain state + state + end + + :needs_reconnect_with_timestamp -> + Logger.warning( + "[Client] Health check triggering reconnect (retry count: #{state.retry_count})" + ) + + new_state = %{state | last_health_reconnect_attempt: System.system_time(:millisecond)} + + if state.connected or state.connecting do + send(self(), {:disconnected, :health_check_failed}) + %{new_state | connected: false, connecting: false, socket_pid: nil} + else + # Already disconnected, trigger reconnect + send(self(), :connect) + new_state + end + + :needs_reconnect_reset_retries -> + Logger.warning("[Client] Health check resetting retry count and triggering reconnect") + new_state = %{state | retry_count: 0, last_retry_cycle_end: nil} + + if state.connected or state.connecting do + send(self(), {:disconnected, :health_check_failed}) + %{new_state | connected: false, connecting: false, socket_pid: nil} + else + # Already disconnected, trigger immediate reconnect with reset count + send(self(), :connect) + new_state + end + end schedule_health_check() {:noreply, new_state} @@ -261,7 +274,9 @@ defmodule WandererApp.Kills.Client do end def handle_info({:connection_timeout, socket_pid}, %{socket_pid: socket_pid} = state) do - Logger.error("[Client] Connection timeout - socket process failed to connect within 10s (retry #{state.retry_count}/#{@max_retries})") + Logger.error( + "[Client] Connection timeout - socket process failed to connect within 10s (retry #{state.retry_count}/#{@max_retries})" + ) # Kill the socket process if it's still alive if socket_alive?(socket_pid) do @@ -303,9 +318,9 @@ defmodule WandererApp.Kills.Client do Logger.debug(fn -> "[Client] Subscribing to #{length(to_subscribe)} new systems. " <> - "Total subscribed: #{MapSet.size(updated_systems)}. " <> - "Map breakdown: #{inspect(map_info)}" end - ) + "Total subscribed: #{MapSet.size(updated_systems)}. " <> + "Map breakdown: #{inspect(map_info)}" + end) end if length(to_subscribe) > 0 and state.socket_pid do @@ -318,6 +333,7 @@ defmodule WandererApp.Kills.Client do def handle_cast({:unsubscribe_systems, system_ids}, state) do {updated_systems, to_unsubscribe} = Manager.unsubscribe_systems(state.subscribed_systems, system_ids) + if length(to_unsubscribe) > 0 and state.socket_pid do Manager.sync_with_server(state.socket_pid, [], to_unsubscribe) end @@ -354,7 +370,8 @@ defmodule WandererApp.Kills.Client do | connected: false, connecting: false, socket_pid: nil, - retry_count: 0, # Manual reconnect resets retry count + # Manual reconnect resets retry count + retry_count: 0, last_error: nil } @@ -378,10 +395,12 @@ defmodule WandererApp.Kills.Client do defp connect_to_server do url = Config.server_url() + systems = case MapIntegration.get_tracked_system_ids() do {:ok, system_list} -> system_list + {:error, reason} -> Logger.warning( "[Client] Failed to get tracked system IDs for initial subscription: #{inspect(reason)}, will retry after connection" @@ -402,9 +421,11 @@ defmodule WandererApp.Kills.Client do # GenSocketClient expects transport_opts to be wrapped in a specific format opts = [ transport_opts: [ - timeout: 10_000, # 10 second connection timeout + # 10 second connection timeout + timeout: 10_000, tcp_opts: [ - connect_timeout: 10_000, # TCP connection timeout + # TCP connection timeout + connect_timeout: 10_000, send_timeout: 5_000, recv_timeout: 5_000 ] @@ -430,6 +451,7 @@ defmodule WandererApp.Kills.Client do defp should_retry?(_), do: true defp should_start_new_retry_cycle?(%{last_retry_cycle_end: nil}), do: true + defp should_start_new_retry_cycle?(%{last_retry_cycle_end: end_time}) do System.system_time(:millisecond) - end_time >= @message_timeout end @@ -437,8 +459,9 @@ defmodule WandererApp.Kills.Client do # Prevent health check from triggering reconnects too frequently # Allow health check reconnects only every 2 minutes to avoid spam @health_check_reconnect_cooldown :timer.minutes(2) - + defp should_health_check_reconnect?(%{last_health_reconnect_attempt: nil}), do: true + defp should_health_check_reconnect?(%{last_health_reconnect_attempt: last_attempt}) do System.system_time(:millisecond) - last_attempt >= @health_check_reconnect_cooldown end @@ -449,14 +472,15 @@ defmodule WandererApp.Kills.Client do # Increment retry count first new_retry_count = state.retry_count + 1 - + # If we've hit max retries, mark the end of this retry cycle - state = if new_retry_count >= @max_retries do - %{state | last_retry_cycle_end: System.system_time(:millisecond)} - else - state - end - + state = + if new_retry_count >= @max_retries do + %{state | last_retry_cycle_end: System.system_time(:millisecond)} + else + state + end + delay = Enum.at(@retry_delays, min(state.retry_count, length(@retry_delays) - 1)) timer_ref = Process.send_after(self(), :retry_connection, delay) @@ -478,11 +502,13 @@ defmodule WandererApp.Kills.Client do end defp check_health(%{connecting: true} = _state) do - :healthy # Don't interfere with ongoing connection attempts + # Don't interfere with ongoing connection attempts + :healthy end defp check_health(%{connected: false, retry_timer_ref: ref} = _state) when not is_nil(ref) do - :healthy # Don't interfere with scheduled retries + # Don't interfere with scheduled retries + :healthy end defp check_health(%{connected: false} = state) do @@ -491,7 +517,8 @@ defmodule WandererApp.Kills.Client do if should_health_check_reconnect?(state) do :needs_reconnect_with_timestamp else - :healthy # Recent health check reconnect attempt + # Recent health check reconnect attempt + :healthy end else # Max retries reached, check if 15 minutes have passed since last retry cycle @@ -499,7 +526,8 @@ defmodule WandererApp.Kills.Client do Logger.info("[Client] 15 minutes elapsed since max retries, starting new retry cycle") :needs_reconnect_reset_retries else - :healthy # Still within 15-minute cooldown period + # Still within 15-minute cooldown period + :healthy end end end @@ -515,17 +543,21 @@ defmodule WandererApp.Kills.Client do end end - defp check_health(%{socket_pid: pid, last_message_time: last_msg_time} = state) when not is_nil(pid) and not is_nil(last_msg_time) do + defp check_health(%{socket_pid: pid, last_message_time: last_msg_time} = state) + when not is_nil(pid) and not is_nil(last_msg_time) do cond do not socket_alive?(pid) -> Logger.warning("[Client] Health check: Socket process #{inspect(pid)} is dead") :needs_reconnect - + # Check if we haven't received a message in the configured timeout System.system_time(:millisecond) - last_msg_time > @message_timeout -> - Logger.warning("[Client] Health check: No messages received for 15+ minutes, reconnecting") + Logger.warning( + "[Client] Health check: No messages received for 15+ minutes, reconnecting" + ) + :needs_reconnect - + true -> :healthy end @@ -571,7 +603,6 @@ defmodule WandererApp.Kills.Client do send(self(), {:disconnected, :connection_lost}) end - # Handler module for WebSocket events defmodule Handler do @moduledoc """ @@ -591,8 +622,10 @@ defmodule WandererApp.Kills.Client do # Configure with heartbeat interval (Phoenix default is 30s) params = [ {"vsn", "2.0.0"}, - {"heartbeat", "30000"} # 30 second heartbeat + # 30 second heartbeat + {"heartbeat", "30000"} ] + {:connect, ws_url, params, state} end @@ -644,7 +677,7 @@ defmodule WandererApp.Kills.Client do {"killmails:lobby", "killmail_update"} -> # Notify parent that we received a message send(state.parent, {:message_received, :killmail_update}) - + # Use supervised task to handle failures gracefully Task.Supervisor.start_child( WandererApp.Kills.TaskSupervisor, @@ -654,7 +687,7 @@ defmodule WandererApp.Kills.Client do {"killmails:lobby", "kill_count_update"} -> # Notify parent that we received a message send(state.parent, {:message_received, :kill_count_update}) - + # Use supervised task to handle failures gracefully Task.Supervisor.start_child( WandererApp.Kills.TaskSupervisor, @@ -677,6 +710,7 @@ defmodule WandererApp.Kills.Client do case push_to_channel(transport, "subscribe_systems", %{"systems" => system_ids}) do :ok -> Logger.debug(fn -> "[Handler] Successfully pushed subscribe_systems event" end) + error -> Logger.error("[Handler] Failed to push subscribe_systems: #{inspect(error)}") end @@ -689,6 +723,7 @@ defmodule WandererApp.Kills.Client do case push_to_channel(transport, "unsubscribe_systems", %{"systems" => system_ids}) do :ok -> Logger.debug(fn -> "[Handler] Successfully pushed unsubscribe_systems event" end) + error -> Logger.error("[Handler] Failed to push unsubscribe_systems: #{inspect(error)}") end @@ -720,12 +755,15 @@ defmodule WandererApp.Kills.Client do end defp push_to_channel(transport, event, payload) do - Logger.debug(fn -> "[Handler] Pushing event '#{event}' with payload: #{inspect(payload)}" end) + Logger.debug(fn -> + "[Handler] Pushing event '#{event}' with payload: #{inspect(payload)}" + end) case GenSocketClient.push(transport, "killmails:lobby", event, payload) do {:ok, ref} -> Logger.debug(fn -> "[Handler] Push successful, ref: #{inspect(ref)}" end) :ok + error -> Logger.error("[Handler] Push failed: #{inspect(error)}") error @@ -775,6 +813,7 @@ defmodule WandererApp.Kills.Client do system_ids |> Enum.reduce(%{}, fn system_id, acc -> maps = WandererApp.Kills.Subscription.SystemMapIndex.get_maps_for_system(system_id) + Enum.reduce(maps, acc, fn map_id, inner_acc -> Map.update(inner_acc, map_id, 1, &(&1 + 1)) end) diff --git a/lib/wanderer_app/kills/map_event_listener.ex b/lib/wanderer_app/kills/map_event_listener.ex index 67500890..01de4441 100644 --- a/lib/wanderer_app/kills/map_event_listener.ex +++ b/lib/wanderer_app/kills/map_event_listener.ex @@ -71,7 +71,10 @@ defmodule WandererApp.Kills.MapEventListener do end def handle_info({:systems_removed, system_ids}, state) do - Logger.debug(fn -> "[MapEventListener] Systems removed (alt format): #{length(system_ids)} systems" end) + Logger.debug(fn -> + "[MapEventListener] Systems removed (alt format): #{length(system_ids)} systems" + end) + # Track pending removals so we can handle them immediately new_pending_removals = MapSet.union(state.pending_removals, MapSet.new(system_ids)) {:noreply, schedule_subscription_update(%{state | pending_removals: new_pending_removals})} @@ -106,18 +109,21 @@ defmodule WandererApp.Kills.MapEventListener do def handle_info(:resubscribe_to_maps, state) do running_maps = WandererApp.Map.RegistryHelper.list_all_maps() current_running_map_ids = MapSet.new(Enum.map(running_maps, & &1.id)) + Logger.debug(fn -> "[MapEventListener] Resubscribing to maps. Running maps: #{MapSet.size(current_running_map_ids)}" end) # Unsubscribe from maps no longer running maps_to_unsubscribe = MapSet.difference(state.subscribed_maps, current_running_map_ids) + Enum.each(maps_to_unsubscribe, fn map_id -> Phoenix.PubSub.unsubscribe(WandererApp.PubSub, map_id) end) # Subscribe to new running maps maps_to_subscribe = MapSet.difference(current_running_map_ids, state.subscribed_maps) + Enum.each(maps_to_subscribe, fn map_id -> Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id) end) @@ -232,9 +238,10 @@ defmodule WandererApp.Kills.MapEventListener do defp apply_subscription_changes(current_systems, pending_removals) do current_set = MapSet.new(current_systems) + Logger.debug(fn -> "[MapEventListener] Current subscriptions: #{MapSet.size(current_set)} systems, " <> - "Pending removals: #{MapSet.size(pending_removals)} systems" + "Pending removals: #{MapSet.size(pending_removals)} systems" end) # Use get_tracked_system_ids to get only systems from running maps @@ -252,9 +259,10 @@ defmodule WandererApp.Kills.MapEventListener do # Remove pending removals from tracked_systems since DB might not be updated yet tracked_systems_adjusted = MapSet.difference(tracked_systems_set, pending_removals) + Logger.debug(fn -> "[MapEventListener] Tracked systems from maps: #{MapSet.size(tracked_systems_set)}, " <> - "After removing pending: #{MapSet.size(tracked_systems_adjusted)}" + "After removing pending: #{MapSet.size(tracked_systems_adjusted)}" end) # Use the existing MapIntegration logic to determine changes @@ -266,12 +274,18 @@ defmodule WandererApp.Kills.MapEventListener do # Apply the changes if to_subscribe != [] do - Logger.debug(fn -> "[MapEventListener] Triggering subscription for #{length(to_subscribe)} systems" end) + Logger.debug(fn -> + "[MapEventListener] Triggering subscription for #{length(to_subscribe)} systems" + end) + Client.subscribe_to_systems(to_subscribe) end if to_unsubscribe != [] do - Logger.debug(fn -> "[MapEventListener] Triggering unsubscription for #{length(to_unsubscribe)} systems" end) + Logger.debug(fn -> + "[MapEventListener] Triggering unsubscription for #{length(to_unsubscribe)} systems" + end) + Client.unsubscribe_from_systems(to_unsubscribe) end end diff --git a/lib/wanderer_app/kills/message_handler.ex b/lib/wanderer_app/kills/message_handler.ex index fc5726e3..f9fd734b 100644 --- a/lib/wanderer_app/kills/message_handler.ex +++ b/lib/wanderer_app/kills/message_handler.ex @@ -187,13 +187,11 @@ defmodule WandererApp.Kills.MessageHandler do # Pattern match on flat format - already adapted defp adapt_kill_data(%{"victim_char_id" => _} = kill) do validated_kill = validate_flat_format_kill(kill) - + if map_size(validated_kill) > 0 do {:ok, validated_kill} else - Logger.warning( - "[MessageHandler] Invalid flat format kill: #{inspect(kill["killmail_id"])}" - ) + Logger.warning("[MessageHandler] Invalid flat format kill: #{inspect(kill["killmail_id"])}") {:error, :invalid_data} end end @@ -219,7 +217,7 @@ defmodule WandererApp.Kills.MessageHandler do |> Map.delete("system_id") adapted_kill = adapt_nested_format_kill(normalized_kill) - + if map_size(adapted_kill) > 0 do {:ok, adapted_kill} else @@ -410,6 +408,7 @@ defmodule WandererApp.Kills.MessageHandler do defp get_character_name(data) when is_map(data) do # Try multiple possible field names field_names = ["attacker_name", "victim_name", "character_name", "name"] + extract_field(data, field_names) || case Map.get(data, "character") do %{"name" => name} when is_binary(name) -> name @@ -420,33 +419,38 @@ defmodule WandererApp.Kills.MessageHandler do defp get_character_name(_), do: nil @spec get_corp_ticker(map() | any()) :: String.t() | nil - defp get_corp_ticker(data) when is_map(data) do + defp get_corp_ticker(data) when is_map(data) do extract_field(data, ["corporation_ticker", "corp_ticker"]) end + defp get_corp_ticker(_), do: nil @spec get_corp_name(map() | any()) :: String.t() | nil defp get_corp_name(data) when is_map(data) do extract_field(data, ["corporation_name", "corp_name"]) end + defp get_corp_name(_), do: nil @spec get_alliance_ticker(map() | any()) :: String.t() | nil defp get_alliance_ticker(data) when is_map(data) do extract_field(data, ["alliance_ticker"]) end + defp get_alliance_ticker(_), do: nil @spec get_alliance_name(map() | any()) :: String.t() | nil defp get_alliance_name(data) when is_map(data) do extract_field(data, ["alliance_name"]) end + defp get_alliance_name(_), do: nil @spec get_ship_name(map() | any()) :: String.t() | nil defp get_ship_name(data) when is_map(data) do extract_field(data, ["ship_name", "ship_type_name"]) end + defp get_ship_name(_), do: nil defp get_and_validate_system_id(kill) do diff --git a/lib/wanderer_app/map/map_audit.ex b/lib/wanderer_app/map/map_audit.ex index d91b6fa1..8810f2d4 100644 --- a/lib/wanderer_app/map/map_audit.ex +++ b/lib/wanderer_app/map/map_audit.ex @@ -70,7 +70,8 @@ defmodule WandererApp.Map.Audit do def track_acl_event( event_type, %{user_id: user_id, acl_id: acl_id} = metadata - ) when not is_nil(user_id) and not is_nil(acl_id), + ) + when not is_nil(user_id) and not is_nil(acl_id), do: WandererApp.Api.UserActivity.new(%{ user_id: user_id, @@ -85,7 +86,8 @@ defmodule WandererApp.Map.Audit do def track_map_event( event_type, %{character_id: character_id, user_id: user_id, map_id: map_id} = metadata - ) when not is_nil(character_id) and not is_nil(user_id) and not is_nil(map_id), + ) + when not is_nil(character_id) and not is_nil(user_id) and not is_nil(map_id), do: WandererApp.Api.UserActivity.new(%{ character_id: character_id, diff --git a/lib/wanderer_app/map/map_manager.ex b/lib/wanderer_app/map/map_manager.ex index dd328e79..047f8fba 100644 --- a/lib/wanderer_app/map/map_manager.ex +++ b/lib/wanderer_app/map/map_manager.ex @@ -161,7 +161,6 @@ defmodule WandererApp.Map.Manager do case MapSystemSignature.by_deleted_and_updated_before!(true, delete_after_date) do {:ok, deleted_signatures} -> - Enum.each(deleted_signatures, fn sig -> Ash.destroy!(sig) end) @@ -174,8 +173,6 @@ defmodule WandererApp.Map.Manager do end end - - defp cleanup_expired_pings() do delete_after_date = DateTime.utc_now() |> DateTime.add(-1 * @pings_expire_minutes, :minute) diff --git a/lib/wanderer_app/map/map_operations.ex b/lib/wanderer_app/map/map_operations.ex index 6499cd3f..6dbe0dea 100644 --- a/lib/wanderer_app/map/map_operations.ex +++ b/lib/wanderer_app/map/map_operations.ex @@ -91,7 +91,8 @@ defmodule WandererApp.Map.Operations do defdelegate delete_connection(map_id, src_id, tgt_id), to: Connections @doc "Get a connection by source and target system IDs" - @spec get_connection_by_systems(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()} + @spec get_connection_by_systems(String.t(), integer(), integer()) :: + {:ok, map()} | {:error, String.t()} defdelegate get_connection_by_systems(map_id, source, target), to: Connections # -- Structures ------------------------------------------------------------ diff --git a/lib/wanderer_app/map/map_zkb_data_fetcher.ex b/lib/wanderer_app/map/map_zkb_data_fetcher.ex index b9cef28f..0fdce816 100644 --- a/lib/wanderer_app/map/map_zkb_data_fetcher.ex +++ b/lib/wanderer_app/map/map_zkb_data_fetcher.ex @@ -39,6 +39,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do # Update detailed kills for maps with active subscriptions {:ok, is_subscription_active} = map_id |> WandererApp.Map.is_subscription_active?() + if is_subscription_active do update_detailed_map_kills(map_id) end @@ -89,18 +90,25 @@ defmodule WandererApp.Map.ZkbDataFetcher do cache_key_ids = "map:#{map_id}:zkb:ids" cache_key_details = "map:#{map_id}:zkb:detailed_kills" - old_ids_map = case WandererApp.Cache.get(cache_key_ids) do - map when is_map(map) -> map - _ -> %{} - end + old_ids_map = + case WandererApp.Cache.get(cache_key_ids) do + map when is_map(map) -> map + _ -> %{} + end - old_details_map = case WandererApp.Cache.get(cache_key_details) do - map when is_map(map) -> map - _ -> - # Initialize with empty map and store it - WandererApp.Cache.insert(cache_key_details, %{}, ttl: :timer.hours(@killmail_ttl_hours)) - %{} - end + old_details_map = + case WandererApp.Cache.get(cache_key_details) do + map when is_map(map) -> + map + + _ -> + # Initialize with empty map and store it + WandererApp.Cache.insert(cache_key_details, %{}, + ttl: :timer.hours(@killmail_ttl_hours) + ) + + %{} + end # Build current killmail ID map from cache new_ids_map = @@ -117,7 +125,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do old_set = MapSet.new(Map.get(old_ids_map, system_id, [])) old_details = Map.get(old_details_map, system_id, []) # Update if IDs changed OR if we have IDs but no detailed kills - not MapSet.equal?(new_ids_set, old_set) or + not MapSet.equal?(new_ids_set, old_set) or (MapSet.size(new_ids_set) > 0 and old_details == []) end) |> Enum.map(&elem(&1, 0)) @@ -132,7 +140,8 @@ defmodule WandererApp.Map.ZkbDataFetcher do :ok else # Build new details for each changed system - updated_details_map = build_updated_details_map(changed_systems, old_details_map, new_ids_map) + updated_details_map = + build_updated_details_map(changed_systems, old_details_map, new_ids_map) # Update the ID map cache updated_ids_map = build_updated_ids_map(changed_systems, old_ids_map, new_ids_map) @@ -198,7 +207,10 @@ defmodule WandererApp.Map.ZkbDataFetcher do defp maybe_initialize_empty_details_map(%{}, systems, cache_key_details) do # First time initialization - create empty structure initial_map = Enum.into(systems, %{}, fn {system_id, _} -> {system_id, []} end) - WandererApp.Cache.insert(cache_key_details, initial_map, ttl: :timer.hours(@killmail_ttl_hours)) + + WandererApp.Cache.insert(cache_key_details, initial_map, + ttl: :timer.hours(@killmail_ttl_hours) + ) end defp maybe_initialize_empty_details_map(_old_details_map, _systems, _cache_key_details), do: :ok diff --git a/lib/wanderer_app/map/operations/connections.ex b/lib/wanderer_app/map/operations/connections.ex index 34abef87..7083b1ef 100644 --- a/lib/wanderer_app/map/operations/connections.ex +++ b/lib/wanderer_app/map/operations/connections.ex @@ -15,9 +15,9 @@ defmodule WandererApp.Map.Operations.Connections do @connection_type_stargate 1 # Ship size constants - @small_ship_size 0 + @small_ship_size 0 @medium_ship_size 1 - @large_ship_size 2 + @large_ship_size 2 @xlarge_ship_size 3 # System class constants @@ -40,13 +40,15 @@ defmodule WandererApp.Map.Operations.Connections do build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info) else {:error, reason} -> handle_precondition_error(reason, attrs) - {:ok, []} -> {:error, :inconsistent_state} - other -> {:error, :unexpected_precondition_error, other} + {:ok, []} -> {:error, :inconsistent_state} + other -> {:error, :unexpected_precondition_error, other} end end defp build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info) do - Logger.debug("[Connections] build_and_add_connection called with src_info: #{inspect(src_info)}, tgt_info: #{inspect(tgt_info)}") + Logger.debug( + "[Connections] build_and_add_connection called with src_info: #{inspect(src_info)}, tgt_info: #{inspect(tgt_info)}" + ) # Guard against nil info if is_nil(src_info) or is_nil(tgt_info) do @@ -61,12 +63,22 @@ defmodule WandererApp.Map.Operations.Connections do } case Server.add_connection(map_id, info) do - :ok -> {:ok, :created} - {:ok, []} -> log_warn_and(:inconsistent_state, info) + :ok -> + {:ok, :created} + + {:ok, []} -> + log_warn_and(:inconsistent_state, info) + {:error, %Invalid{errors: errs}} = err -> if Enum.any?(errs, &is_unique_constraint_error?/1), do: {:skip, :exists}, else: err - {:error, _} = err -> Logger.error("[add_connection] #{inspect(err)}"); {:error, :server_error} - other -> Logger.error("[add_connection] unexpected: #{inspect(other)}"); {:error, :unexpected_error} + + {:error, _} = err -> + Logger.error("[add_connection] #{inspect(err)}") + {:error, :server_error} + + other -> + Logger.error("[add_connection] unexpected: #{inspect(other)}") + {:error, :unexpected_error} end end end @@ -75,46 +87,55 @@ defmodule WandererApp.Map.Operations.Connections do type = parse_type(attrs["type"]) if type == @connection_type_wormhole and - (src_info.system_class == @c1_system_class or - tgt_info.system_class == @c1_system_class) do + (src_info.system_class == @c1_system_class or + tgt_info.system_class == @c1_system_class) do @medium_ship_size else parse_ship_size(attrs["ship_size_type"], @large_ship_size) end end - defp parse_ship_size(nil, default), do: default + defp parse_ship_size(nil, default), do: default defp parse_ship_size(val, _default) when is_integer(val), do: val + defp parse_ship_size(val, default) when is_binary(val) do case Integer.parse(val) do {i, _} -> i :error -> default end end - defp parse_ship_size(_, default), do: default + + defp parse_ship_size(_, default), do: default defp parse_type(nil), do: @connection_type_wormhole defp parse_type(val) when is_integer(val), do: val + defp parse_type(val) when is_binary(val) do case Integer.parse(val) do {i, _} -> i :error -> @connection_type_wormhole end end + defp parse_type(_), do: @connection_type_wormhole defp parse_int(nil, field), do: {:error, {:missing_field, field}} defp parse_int(val, _) when is_integer(val), do: {:ok, val} + defp parse_int(val, _) when is_binary(val) do case Integer.parse(val) do {i, _} -> {:ok, i} :error -> {:error, :invalid_integer} end end + defp parse_int(_, field), do: {:error, {:invalid_field, field}} defp handle_precondition_error(reason, attrs) do - Logger.warning("[add_connection] precondition failed: #{inspect(reason)} for #{inspect(attrs)}") + Logger.warning( + "[add_connection] precondition failed: #{inspect(reason)} for #{inspect(attrs)}" + ) + {:error, :precondition_failed, reason} end @@ -134,6 +155,7 @@ defmodule WandererApp.Map.Operations.Connections do {:error, err} -> Logger.warning("[list_connections] Repo error: #{inspect(err)}") {:error, :repo_error} + other -> Logger.error("[list_connections] Unexpected repo result: #{inspect(other)}") {:error, :unexpected_repo_result} @@ -157,28 +179,33 @@ defmodule WandererApp.Map.Operations.Connections do end @spec update_connection(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()} - def update_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, conn_id, attrs) do + def update_connection( + %{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, + conn_id, + attrs + ) do with {:ok, conn_struct} <- MapConnectionRepo.get_by_id(map_id, conn_id), - result <- ( - try do - _allowed_keys = [ - :mass_status, - :ship_size_type, - :type - ] - _update_map = - attrs - |> Enum.filter(fn {k, _v} -> k in ["mass_status", "ship_size_type", "type"] end) - |> Enum.map(fn {k, v} -> {String.to_atom(k), v} end) - |> Enum.into(%{}) - res = apply_connection_updates(map_id, conn_struct, attrs, char_id) - res - rescue - error -> - Logger.error("[update_connection] Exception: #{inspect(error)}") - {:error, :exception} - end - ), + result <- + (try do + _allowed_keys = [ + :mass_status, + :ship_size_type, + :type + ] + + _update_map = + attrs + |> Enum.filter(fn {k, _v} -> k in ["mass_status", "ship_size_type", "type"] end) + |> Enum.map(fn {k, v} -> {String.to_atom(k), v} end) + |> Enum.into(%{}) + + res = apply_connection_updates(map_id, conn_struct, attrs, char_id) + res + rescue + error -> + Logger.error("[update_connection] Exception: #{inspect(error)}") + {:error, :exception} + end), :ok <- result, {:ok, updated_conn} <- MapConnectionRepo.get_by_id(map_id, conn_id) do {:ok, updated_conn} @@ -187,29 +214,46 @@ defmodule WandererApp.Map.Operations.Connections do _ -> {:error, :unexpected_error} end end + def update_connection(_conn, _conn_id, _attrs), do: {:error, :missing_params} @spec delete_connection(Plug.Conn.t(), integer(), integer()) :: :ok | {:error, atom()} def delete_connection(%{assigns: %{map_id: map_id}} = _conn, src, tgt) do - case Server.delete_connection(map_id, %{solar_system_source_id: src, solar_system_target_id: tgt}) do - :ok -> :ok + case Server.delete_connection(map_id, %{ + solar_system_source_id: src, + solar_system_target_id: tgt + }) do + :ok -> + :ok + {:error, :not_found} -> - Logger.warning("[delete_connection] Connection not found: source=#{inspect(src)}, target=#{inspect(tgt)}") + Logger.warning( + "[delete_connection] Connection not found: source=#{inspect(src)}, target=#{inspect(tgt)}" + ) + {:error, :not_found} + {:error, _} = err -> Logger.error("[delete_connection] Server error: #{inspect(err)}") {:error, :server_error} + _ -> Logger.error("[delete_connection] Unknown error") {:error, :unknown} end end + def delete_connection(_conn, _src, _tgt), do: {:error, :missing_params} @doc "Batch upsert for connections" - @spec upsert_batch(Plug.Conn.t(), [map()]) :: %{created: integer(), updated: integer(), skipped: integer()} + @spec upsert_batch(Plug.Conn.t(), [map()]) :: %{ + created: integer(), + updated: integer(), + skipped: integer() + } def upsert_batch(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conns) do _assigns = %{map_id: map_id, char_id: char_id} + Enum.reduce(conns, %{created: 0, updated: 0, skipped: 0}, fn conn_attrs, acc -> case upsert_single(conn, conn_attrs) do {:ok, :created} -> %{acc | created: acc.created + 1} @@ -218,6 +262,7 @@ defmodule WandererApp.Map.Operations.Connections do end end) end + def upsert_batch(_conn, _conns), do: %{created: 0, updated: 0, skipped: 0} @doc "Upsert a single connection" @@ -225,6 +270,7 @@ defmodule WandererApp.Map.Operations.Connections do def upsert_single(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = conn, conn_data) do source = conn_data["solar_system_source"] || conn_data[:solar_system_source] target = conn_data["solar_system_target"] || conn_data[:solar_system_target] + with {:ok, %{} = existing_conn} <- get_connection_by_systems(map_id, source, target), {:ok, _} <- update_connection(conn, existing_conn.id, conn_data) do {:ok, :updated} @@ -235,18 +281,22 @@ defmodule WandererApp.Map.Operations.Connections do {:skip, :exists} -> {:ok, :updated} err -> {:error, err} end + {:error, _} = err -> Logger.warning("[upsert_single] Connection lookup error: #{inspect(err)}") {:error, :lookup_error} + err -> Logger.error("[upsert_single] Update failed: #{inspect(err)}") {:error, :unexpected_error} end end + def upsert_single(_conn, _conn_data), do: {:error, :missing_params} @doc "Get a connection by source and target system IDs" - @spec get_connection_by_systems(String.t(), integer(), integer()) :: {:ok, map()} | {:error, String.t()} + @spec get_connection_by_systems(String.t(), integer(), integer()) :: + {:ok, map()} | {:error, String.t()} def get_connection_by_systems(map_id, source, target) do with {:ok, conn} <- WandererApp.Map.find_connection(map_id, source, target) do if conn, do: {:ok, conn}, else: WandererApp.Map.find_connection(map_id, target, source) @@ -266,6 +316,7 @@ defmodule WandererApp.Map.Operations.Connections do "type" -> maybe_update_type(map_id, conn, val) _ -> :ok end + if result == :ok do {:cont, :ok} else @@ -279,6 +330,7 @@ defmodule WandererApp.Map.Operations.Connections do end defp maybe_update_mass_status(_map_id, _conn, nil), do: :ok + defp maybe_update_mass_status(map_id, conn, value) do Server.update_connection_mass_status(map_id, %{ solar_system_source_id: conn.solar_system_source, @@ -288,6 +340,7 @@ defmodule WandererApp.Map.Operations.Connections do end defp maybe_update_ship_size_type(_map_id, _conn, nil), do: :ok + defp maybe_update_ship_size_type(map_id, conn, value) do Server.update_connection_ship_size_type(map_id, %{ solar_system_source_id: conn.solar_system_source, @@ -297,6 +350,7 @@ defmodule WandererApp.Map.Operations.Connections do end defp maybe_update_type(_map_id, _conn, nil), do: :ok + defp maybe_update_type(map_id, conn, value) do Server.update_connection_type(map_id, %{ solar_system_source_id: conn.solar_system_source, @@ -306,15 +360,16 @@ defmodule WandererApp.Map.Operations.Connections do end @doc "Creates a connection between two systems" - @spec create_connection(String.t(), map(), String.t()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()} + @spec create_connection(String.t(), map(), String.t()) :: + {:ok, :created} | {:skip, :exists} | {:error, atom()} def create_connection(map_id, attrs, char_id) do do_create(attrs, map_id, char_id) end @doc "Creates a connection between two systems from a Plug.Conn" - @spec create_connection(Plug.Conn.t(), map()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()} + @spec create_connection(Plug.Conn.t(), map()) :: + {:ok, :created} | {:skip, :exists} | {:error, atom()} def create_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, attrs) do do_create(attrs, map_id, char_id) end - end diff --git a/lib/wanderer_app/map/operations/owner.ex b/lib/wanderer_app/map/operations/owner.ex index a24fef5a..77bf2e83 100644 --- a/lib/wanderer_app/map/operations/owner.ex +++ b/lib/wanderer_app/map/operations/owner.ex @@ -12,10 +12,12 @@ defmodule WandererApp.Map.Operations.Owner do MapUserSettingsRepo, Cache } + alias WandererApp.Character alias WandererApp.Character.TrackingUtils - @spec get_owner_character_id(String.t()) :: {:ok, %{id: term(), user_id: term()}} | {:error, String.t()} + @spec get_owner_character_id(String.t()) :: + {:ok, %{id: term(), user_id: term()}} | {:error, String.t()} def get_owner_character_id(map_id) do cache_key = "map_#{map_id}:owner_info" @@ -25,7 +27,8 @@ defmodule WandererApp.Map.Operations.Owner do {:ok, char_ids} <- fetch_character_ids(map_id), {:ok, characters} <- load_characters(char_ids), {:ok, user_settings} <- MapUserSettingsRepo.get(map_id, owner.id), - {:ok, main} <- TrackingUtils.get_main_character(user_settings, characters, characters) do + {:ok, main} <- + TrackingUtils.get_main_character(user_settings, characters, characters) do result = %{id: main.id, user_id: main.user_id} Cache.insert(cache_key, result, ttl: @owner_info_cache_ttl) {:ok, result} diff --git a/lib/wanderer_app/map/operations/signatures.ex b/lib/wanderer_app/map/operations/signatures.ex index 6e01ec48..44564586 100644 --- a/lib/wanderer_app/map/operations/signatures.ex +++ b/lib/wanderer_app/map/operations/signatures.ex @@ -11,6 +11,7 @@ defmodule WandererApp.Map.Operations.Signatures do @spec list_signatures(String.t()) :: [map()] def list_signatures(map_id) do systems = Operations.list_systems(map_id) + if systems != [] do systems |> Enum.flat_map(fn sys -> @@ -28,18 +29,25 @@ defmodule WandererApp.Map.Operations.Signatures do end @spec create_signature(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()} - def create_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, %{"solar_system_id" => _solar_system_id} = params) do + def create_signature( + %{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = + _conn, + %{"solar_system_id" => _solar_system_id} = params + ) do attrs = Map.put(params, "character_eve_id", char_id) + case Server.update_signatures(map_id, %{ - added_signatures: [attrs], - updated_signatures: [], - removed_signatures: [], - solar_system_id: params["solar_system_id"], - character_id: char_id, - user_id: user_id, - delete_connection_with_sigs: false - }) do - :ok -> {:ok, attrs} + added_signatures: [attrs], + updated_signatures: [], + removed_signatures: [], + solar_system_id: params["solar_system_id"], + character_id: char_id, + user_id: user_id, + delete_connection_with_sigs: false + }) do + :ok -> + {:ok, attrs} + err -> Logger.error("[create_signature] Unexpected error: #{inspect(err)}") {:error, :unexpected_error} @@ -49,7 +57,12 @@ defmodule WandererApp.Map.Operations.Signatures do def create_signature(_conn, _params), do: {:error, :missing_params} @spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()} - def update_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, sig_id, params) do + def update_signature( + %{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = + _conn, + sig_id, + params + ) do with {:ok, sig} <- MapSystemSignature.by_id(sig_id), {:ok, system} <- MapSystem.by_id(sig.system_id) do base = %{ @@ -63,16 +76,20 @@ defmodule WandererApp.Map.Operations.Signatures do "description" => sig.description, "linked_system_id" => sig.linked_system_id } + attrs = Map.merge(base, params) - :ok = Server.update_signatures(map_id, %{ - added_signatures: [], - updated_signatures: [attrs], - removed_signatures: [], - solar_system_id: system.solar_system_id, - character_id: char_id, - user_id: user_id, - delete_connection_with_sigs: false - }) + + :ok = + Server.update_signatures(map_id, %{ + added_signatures: [], + updated_signatures: [attrs], + removed_signatures: [], + solar_system_id: system.solar_system_id, + character_id: char_id, + user_id: user_id, + delete_connection_with_sigs: false + }) + {:ok, attrs} else err -> @@ -84,24 +101,33 @@ defmodule WandererApp.Map.Operations.Signatures do def update_signature(_conn, _sig_id, _params), do: {:error, :missing_params} @spec delete_signature(Plug.Conn.t(), String.t()) :: :ok | {:error, atom()} - def delete_signature(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, sig_id) do + def delete_signature( + %{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = + _conn, + sig_id + ) do with {:ok, sig} <- MapSystemSignature.by_id(sig_id), {:ok, system} <- MapSystem.by_id(sig.system_id) do - removed = [%{ - "eve_id" => sig.eve_id, - "name" => sig.name, - "kind" => sig.kind, - "group" => sig.group - }] - :ok = Server.update_signatures(map_id, %{ - added_signatures: [], - updated_signatures: [], - removed_signatures: removed, - solar_system_id: system.solar_system_id, - character_id: char_id, - user_id: user_id, - delete_connection_with_sigs: false - }) + removed = [ + %{ + "eve_id" => sig.eve_id, + "name" => sig.name, + "kind" => sig.kind, + "group" => sig.group + } + ] + + :ok = + Server.update_signatures(map_id, %{ + added_signatures: [], + updated_signatures: [], + removed_signatures: removed, + solar_system_id: system.solar_system_id, + character_id: char_id, + user_id: user_id, + delete_connection_with_sigs: false + }) + :ok else err -> diff --git a/lib/wanderer_app/map/operations/structures.ex b/lib/wanderer_app/map/operations/structures.ex index 9948fae6..18a220b1 100644 --- a/lib/wanderer_app/map/operations/structures.ex +++ b/lib/wanderer_app/map/operations/structures.ex @@ -11,13 +11,12 @@ defmodule WandererApp.Map.Operations.Structures do @spec list_structures(String.t()) :: [map()] def list_structures(map_id) do - with systems when is_list(systems) and systems != [] <- ( - case Operations.list_systems(map_id) do - {:ok, systems} -> systems - systems when is_list(systems) -> systems - _ -> [] - end - ) do + with systems when is_list(systems) and systems != [] <- + (case Operations.list_systems(map_id) do + {:ok, systems} -> systems + systems when is_list(systems) -> systems + _ -> [] + end) do systems |> Enum.flat_map(fn sys -> with {:ok, structs} <- MapSystemStructure.by_system_id(sys.id) do @@ -32,8 +31,16 @@ defmodule WandererApp.Map.Operations.Structures do end @spec create_structure(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()} - def create_structure(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, %{"solar_system_id" => _solar_system_id} = params) do - with {:ok, system} <- MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: params["solar_system_id"]}), + def create_structure( + %{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = + _conn, + %{"solar_system_id" => _solar_system_id} = params + ) do + with {:ok, system} <- + MapSystem.read_by_map_and_solar_system(%{ + map_id: map_id, + solar_system_id: params["solar_system_id"] + }), attrs <- Map.put(prepare_attrs(params), "system_id", system.id), :ok <- Structure.update_structures(system, [attrs], [], [], char_id, user_id), name = Map.get(attrs, "name"), @@ -46,6 +53,7 @@ defmodule WandererApp.Map.Operations.Structures do nil -> Logger.warning("[create_structure] Structure not found after creation") {:error, :structure_not_found} + err -> Logger.error("[create_structure] Unexpected error: #{inspect(err)}") {:error, :unexpected_error} @@ -55,13 +63,25 @@ defmodule WandererApp.Map.Operations.Structures do def create_structure(_conn, _params), do: {:error, "missing params"} @spec update_structure(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()} - def update_structure(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, struct_id, params) do + def update_structure( + %{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = + _conn, + struct_id, + params + ) do with {:ok, struct} <- MapSystemStructure.by_id(struct_id), - {:ok, system} <- MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: struct.solar_system_id}) do + {:ok, system} <- + MapSystem.read_by_map_and_solar_system(%{ + map_id: map_id, + solar_system_id: struct.solar_system_id + }) do attrs = Map.merge(prepare_attrs(params), %{"id" => struct_id}) :ok = Structure.update_structures(system, [], [attrs], [], char_id, user_id) + case MapSystemStructure.by_id(struct_id) do - {:ok, updated} -> {:ok, updated} + {:ok, updated} -> + {:ok, updated} + err -> Logger.error("[update_structure] Unexpected error: #{inspect(err)}") {:error, :unexpected_error} @@ -76,7 +96,11 @@ defmodule WandererApp.Map.Operations.Structures do def update_structure(_conn, _struct_id, _params), do: {:error, "missing params"} @spec delete_structure(Plug.Conn.t(), String.t()) :: :ok | {:error, atom()} - def delete_structure(%{assigns: %{map_id: _map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, struct_id) do + def delete_structure( + %{assigns: %{map_id: _map_id, owner_character_id: char_id, owner_user_id: user_id}} = + _conn, + struct_id + ) do with {:ok, struct} <- MapSystemStructure.by_id(struct_id), {:ok, system} <- MapSystem.by_id(struct.system_id) do :ok = Structure.update_structures(system, [], [], [%{"id" => struct_id}], char_id, user_id) diff --git a/lib/wanderer_app/map/operations/systems.ex b/lib/wanderer_app/map/operations/systems.ex index e11d3b1e..014413a9 100644 --- a/lib/wanderer_app/map/operations/systems.ex +++ b/lib/wanderer_app/map/operations/systems.ex @@ -23,9 +23,14 @@ defmodule WandererApp.Map.Operations.Systems do end @spec create_system(Plug.Conn.t(), map()) :: {:ok, map()} | {:error, atom()} - def create_system(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, params) do + def create_system( + %{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = + _conn, + params + ) do do_create_system(map_id, user_id, char_id, params) end + def create_system(_conn, _params), do: {:error, :missing_params} # Private helper for batch upsert @@ -36,13 +41,20 @@ defmodule WandererApp.Map.Operations.Systems do defp do_create_system(map_id, user_id, char_id, params) do with {:ok, system_id} <- fetch_system_id(params), coords <- normalize_coordinates(params), - :ok <- Server.add_system(map_id, %{solar_system_id: system_id, coordinates: coords}, user_id, char_id), + :ok <- + Server.add_system( + map_id, + %{solar_system_id: system_id, coordinates: coords}, + user_id, + char_id + ), {:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do {:ok, system} else {:error, reason} when is_binary(reason) -> Logger.warning("[do_create_system] Expected error: #{inspect(reason)}") {:error, :expected_error} + _ -> Logger.error("[do_create_system] Unexpected error") {:error, :unexpected_error} @@ -64,15 +76,21 @@ defmodule WandererApp.Map.Operations.Systems do {:error, reason} when is_binary(reason) -> Logger.warning("[update_system] Expected error: #{inspect(reason)}") {:error, :expected_error} + _ -> Logger.error("[update_system] Unexpected error") {:error, :unexpected_error} end end + def update_system(_conn, _system_id, _attrs), do: {:error, :missing_params} @spec delete_system(Plug.Conn.t(), integer()) :: {:ok, integer()} | {:error, atom()} - def delete_system(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = _conn, system_id) do + def delete_system( + %{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = + _conn, + system_id + ) do with {:ok, _} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id), :ok <- Server.delete_systems(map_id, [system_id], user_id, char_id) do {:ok, 1} @@ -80,17 +98,27 @@ defmodule WandererApp.Map.Operations.Systems do {:error, :not_found} -> Logger.warning("[delete_system] System not found: #{inspect(system_id)}") {:error, :not_found} + _ -> Logger.error("[delete_system] Unexpected error") {:error, :unexpected_error} end end + def delete_system(_conn, _system_id), do: {:error, :missing_params} - @spec upsert_systems_and_connections(Plug.Conn.t(), [map()], [map()]) :: {:ok, map()} | {:error, atom()} - def upsert_systems_and_connections(%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = conn, systems, connections) do + @spec upsert_systems_and_connections(Plug.Conn.t(), [map()], [map()]) :: + {:ok, map()} | {:error, atom()} + def upsert_systems_and_connections( + %{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} = conn, + systems, + connections + ) do assigns = %{map_id: map_id, user_id: user_id, char_id: char_id} - {created_s, updated_s, _skipped_s} = upsert_each(systems, fn sys -> create_system_batch(assigns, sys) end, 0, 0, 0) + + {created_s, updated_s, _skipped_s} = + upsert_each(systems, fn sys -> create_system_batch(assigns, sys) end, 0, 0, 0) + conn_results = connections |> Enum.reduce(%{created: 0, updated: 0, skipped: 0}, fn conn_data, acc -> @@ -100,33 +128,44 @@ defmodule WandererApp.Map.Operations.Systems do _ -> %{acc | skipped: acc.skipped + 1} end end) - {:ok, %{ - systems: %{created: created_s, updated: updated_s}, - connections: %{created: conn_results.created, updated: conn_results.updated} - }} + + {:ok, + %{ + systems: %{created: created_s, updated: updated_s}, + connections: %{created: conn_results.created, updated: conn_results.updated} + }} end + def upsert_systems_and_connections(_conn, _systems, _connections), do: {:error, :missing_params} # -- Internal Helpers ------------------------------------------------------- defp fetch_system_id(%{"solar_system_id" => id}), do: parse_int(id, "solar_system_id") - defp fetch_system_id(%{solar_system_id: id}) when not is_nil(id), do: parse_int(id, "solar_system_id") + + defp fetch_system_id(%{solar_system_id: id}) when not is_nil(id), + do: parse_int(id, "solar_system_id") + defp fetch_system_id(_), do: {:error, "Missing system identifier (id)"} defp parse_int(val, _field) when is_integer(val), do: {:ok, val} + defp parse_int(val, field) when is_binary(val) do case Integer.parse(val) do {i, _} -> {:ok, i} _ -> {:error, "Invalid #{field}: #{val}"} end end + defp parse_int(nil, field), do: {:error, "Missing #{field}"} defp parse_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"} - defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}}) when is_number(x) and is_number(y), - do: %{x: x, y: y} + defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}}) + when is_number(x) and is_number(y), + do: %{x: x, y: y} + defp normalize_coordinates(%{coordinates: %{x: x, y: y}}) when is_number(x) and is_number(y), do: %{x: x, y: y} + defp normalize_coordinates(params) do %{ x: params |> Map.get("position_x", Map.get(params, :position_x, 0)), @@ -135,10 +174,23 @@ defmodule WandererApp.Map.Operations.Systems do end defp apply_system_updates(map_id, system_id, attrs, %{x: x, y: y}) do - with :ok <- Server.update_system_position(map_id, %{solar_system_id: system_id, position_x: round(x), position_y: round(y)}) do + with :ok <- + Server.update_system_position(map_id, %{ + solar_system_id: system_id, + position_x: round(x), + position_y: round(y) + }) do attrs - |> Map.drop([:coordinates, :position_x, :position_y, :solar_system_id, - "coordinates", "position_x", "position_y", "solar_system_id"]) + |> Map.drop([ + :coordinates, + :position_x, + :position_y, + :solar_system_id, + "coordinates", + "position_x", + "position_y", + "solar_system_id" + ]) |> Enum.reduce_while(:ok, fn {key, val}, _ -> case update_system_field(map_id, system_id, to_string(key), val) do :ok -> {:cont, :ok} @@ -150,21 +202,43 @@ defmodule WandererApp.Map.Operations.Systems do defp update_system_field(map_id, system_id, field, val) do case field do - "status" -> Server.update_system_status(map_id, %{solar_system_id: system_id, status: convert_status(val)}) - "description" -> Server.update_system_description(map_id, %{solar_system_id: system_id, description: val}) - "tag" -> Server.update_system_tag(map_id, %{solar_system_id: system_id, tag: val}) + "status" -> + Server.update_system_status(map_id, %{ + solar_system_id: system_id, + status: convert_status(val) + }) + + "description" -> + Server.update_system_description(map_id, %{solar_system_id: system_id, description: val}) + + "tag" -> + Server.update_system_tag(map_id, %{solar_system_id: system_id, tag: val}) + "locked" -> bool = val in [true, "true", 1, "1"] Server.update_system_locked(map_id, %{solar_system_id: system_id, locked: bool}) + f when f in ["label", "labels"] -> - labels = cond do - is_list(val) -> val - is_binary(val) -> String.split(val, ",", trim: true) - true -> [] - end - Server.update_system_labels(map_id, %{solar_system_id: system_id, labels: Enum.join(labels, ",")}) - "temporary_name" -> Server.update_system_temporary_name(map_id, %{solar_system_id: system_id, temporary_name: val}) - _ -> :ok + labels = + cond do + is_list(val) -> val + is_binary(val) -> String.split(val, ",", trim: true) + true -> [] + end + + Server.update_system_labels(map_id, %{ + solar_system_id: system_id, + labels: Enum.join(labels, ",") + }) + + "temporary_name" -> + Server.update_system_temporary_name(map_id, %{ + solar_system_id: system_id, + temporary_name: val + }) + + _ -> + :ok end end @@ -175,15 +249,18 @@ defmodule WandererApp.Map.Operations.Systems do defp convert_status("TIME_CRITICAL"), do: 4 defp convert_status("REINFORCED"), do: 5 defp convert_status(i) when is_integer(i), do: i + defp convert_status(s) when is_binary(s) do case Integer.parse(s) do {i, _} -> i _ -> 0 end end + defp convert_status(_), do: 0 defp upsert_each([], _fun, c, u, d), do: {c, u, d} + defp upsert_each([item | rest], fun, c, u, d) do case fun.(item) do {:ok, _} -> upsert_each(rest, fun, c + 1, u, d) diff --git a/lib/wanderer_app/map/server/map_server_connections_impl.ex b/lib/wanderer_app/map/server/map_server_connections_impl.ex index a59e5614..df4bb5d0 100644 --- a/lib/wanderer_app/map/server/map_server_connections_impl.ex +++ b/lib/wanderer_app/map/server/map_server_connections_impl.ex @@ -359,13 +359,15 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do {:ok, target_system_info} = get_system_static_info(location.solar_system_id) # Set ship size type to medium only for wormhole connections involving C1 systems - ship_size_type = if connection_type == @connection_type_wormhole and - (source_system_info.system_class == @c1 or - target_system_info.system_class == @c1) do - @medium_ship_size - else - 2 # Default to large for non-wormhole or non-C1 connections - end + ship_size_type = + if connection_type == @connection_type_wormhole and + (source_system_info.system_class == @c1 or + target_system_info.system_class == @c1) do + @medium_ship_size + else + # Default to large for non-wormhole or non-C1 connections + 2 + end {:ok, connection} = WandererApp.MapConnectionRepo.create(%{ diff --git a/lib/wanderer_app/utils/eve_util.ex b/lib/wanderer_app/utils/eve_util.ex index e2393068..86130e04 100644 --- a/lib/wanderer_app/utils/eve_util.ex +++ b/lib/wanderer_app/utils/eve_util.ex @@ -18,8 +18,13 @@ defmodule WandererApp.Utils.EVEUtil do "https://images.evetech.net/characters/12345678/portrait?size=128" """ def get_portrait_url(eve_id, size \\ 64) - def get_portrait_url(nil, size), do: "https://images.evetech.net/characters/0/portrait?size=#{size}" - def get_portrait_url("", size), do: "https://images.evetech.net/characters/0/portrait?size=#{size}" + + def get_portrait_url(nil, size), + do: "https://images.evetech.net/characters/0/portrait?size=#{size}" + + def get_portrait_url("", size), + do: "https://images.evetech.net/characters/0/portrait?size=#{size}" + def get_portrait_url(eve_id, size) do "https://images.evetech.net/characters/#{eve_id}/portrait?size=#{size}" end diff --git a/lib/wanderer_app_web/components/core_components.ex b/lib/wanderer_app_web/components/core_components.ex index 8bc396fe..57273377 100644 --- a/lib/wanderer_app_web/components/core_components.ex +++ b/lib/wanderer_app_web/components/core_components.ex @@ -88,7 +88,7 @@ defmodule WandererAppWeb.CoreComponents do ]} >
- <%= render_slot(@inner_block) %> + {render_slot(@inner_block)}