Compare commits

...

54 Commits

Author SHA1 Message Date
CI
0b7c067de7 chore: release version v1.77.4 2025-09-02 10:26:47 +00:00
Dmitry Popov
0d0db8c129 Merge pull request #509 from guarzo/guarzo/aclapi
fix: ensure pub/sub occurs after acl api change
2025-09-02 14:26:20 +04:00
guarzo
378df0ac70 fix: pr feedback 2025-09-02 00:27:40 +00:00
guarzo
0e4a132f69 refactor: dry 2025-09-01 22:38:12 +00:00
guarzo
631746375d fix: ensure pub/sub occurs after acl api change 2025-09-01 22:11:58 +00:00
CI
7dc01dad54 chore: [skip ci] 2025-08-29 00:33:30 +00:00
CI
8a9807d3e5 chore: release version v1.77.3 2025-08-29 00:33:30 +00:00
Dmitry Popov
39df3c97ce Merge pull request #505 from wanderer-industries/tracking-fix
Tracking fix
2025-08-29 04:33:00 +04:00
Dmitry Popov
46c1ccdfcc fix: Fixed character tracking settings 2025-08-29 02:31:00 +02:00
Dmitry Popov
8817536038 fix: Fixed character tracking settings 2025-08-29 02:30:19 +02:00
Dmitry Popov
c3bb23a6ee fix: Fixed character tracking settings 2025-08-29 01:41:08 +02:00
Dmitry Popov
7e9c4c575e fix: Fixed character tracking settings 2025-08-28 22:18:18 +02:00
CI
5a70eee91e chore: [skip ci] 2025-08-28 10:24:51 +00:00
CI
228f6990a1 chore: release version v1.77.2 2025-08-28 10:24:51 +00:00
Dmitry Popov
d80ed0e70e Merge pull request #504 from guarzo/guarzo/sigapi
fix: update system signature api to return correct system id
2025-08-28 14:24:26 +04:00
CI
4576c75737 chore: [skip ci] 2025-08-28 10:03:36 +00:00
CI
67764faaa7 chore: release version v1.77.1 2025-08-28 10:03:36 +00:00
Dmitry Popov
91dd0b27ae chore: Added support for limited telemetry (base only). 2025-08-28 12:03:01 +02:00
guarzo
99dcf49fbc Merge branch 'main' into guarzo/sigapi 2025-08-27 21:30:59 -04:00
guarzo
6fb3edbfd6 fix: update system signature api to return correct system id 2025-08-28 01:30:37 +00:00
CI
26f13ce857 chore: [skip ci] 2025-08-27 21:18:31 +00:00
CI
e9b475c0a8 chore: release version v1.77.0 2025-08-27 21:18:31 +00:00
Dmitry Popov
7752010092 feat(Core): Reduced DB calls to check existing system jumps 2025-08-27 23:17:58 +02:00
CI
d3705b3ed7 chore: [skip ci] 2025-08-27 20:46:18 +00:00
CI
1394e2897e chore: release version v1.76.13 2025-08-27 20:46:18 +00:00
Dmitry Popov
5117a1c5af fix(Core): Fixed maps start timeout 2025-08-27 22:42:29 +02:00
CI
3c62403f33 chore: [skip ci] 2025-08-20 14:37:18 +00:00
CI
a4760f5162 chore: release version v1.76.12 2025-08-20 14:37:18 +00:00
Dmitry Popov
b071070431 fix(Core): Reduced ESI api calls to update character corp/ally info 2025-08-20 16:36:46 +02:00
CI
3bcb9628e7 chore: [skip ci] 2025-08-20 07:53:27 +00:00
CI
e62c4cf5bf chore: release version v1.76.11 2025-08-20 07:53:27 +00:00
Dmitry Popov
af46962ce4 Merge pull request #503 from wanderer-industries/revert-501-guarzo/sigsfix
Revert "fix: default signature types not being shown"
2025-08-20 11:53:00 +04:00
Dmitry Popov
0b0967830b Revert "fix: default signature types not being shown" 2025-08-20 11:52:34 +04:00
CI
172251a208 chore: [skip ci] 2025-08-18 23:28:33 +00:00
CI
8a6fb63d55 chore: release version v1.76.10 2025-08-18 23:28:33 +00:00
Dmitry Popov
9652959e5e fix(Core): Added character trackers start queue 2025-08-19 01:27:58 +02:00
CI
825ef46d41 chore: [skip ci] 2025-08-18 11:42:47 +00:00
CI
ad9f7c6b95 chore: release version v1.76.9 2025-08-18 11:42:47 +00:00
Dmitry Popov
b960b5c149 Merge pull request #501 from guarzo/guarzo/sigsfix
fix: default signature types not being shown
2025-08-18 15:42:14 +04:00
CI
0f092d21f9 chore: [skip ci] 2025-08-17 21:28:20 +00:00
CI
031576caa6 chore: release version v1.76.8 2025-08-17 21:28:20 +00:00
Dmitry Popov
7a97a96c42 fix(Core): added DB connection default timeouts 2025-08-17 23:27:21 +02:00
CI
2efb2daba0 chore: [skip ci] 2025-08-16 22:17:44 +00:00
CI
4374c39924 chore: release version v1.76.7 2025-08-16 22:17:44 +00:00
Dmitry Popov
15711495c7 fix(Core): Fixed auth redirect URL 2025-08-17 00:17:17 +02:00
guarzo
236f803427 fix: default signature types not being shown 2025-08-15 23:03:22 +00:00
CI
6772130f2a chore: [skip ci] 2025-08-15 15:27:07 +00:00
CI
ddd72f3fac chore: release version v1.76.6 2025-08-15 15:27:07 +00:00
Dmitry Popov
6e262835ef Merge pull request #500 from guarzo/guarzo/moressefix
fix: empty subscriptions for sse
2025-08-15 19:26:34 +04:00
guarzo
2f3b8ddc5f fix: empty subscriptions for sse 2025-08-15 11:08:40 -04:00
CI
cea3a74b34 chore: [skip ci] 2025-08-15 10:29:11 +00:00
CI
867941a233 chore: release version v1.76.5 2025-08-15 10:29:11 +00:00
Dmitry Popov
3ff388a16d fix(Core): fixed tracking paused issues, fixed user activity data 2025-08-15 12:28:36 +02:00
CI
f4248e9ab9 chore: [skip ci] 2025-08-14 23:40:20 +00:00
40 changed files with 825 additions and 490 deletions

View File

@@ -2,6 +2,132 @@
<!-- changelog -->
## [v1.77.4](https://github.com/wanderer-industries/wanderer/compare/v1.77.3...v1.77.4) (2025-09-02)
### Bug Fixes:
* pr feedback
* ensure pub/sub occurs after acl api change
## [v1.77.3](https://github.com/wanderer-industries/wanderer/compare/v1.77.2...v1.77.3) (2025-08-29)
### Bug Fixes:
* Fixed character tracking settings
* Fixed character tracking settings
* Fixed character tracking settings
* Fixed character tracking settings
## [v1.77.2](https://github.com/wanderer-industries/wanderer/compare/v1.77.1...v1.77.2) (2025-08-28)
### Bug Fixes:
* update system signature api to return correct system id
## [v1.77.1](https://github.com/wanderer-industries/wanderer/compare/v1.77.0...v1.77.1) (2025-08-28)
## [v1.77.0](https://github.com/wanderer-industries/wanderer/compare/v1.76.13...v1.77.0) (2025-08-27)
### Features:
* Core: Reduced DB calls to check existing system jumps
## [v1.76.13](https://github.com/wanderer-industries/wanderer/compare/v1.76.12...v1.76.13) (2025-08-27)
### Bug Fixes:
* Core: Fixed maps start timeout
## [v1.76.12](https://github.com/wanderer-industries/wanderer/compare/v1.76.11...v1.76.12) (2025-08-20)
### Bug Fixes:
* Core: Reduced ESI api calls to update character corp/ally info
## [v1.76.11](https://github.com/wanderer-industries/wanderer/compare/v1.76.10...v1.76.11) (2025-08-20)
## [v1.76.10](https://github.com/wanderer-industries/wanderer/compare/v1.76.9...v1.76.10) (2025-08-18)
### Bug Fixes:
* Core: Added character trackers start queue
## [v1.76.9](https://github.com/wanderer-industries/wanderer/compare/v1.76.8...v1.76.9) (2025-08-18)
### Bug Fixes:
* default signature types not being shown
## [v1.76.8](https://github.com/wanderer-industries/wanderer/compare/v1.76.7...v1.76.8) (2025-08-17)
### Bug Fixes:
* Core: added DB connection default timeouts
## [v1.76.7](https://github.com/wanderer-industries/wanderer/compare/v1.76.6...v1.76.7) (2025-08-16)
### Bug Fixes:
* Core: Fixed auth redirect URL
## [v1.76.6](https://github.com/wanderer-industries/wanderer/compare/v1.76.5...v1.76.6) (2025-08-15)
### Bug Fixes:
* empty subscriptions for sse
## [v1.76.5](https://github.com/wanderer-industries/wanderer/compare/v1.76.4...v1.76.5) (2025-08-15)
### Bug Fixes:
* Core: fixed tracking paused issues, fixed user activity data
## [v1.76.4](https://github.com/wanderer-industries/wanderer/compare/v1.76.3...v1.76.4) (2025-08-14)

View File

@@ -11,11 +11,13 @@ config :wanderer_app, WandererAppWeb.Endpoint,
config :wanderer_app, WandererApp.Repo,
ssl: false,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
show_sensitive_data_on_connection_error: false,
pool_size: 15,
migration_timestamps: [type: :utc_datetime_usec],
migration_lock: nil,
queue_target: 5000
queue_target: 5000,
queue_interval: 1000,
checkout_timeout: 15000
# Configures Swoosh API Client
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: WandererApp.Finch

View File

@@ -129,6 +129,8 @@ config :wanderer_app,
admin_username: System.get_env("WANDERER_ADMIN_USERNAME", "admin"),
admin_password: System.get_env("WANDERER_ADMIN_PASSWORD"),
admins: admins,
base_metrics_only:
System.get_env("WANDERER_BASE_METRICS_ONLY", "false") |> String.to_existing_atom(),
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1"),

View File

@@ -124,7 +124,7 @@ defmodule WandererApp.Api.Character do
update :update_corporation do
require_atomic? false
accept([:corporation_id, :corporation_name, :corporation_ticker, :alliance_id])
accept([:corporation_id, :corporation_name, :corporation_ticker])
end
update :update_alliance do

View File

@@ -79,8 +79,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
accept [
:map_id,
:character_id,
:tracked,
:followed
:tracked
]
argument :map_id, :uuid, allow_nil?: false

View File

@@ -113,6 +113,63 @@ defmodule WandererApp.CachedInfo do
end
end
def get_solar_system_jumps() do
case WandererApp.Cache.lookup(:solar_system_jumps) do
{:ok, nil} ->
data = WandererApp.EveDataService.get_solar_system_jumps_data()
cache_items(data, :solar_system_jumps)
{:ok, data}
{:ok, data} ->
{:ok, data}
end
end
def get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
# Create normalized cache key (smaller ID first for bidirectional lookup)
{id1, id2} =
if from_solar_system_id < to_solar_system_id do
{from_solar_system_id, to_solar_system_id}
else
{to_solar_system_id, from_solar_system_id}
end
cache_key = "jump_#{id1}_#{id2}"
case WandererApp.Cache.lookup(cache_key) do
{:ok, nil} ->
# Build jump index if not exists
build_jump_index()
WandererApp.Cache.lookup(cache_key)
result ->
result
end
end
defp build_jump_index() do
case get_solar_system_jumps() do
{:ok, jumps} ->
jumps
|> Enum.each(fn jump ->
{id1, id2} =
if jump.from_solar_system_id < jump.to_solar_system_id do
{jump.from_solar_system_id, jump.to_solar_system_id}
else
{jump.to_solar_system_id, jump.from_solar_system_id}
end
cache_key = "jump_#{id1}_#{id2}"
WandererApp.Cache.put(cache_key, jump)
end)
_ ->
:error
end
end
def get_wormhole_types!() do
case get_wormhole_types() do
{:ok, wormhole_types} ->

View File

@@ -263,7 +263,7 @@ defmodule WandererApp.Character do
end
end
defp maybe_merge_map_character_settings(%{id: character_id} = character, map_id, true) do
defp maybe_merge_map_character_settings(%{id: character_id} = character, _map_id, true) do
{:ok, tracking_paused} =
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)

View File

@@ -49,11 +49,13 @@ 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, raw_activity} <- WandererApp.Map.get_character_activity(map_id),
{: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
process_activity_data(raw_activity, map_user_settings, user_characters)
else
_ ->
[]
end
end

View File

@@ -7,6 +7,7 @@ defmodule WandererApp.Character.Tracker do
defstruct [
:character_id,
:alliance_id,
:corporation_id,
:opts,
server_online: true,
start_time: nil,
@@ -21,6 +22,8 @@ defmodule WandererApp.Character.Tracker do
@type t :: %__MODULE__{
character_id: integer,
alliance_id: integer,
corporation_id: integer,
opts: map,
server_online: boolean,
start_time: DateTime.t(),
@@ -34,10 +37,10 @@ defmodule WandererApp.Character.Tracker do
}
@pause_tracking_timeout :timer.minutes(60 * 10)
@offline_timeout :timer.minutes(5)
@online_error_timeout :timer.minutes(2)
@ship_error_timeout :timer.minutes(2)
@location_error_timeout :timer.minutes(2)
@offline_timeout :timer.minutes(10)
@online_error_timeout :timer.minutes(10)
@ship_error_timeout :timer.minutes(10)
@location_error_timeout :timer.minutes(10)
@online_forbidden_ttl :timer.seconds(7)
@online_limit_ttl :timer.seconds(7)
@forbidden_ttl :timer.seconds(5)
@@ -49,8 +52,15 @@ defmodule WandererApp.Character.Tracker do
def new(args), do: __struct__(args)
def init(args) do
character_id = args[:character_id]
{:ok, %{corporation_id: corporation_id, alliance_id: alliance_id}} =
WandererApp.Character.get_character(character_id)
%{
character_id: args[:character_id],
character_id: character_id,
corporation_id: corporation_id,
alliance_id: alliance_id,
start_time: DateTime.utc_now(),
opts: args
}
@@ -100,7 +110,8 @@ defmodule WandererApp.Character.Tracker do
duration = DateTime.diff(DateTime.utc_now(), error_time, :millisecond)
if duration >= timeout do
# pause_tracking(character_id)
pause_tracking(character_id)
WandererApp.Cache.delete("character:#{character_id}:#{type}_error_time")
:ok
else
@@ -113,15 +124,14 @@ defmodule WandererApp.Character.Tracker do
if WandererApp.Character.can_pause_tracking?(character_id) &&
not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
# Log character tracking statistics before pausing
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
Logger.debug(fn ->
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
Logger.warning(
"CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors",
character_id: character_id,
"CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors: #{inspect(character_id: character_id,
active_maps: length(character_state.active_maps),
is_online: character_state.is_online,
tracking_duration_minutes: get_tracking_duration_minutes(character_id)
)
tracking_duration_minutes: get_tracking_duration_minutes(character_id))}"
end)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
@@ -193,7 +203,7 @@ defmodule WandererApp.Character.Tracker do
access_token: access_token,
character_id: character_id
) do
{:ok, online} ->
{:ok, online} when is_map(online) ->
online = get_online(online)
if online.online == true do
@@ -258,7 +268,7 @@ defmodule WandererApp.Character.Tracker do
character_id: character_id
})
Logger.warning("ESI_ERROR: Character online tracking failed",
Logger.warning("ESI_ERROR: Character online tracking failed #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -388,12 +398,21 @@ defmodule WandererApp.Character.Tracker do
{:ok, %{eve_id: eve_id, tracking_pool: tracking_pool}} =
WandererApp.Character.get_character(character_id)
case WandererApp.Esi.get_character_info(eve_id) do
{:ok, _info} ->
character_eve_id = eve_id |> String.to_integer()
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
update = maybe_update_corporation(character_state, eve_id |> String.to_integer())
WandererApp.Character.update_character_state(character_id, update)
alliance_id = character_aff_info |> Map.get("alliance_id")
corporation_id = character_aff_info |> Map.get("corporation_id")
updated_state =
character_state
|> maybe_update_corporation(corporation_id)
|> maybe_update_alliance(alliance_id)
WandererApp.Character.update_character_state(character_id, updated_state)
:ok
@@ -975,7 +994,38 @@ defmodule WandererApp.Character.Tracker do
end
end
defp update_alliance(%{character_id: character_id} = state, alliance_id) do
defp maybe_update_alliance(
%{character_id: character_id, alliance_id: old_alliance_id} = state,
alliance_id
)
when old_alliance_id != alliance_id and is_nil(alliance_id) do
{:ok, character} = WandererApp.Character.get_character(character_id)
character_update = %{
alliance_id: nil,
alliance_name: nil,
alliance_ticker: nil
}
{:ok, _character} =
Character.update_alliance(character, character_update)
WandererApp.Character.update_character(character_id, character_update)
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}}
)
state
end
defp maybe_update_alliance(
%{character_id: character_id, alliance_id: old_alliance_id} = state,
alliance_id
)
when old_alliance_id != alliance_id do
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
@@ -1015,7 +1065,13 @@ defmodule WandererApp.Character.Tracker do
end
end
defp update_corporation(%{character_id: character_id} = state, corporation_id) do
defp maybe_update_alliance(state, _alliance_id), do: state
defp maybe_update_corporation(
%{character_id: character_id, corporation_id: old_corporation_id} = state,
corporation_id
)
when old_corporation_id != corporation_id do
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
@@ -1027,16 +1083,13 @@ defmodule WandererApp.Character.Tracker do
|> WandererApp.Esi.get_corporation_info()
|> case do
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
alliance_id = Map.get(corporation_info, "alliance_id")
{:ok, character} =
WandererApp.Character.get_character(character_id)
character_update = %{
corporation_id: corporation_id,
corporation_name: corporation_name,
corporation_ticker: corporation_ticker,
alliance_id: alliance_id
corporation_ticker: corporation_ticker
}
{:ok, _character} =
@@ -1057,8 +1110,7 @@ defmodule WandererApp.Character.Tracker do
)
state
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|> maybe_update_alliance()
|> Map.merge(%{corporation_id: corporation_id})
error ->
Logger.warning(
@@ -1072,6 +1124,8 @@ defmodule WandererApp.Character.Tracker do
end
end
defp maybe_update_corporation(state, _corporation_id), do: state
defp maybe_update_ship(
%{
character_id: character_id
@@ -1153,58 +1207,6 @@ defmodule WandererApp.Character.Tracker do
structure_id != new_structure_id ||
station_id != new_station_id
defp maybe_update_corporation(
state,
character_eve_id
)
when not is_nil(character_eve_id) and is_integer(character_eve_id) do
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
update_corporation(state, character_aff_info |> Map.get("corporation_id"))
_error ->
state
end
end
defp maybe_update_corporation(
state,
_info
),
do: state
defp maybe_update_alliance(
%{character_id: character_id, alliance_id: alliance_id} =
state
) do
case alliance_id do
nil ->
{:ok, character} = WandererApp.Character.get_character(character_id)
character_update = %{
alliance_id: nil,
alliance_name: nil,
alliance_ticker: nil
}
{:ok, _character} =
Character.update_alliance(character, character_update)
WandererApp.Character.update_character(character_id, character_update)
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}}
)
state
_ ->
update_alliance(state, alliance_id)
end
end
defp maybe_update_wallet(
%{character_id: character_id} =
state,

View File

@@ -12,6 +12,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
opts: map
}
@check_start_queue_interval :timer.seconds(1)
@garbage_collection_interval :timer.minutes(15)
@untrack_characters_interval :timer.minutes(1)
@inactive_character_timeout :timer.minutes(10)
@@ -23,6 +24,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
def new(args), do: __struct__(args)
def init(args) do
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
@@ -46,25 +48,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def start_tracking(state, character_id, opts) do
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
false <- Enum.member?(characters, character_id) do
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
if not WandererApp.Cache.has_key?("#{character_id}:track_requested") do
WandererApp.Cache.insert(
"#{character_id}:track_requested",
true
)
tracked_characters = [character_id | characters] |> Enum.uniq()
WandererApp.Cache.insert("tracked_characters", tracked_characters)
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
character_id,
%{opts: opts}
])
WandererApp.Cache.insert_or_update(
"track_characters_queue",
[character_id],
fn existing ->
[character_id | existing] |> Enum.uniq()
end
)
end
state
@@ -178,6 +174,21 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
end
def handle_info(
:check_start_queue,
state
) do
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
{:ok, track_characters_queue} = WandererApp.Cache.lookup("track_characters_queue", [])
track_characters_queue
|> Enum.each(fn character_id ->
track_character(character_id, %{})
end)
state
end
def handle_info(
:garbage_collect,
state
@@ -294,8 +305,56 @@ defmodule WandererApp.Character.TrackerManager.Impl do
state
end
def handle_info(_event, state),
do: state
def track_character(character_id, opts) do
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
false <- Enum.member?(characters, character_id) do
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
WandererApp.Cache.insert_or_update(
"tracked_characters",
[character_id],
fn existing ->
[character_id | existing] |> Enum.uniq()
end
)
WandererApp.Cache.insert_or_update(
"track_characters_queue",
[],
fn existing ->
existing
|> Enum.reject(fn c_id -> c_id == character_id end)
end
)
WandererApp.Cache.delete("#{character_id}:track_requested")
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
character_id,
%{opts: opts}
])
else
_ ->
WandererApp.Cache.insert_or_update(
"track_characters_queue",
[],
fn existing ->
existing
|> Enum.reject(fn c_id -> c_id == character_id end)
end
)
WandererApp.Cache.delete("#{character_id}:track_requested")
end
end
def character_is_present(map_id, character_id) do
{:ok, presence_character_ids} =

View File

@@ -23,7 +23,7 @@ defmodule WandererApp.Character.TrackerPool do
@check_ship_errors_interval :timer.minutes(1)
@check_location_errors_interval :timer.minutes(1)
@update_ship_interval :timer.seconds(2)
@update_info_interval :timer.minutes(1)
@update_info_interval :timer.minutes(2)
@update_wallet_interval :timer.minutes(1)
@logger Application.compile_env(:wanderer_app, :logger)

View File

@@ -20,7 +20,7 @@ defmodule WandererApp.Character.TrackingUtils do
)
when not is_nil(caller_pid) do
with {:ok, character} <-
WandererApp.Character.get_by_eve_id(character_eve_id),
WandererApp.Character.get_by_eve_id("#{character_eve_id}"),
{:ok, %{tracked: is_tracked}} <-
do_update_character_tracking(character, map_id, track, caller_pid) do
# Determine which event to send based on tracking mode and previous state
@@ -55,15 +55,19 @@ defmodule WandererApp.Character.TrackingUtils do
Builds tracking data for all characters with access to a map.
"""
def build_tracking_data(map_id, current_user_id) do
with {:ok, map} <- WandererApp.MapRepo.get(map_id, [:acls]),
{:ok, character_settings} <-
WandererApp.Character.Activity.get_map_character_settings(map_id),
with {:ok, map} <-
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
),
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
{:ok, %{characters: characters_with_access}} <-
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
WandererApp.Maps.load_characters(map, current_user_id) do
# Map characters to tracking data
{:ok, characters_data} =
build_character_tracking_data(characters_with_access, character_settings)
build_character_tracking_data(characters_with_access)
{:ok, main_character} =
get_main_character(user_settings, characters_with_access, characters_with_access)
@@ -98,21 +102,19 @@ defmodule WandererApp.Character.TrackingUtils do
end
# Helper to build tracking data for each character
defp build_character_tracking_data(characters, character_settings) do
defp build_character_tracking_data(characters) do
{:ok,
Enum.map(characters, fn char ->
setting = Enum.find(character_settings, &(&1.character_id == char.id))
%{
character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
tracked: (setting && setting.tracked) || false
tracked: char.tracked
}
end)}
end
# Private implementation of update character tracking
defp do_update_character_tracking(character, map_id, track, caller_pid) do
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id)
WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|> case do
# Untracking flow
{:ok, %{tracked: true} = existing_settings} ->

View File

@@ -11,49 +11,50 @@ defmodule WandererApp.Env do
def vsn(), do: Application.spec(@app)[:vsn]
def git_sha(), do: get_key(:git_sha, "<GIT_SHA>")
def base_url, do: get_key(:web_app_url, "<BASE_URL>")
def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
def invites, do: get_key(:invites, false)
def base_url(), do: get_key(:web_app_url, "<BASE_URL>")
def base_metrics_only(), do: get_key(:base_metrics_only, false)
def custom_route_base_url(), do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
def invites(), do: get_key(:invites, false)
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
def websocket_events_enabled?, do: get_key(:websocket_events_enabled, false)
def public_api_disabled?, do: get_key(:public_api_disabled, false)
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
def websocket_events_enabled?(), do: get_key(:websocket_events_enabled, false)
def public_api_disabled?(), do: get_key(:public_api_disabled, false)
@decorate cacheable(
cache: WandererApp.Cache,
key: "active_tracking_pool"
)
def active_tracking_pool, do: get_key(:active_tracking_pool, "default")
def active_tracking_pool(), do: get_key(:active_tracking_pool, "default")
@decorate cacheable(
cache: WandererApp.Cache,
key: "tracking_pool_max_size"
)
def tracking_pool_max_size, do: get_key(:tracking_pool_max_size, 300)
def character_tracking_pause_disabled?, do: get_key(:character_tracking_pause_disabled, true)
def character_api_disabled?, do: get_key(:character_api_disabled, false)
def wanderer_kills_service_enabled?, do: get_key(:wanderer_kills_service_enabled, false)
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
def admins, do: get_key(:admins, [])
def admin_username, do: get_key(:admin_username)
def admin_password, do: get_key(:admin_password)
def corp_wallet, do: get_key(:corp_wallet, "")
def corp_wallet_eve_id, do: get_key(:corp_wallet_eve_id, "-1")
def corp_eve_id, do: get_key(:corp_id, -1)
def subscription_settings, do: get_key(:subscription_settings)
def tracking_pool_max_size(), do: get_key(:tracking_pool_max_size, 300)
def character_tracking_pause_disabled?(), do: get_key(:character_tracking_pause_disabled, true)
def character_api_disabled?(), do: get_key(:character_api_disabled, false)
def wanderer_kills_service_enabled?(), do: get_key(:wanderer_kills_service_enabled, false)
def wallet_tracking_enabled?(), do: get_key(:wallet_tracking_enabled, false)
def admins(), do: get_key(:admins, [])
def admin_username(), do: get_key(:admin_username)
def admin_password(), do: get_key(:admin_password)
def corp_wallet(), do: get_key(:corp_wallet, "")
def corp_wallet_eve_id(), do: get_key(:corp_wallet_eve_id, "-1")
def corp_eve_id(), do: get_key(:corp_id, -1)
def subscription_settings(), do: get_key(:subscription_settings)
@decorate cacheable(
cache: WandererApp.Cache,
key: "restrict_maps_creation"
)
def restrict_maps_creation?, do: get_key(:restrict_maps_creation, false)
def restrict_maps_creation?(), do: get_key(:restrict_maps_creation, false)
def sse_enabled? do
def sse_enabled?() do
Application.get_env(@app, :sse, [])
|> Keyword.get(:enabled, false)
end
def webhooks_enabled? do
def webhooks_enabled?() do
Application.get_env(@app, :external_events, [])
|> Keyword.get(:webhooks_enabled, false)
end
@@ -62,19 +63,19 @@ defmodule WandererApp.Env do
cache: WandererApp.Cache,
key: "map-connection-auto-expire-hours"
)
def map_connection_auto_expire_hours, do: get_key(:map_connection_auto_expire_hours)
def map_connection_auto_expire_hours(), do: get_key(:map_connection_auto_expire_hours)
@decorate cacheable(
cache: WandererApp.Cache,
key: "map-connection-auto-eol-hours"
)
def map_connection_auto_eol_hours, do: get_key(:map_connection_auto_eol_hours)
def map_connection_auto_eol_hours(), do: get_key(:map_connection_auto_eol_hours)
@decorate cacheable(
cache: WandererApp.Cache,
key: "map-connection-eol-expire-timeout-mins"
)
def map_connection_eol_expire_timeout_mins,
def map_connection_eol_expire_timeout_mins(),
do: get_key(:map_connection_eol_expire_timeout_mins)
def get_key(key, default \\ nil), do: Application.get_env(@app, key, default)
@@ -83,7 +84,7 @@ defmodule WandererApp.Env do
A single map containing environment variables
made available to react
"""
def to_client_env do
def to_client_env() do
%{detailedKillsDisabled: not wanderer_kills_service_enabled?()}
end
end

View File

@@ -287,8 +287,8 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
def get_alliance_info(eve_id, opts \\ []) do
case _get_alliance_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
case get_alliance_info(eve_id, "", opts) do
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
@@ -309,8 +309,8 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
def get_corporation_info(eve_id, opts \\ []) do
case _get_corporation_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
case get_corporation_info(eve_id, "", opts) do
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
@@ -327,7 +327,7 @@ defmodule WandererApp.Esi.ApiClient do
opts,
@cache_opts
) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
@@ -434,7 +434,7 @@ defmodule WandererApp.Esi.ApiClient do
defp get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
defp _get_alliance_info(alliance_eve_id, info_path, opts),
defp get_alliance_info(alliance_eve_id, info_path, opts),
do:
get(
"/alliances/#{alliance_eve_id}/#{info_path}",
@@ -442,7 +442,7 @@ defmodule WandererApp.Esi.ApiClient do
@cache_opts
)
defp _get_corporation_info(corporation_eve_id, info_path, opts),
defp get_corporation_info(corporation_eve_id, info_path, opts),
do:
get(
"/corporations/#{corporation_eve_id}/#{info_path}",

View File

@@ -8,6 +8,19 @@ defmodule WandererApp.Map.Manager do
require Logger
alias WandererApp.Map.Server
alias WandererApp.Map.ServerSupervisor
alias WandererApp.Api.MapSystemSignature
@maps_start_per_second 10
@maps_start_interval 1000
@maps_queue :maps_queue
@garbage_collection_interval :timer.hours(1)
@check_maps_queue_interval :timer.seconds(1)
@signatures_cleanup_interval :timer.minutes(30)
@delete_after_minutes 30
@pings_cleanup_interval :timer.minutes(10)
@pings_expire_minutes 60
# Test-aware async task runner
defp safe_async_task(fun) do
@@ -25,20 +38,6 @@ defmodule WandererApp.Map.Manager do
end
end
alias WandererApp.Map.ServerSupervisor
alias WandererApp.Api.MapSystemSignature
@maps_start_per_second 5
@maps_start_interval 1000
@maps_queue :maps_queue
@garbage_collection_interval :timer.hours(1)
@check_maps_queue_interval :timer.seconds(1)
@signatures_cleanup_interval :timer.minutes(30)
@delete_after_minutes 30
@pings_cleanup_interval :timer.minutes(10)
@pings_expire_minutes 60
def start_map(map_id) when is_binary(map_id),
do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
@@ -247,22 +246,29 @@ defmodule WandererApp.Map.Manager do
Logger.debug(fn -> "All maps started" end)
else
# In production, run async as normal
tasks =
for chunk <- chunks do
task =
Task.async(fn ->
chunk
|> Enum.map(&start_map_server/1)
end)
chunks
|> Task.async_stream(
fn chunk ->
chunk
|> Enum.map(&start_map_server/1)
:timer.sleep(@maps_start_interval)
end,
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task,
timeout: :timer.seconds(60)
)
|> Enum.each(fn result ->
case result do
{:ok, _} ->
:ok
task
_ ->
:ok
end
end)
Logger.debug(fn -> "Waiting for maps to start" end)
Task.await_many(tasks)
Logger.debug(fn -> "All maps started" end)
Logger.info(fn -> "All maps started" end)
end
end

View File

@@ -88,11 +88,18 @@ defmodule WandererApp.Map.Server do
|> map_pid!
|> GenServer.cast({&Impl.remove_character/2, [character_id]})
def untrack_characters(map_id, character_ids) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.cast({&Impl.untrack_characters/2, [character_ids]})
def untrack_characters(map_id, character_ids) when is_binary(map_id) do
map_id
|> map_pid()
|> case do
pid when is_pid(pid) ->
GenServer.cast(pid, {&Impl.untrack_characters/2, [character_ids]})
_ ->
WandererApp.Cache.insert("map_#{map_id}:started", false)
:ok
end
end
def add_system(map_id, system_info, user_id, character_id) when is_binary(map_id),
do:

View File

@@ -16,7 +16,13 @@ defmodule WandererApp.Map.Operations.Signatures do
systems
|> Enum.flat_map(fn sys ->
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
sigs
# Add solar_system_id to each signature and remove system_id
Enum.map(sigs, fn sig ->
sig
|> Map.from_struct()
|> Map.put(:solar_system_id, sys.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
end)
else
err ->
Logger.error("[list_signatures] error: #{inspect(err)}")
@@ -32,28 +38,70 @@ defmodule WandererApp.Map.Operations.Signatures 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)
%{"solar_system_id" => solar_system_id} = params
)
when is_integer(solar_system_id) do
# Convert solar_system_id to system_id for internal use
with {:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
attrs =
params
|> Map.put("character_eve_id", char_id)
|> Map.put("system_id", system.id)
|> Map.delete("solar_system_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}
case Server.update_signatures(map_id, %{
added_signatures: [attrs],
updated_signatures: [],
removed_signatures: [],
solar_system_id: solar_system_id,
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
}) do
:ok ->
# Try to fetch the created signature to return with proper fields
with {:ok, sigs} <-
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
sig when not is_nil(sig) <- List.first(sigs) do
result =
sig
|> Map.from_struct()
|> Map.put(:solar_system_id, system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
err ->
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
{:ok, result}
else
_ ->
# Fallback: return attrs with solar_system_id added
attrs_result =
attrs
|> Map.put(:solar_system_id, solar_system_id)
|> Map.drop(["system_id"])
{:ok, attrs_result}
end
err ->
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
end
else
_ ->
Logger.error(
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
)
{:error, :system_not_found}
end
end
def create_signature(
%{assigns: %{map_id: _map_id, owner_character_id: _char_id, owner_user_id: _user_id}} =
_conn,
%{"solar_system_id" => _invalid} = _params
),
do: {:error, :missing_params}
def create_signature(_conn, _params), do: {:error, :missing_params}
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
@@ -90,7 +138,18 @@ defmodule WandererApp.Map.Operations.Signatures do
delete_connection_with_sigs: false
})
{:ok, attrs}
# Fetch the updated signature to return with proper fields
with {:ok, updated_sig} <- MapSystemSignature.by_id(sig_id) do
result =
updated_sig
|> Map.from_struct()
|> Map.put(:solar_system_id, system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
{:ok, result}
else
_ -> {:ok, attrs}
end
else
err ->
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")

View File

@@ -59,6 +59,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
map_update = %{acls: map.acls, scope: map.scope}
WandererApp.Map.update_map(map_id, map_update)
WandererApp.Cache.delete("map_characters-#{map_id}")
broadcast_acl_updates({:ok, result}, map_id)
@@ -66,7 +67,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
end
def handle_acl_updated(map_id, acl_id) do
{:ok, map} =
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
@@ -74,8 +75,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
]
)
if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
WandererApp.Map.update_map(map_id, %{acls: map.acls})
if acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
WandererApp.Map.update_map(map_id, %{acls: acls})
WandererApp.Cache.delete("map_characters-#{map_id}")
:ok =
acl_id
@@ -85,7 +87,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
end
def handle_acl_deleted(map_id, _acl_id) do
{:ok, map} =
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
@@ -93,7 +95,8 @@ defmodule WandererApp.Map.Server.AclsImpl do
]
)
WandererApp.Map.update_map(map_id, %{acls: map.acls})
WandererApp.Map.update_map(map_id, %{acls: acls})
WandererApp.Cache.delete("map_characters-#{map_id}")
character_ids =
map_id

View File

@@ -78,15 +78,12 @@ defmodule WandererApp.Map.Server.CharactersImpl do
characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
"map_#{map_id}:invalidate_character_ids",
[]
)
WandererApp.Cache.insert(
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
(invalidate_character_ids ++ characters_to_remove) |> Enum.uniq()
characters_to_remove,
fn ids ->
(ids ++ characters_to_remove) |> Enum.uniq()
end
)
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
@@ -126,15 +123,18 @@ defmodule WandererApp.Map.Server.CharactersImpl do
def cleanup_characters(map_id, owner_id) do
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
WandererApp.Cache.get_and_remove(
"map_#{map_id}:invalidate_character_ids",
[]
)
acls =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:acls, [])
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
)
invalidate_character_ids
|> Task.async_stream(
@@ -186,11 +186,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:error, reason} ->
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
end)
WandererApp.Cache.insert(
"map_#{map_id}:invalidate_character_ids",
[]
)
end
defp remove_and_untrack_characters(map_id, character_ids) do
@@ -373,6 +368,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:ok, character} =
WandererApp.Character.get_map_character(map_id, character_id, not_present: true)
WandererApp.Cache.delete("character:#{character.id}:tracking_paused")
add_character(%{map_id: map_id}, character, true)
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{

View File

@@ -527,33 +527,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
when not is_nil(from_solar_system_id) and not is_nil(to_solar_system_id) do
{:ok, known_jumps} =
WandererApp.Api.MapSolarSystemJumps.find(%{
before_system_id: from_solar_system_id,
current_system_id: to_solar_system_id
})
{:ok, from_system_static_info} = get_system_static_info(from_solar_system_id)
{:ok, to_system_static_info} = get_system_static_info(to_solar_system_id)
case scope do
:wormholes ->
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
known_jumps |> Enum.empty?()
:stargates ->
# For stargates, we need to check:
# 1. Both systems are in known space (HS, LS, NS)
# 2. There is a known jump between them
# 3. Neither system is prohibited
from_system_static_info.system_class in @known_space and
to_system_static_info.system_class in @known_space and
with {:ok, known_jumps} <- find_solar_system_jump(from_solar_system_id, to_solar_system_id),
{:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
{:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do
case scope do
:wormholes ->
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (known_jumps |> Enum.empty?())
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
known_jumps |> Enum.empty?()
:stargates ->
# For stargates, we need to check:
# 1. Both systems are in known space (HS, LS, NS)
# 2. There is a known jump between them
# 3. Neither system is prohibited
from_system_static_info.system_class in @known_space and
to_system_static_info.system_class in @known_space and
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (known_jumps |> Enum.empty?())
end
else
_ -> false
end
end
@@ -570,6 +567,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
end
end
defp find_solar_system_jump(from_solar_system_id, to_solar_system_id) do
case WandererApp.CachedInfo.get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
{:ok, jump} when not is_nil(jump) -> {:ok, [jump]}
_ -> {:ok, []}
end
end
defp get_system_static_info(solar_system_id) do
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->

View File

@@ -25,7 +25,7 @@ defmodule WandererApp.Map.Server.Impl do
]
@systems_cleanup_timeout :timer.minutes(30)
@characters_cleanup_timeout :timer.minutes(1)
@characters_cleanup_timeout :timer.minutes(5)
@connections_cleanup_timeout :timer.minutes(2)
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
@@ -100,7 +100,7 @@ defmodule WandererApp.Map.Server.Impl do
Process.send_after(self(), :update_presence, @update_presence_timeout)
Process.send_after(self(), :cleanup_connections, 5_000)
Process.send_after(self(), :cleanup_systems, 10_000)
Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
Process.send_after(self(), :backup_state, @backup_state_timeout)
WandererApp.Cache.insert("map_#{map_id}:started", true)
@@ -127,6 +127,7 @@ defmodule WandererApp.Map.Server.Impl do
Logger.debug(fn -> "Stopping map server for #{map_id}" end)
WandererApp.Cache.delete("map_#{map_id}:started")
WandererApp.Cache.delete("map_characters-#{map_id}")
:telemetry.execute([:wanderer_app, :map, :stopped], %{count: 1})
@@ -278,7 +279,7 @@ defmodule WandererApp.Map.Server.Impl do
end
def handle_event({:acl_deleted, %{acl_id: acl_id}}, %{map_id: map_id} = state) do
AclsImpl.handle_acl_updated(map_id, acl_id)
AclsImpl.handle_acl_deleted(map_id, acl_id)
state
end

View File

@@ -94,13 +94,22 @@ defmodule WandererApp.Maps do
end
end
def load_characters(map, character_settings, user_id) do
def load_characters(map, user_id) do
{:ok, user_characters} =
WandererApp.Api.Character.active_by_user(%{user_id: user_id})
characters =
map_available_characters =
map
|> get_map_available_characters(user_characters)
{:ok, character_settings} =
WandererApp.MapCharacterSettingsRepo.get_by_map_filtered(
map.id,
map_available_characters |> Enum.map(& &1.id)
)
characters =
map_available_characters
|> Enum.map(fn c ->
map_character(c, character_settings |> Enum.find(&(&1.character_id == c.id)))
end)
@@ -176,48 +185,57 @@ defmodule WandererApp.Maps do
tracked: tracked
}
@decorate cacheable(
cache: WandererApp.Cache,
key: "map_characters-#{map_id}",
opts: [ttl: :timer.seconds(2)]
)
defp _get_map_characters(%{id: map_id} = map) do
map_acls =
map.acls
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
defp get_map_characters(%{id: map_id} = map) do
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|> case do
nil ->
map_acls =
map.acls
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
map_acl_owner_ids =
map_acls
|> Enum.map(fn acl -> acl.owner_id end)
map_acl_owner_ids =
map_acls
|> Enum.map(fn acl -> acl.owner_id end)
map_members =
map_acls
|> Enum.map(fn acl -> acl.members end)
|> List.flatten()
|> Enum.filter(fn member -> member.role != :blocked end)
map_members =
map_acls
|> Enum.map(fn acl -> acl.members end)
|> List.flatten()
|> Enum.filter(fn member -> member.role != :blocked end)
map_member_eve_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|> Enum.map(fn member -> member.eve_character_id end)
map_member_eve_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|> Enum.map(fn member -> member.eve_character_id end)
map_member_corporation_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|> Enum.map(fn member -> member.eve_corporation_id end)
map_member_corporation_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|> Enum.map(fn member -> member.eve_corporation_id end)
map_member_alliance_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|> Enum.map(fn member -> member.eve_alliance_id end)
map_member_alliance_ids =
map_members
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|> Enum.map(fn member -> member.eve_alliance_id end)
{:ok,
%{
map_acl_owner_ids: map_acl_owner_ids,
map_member_eve_ids: map_member_eve_ids,
map_member_corporation_ids: map_member_corporation_ids,
map_member_alliance_ids: map_member_alliance_ids
}}
map_characters =
%{
map_acl_owner_ids: map_acl_owner_ids,
map_member_eve_ids: map_member_eve_ids,
map_member_corporation_ids: map_member_corporation_ids,
map_member_alliance_ids: map_member_alliance_ids
}
WandererApp.Cache.insert(
"map_characters-#{map_id}",
map_characters
)
{:ok, map_characters}
map_characters ->
{:ok, map_characters}
end
end
defp get_map_available_characters(map, user_characters) do
@@ -227,7 +245,7 @@ defmodule WandererApp.Maps do
map_member_eve_ids: map_member_eve_ids,
map_member_corporation_ids: map_member_corporation_ids,
map_member_alliance_ids: map_member_alliance_ids
}} = _get_map_characters(map)
}} = get_map_characters(map)
user_characters
|> Enum.filter(fn c ->

View File

@@ -29,15 +29,24 @@ defmodule WandererApp.Metrics.PromExPlugin do
@impl true
def event_metrics(_opts) do
[
base_metrics = [
user_event_metrics(),
character_event_metrics(),
map_event_metrics(),
map_subscription_metrics(),
map_subscription_metrics()
]
advanced_metrics = [
character_event_metrics(),
characters_distribution_event_metrics(),
esi_event_metrics(),
json_api_metrics()
]
if WandererApp.Env.base_metrics_only() do
base_metrics
else
base_metrics ++ advanced_metrics
end
end
defp user_event_metrics do
@@ -227,8 +236,8 @@ defmodule WandererApp.Metrics.PromExPlugin do
defp get_esi_error_tag_values(metadata) do
%{
endpoint: Map.get(metadata, :endpoint, "unknown"),
error_type: to_string(Map.get(metadata, :error_type, "unknown")),
tracking_pool: Map.get(metadata, :tracking_pool, "unknown")
error_type: inspect(Map.get(metadata, :error_type, "unknown")),
tracking_pool: Map.get(metadata, :tracking_pool, "default")
}
end

View File

@@ -53,20 +53,8 @@ defmodule WandererApp.MapCharacterSettingsRepo do
def get_tracked_by_map_all(map_id),
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
def get_by_map(map_id, character_id) do
case get_by_map_filtered(map_id, [character_id]) do
{:ok, [setting | _]} ->
{:ok, setting}
{:ok, []} ->
{:error, :not_found}
{:error, reason} ->
{:error, reason}
end
end
def track(settings) do
{:ok, _} = get(settings.map_id, settings.character_id)
# Only update the tracked field, preserving other fields
WandererApp.Api.MapCharacterSettings.track(%{
map_id: settings.map_id,
@@ -75,6 +63,7 @@ defmodule WandererApp.MapCharacterSettingsRepo do
end
def untrack(settings) do
{:ok, _} = get(settings.map_id, settings.character_id)
# Only update the tracked field, preserving other fields
WandererApp.Api.MapCharacterSettings.untrack(%{
map_id: settings.map_id,
@@ -83,22 +72,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
end
def track!(settings) do
case WandererApp.Api.MapCharacterSettings.track(%{
map_id: settings.map_id,
character_id: settings.character_id
}) do
case track(settings) do
{:ok, result} -> result
{:error, error} -> raise "Failed to track: #{inspect(error)}"
error -> raise "Failed to track: #{inspect(error)}"
end
end
def untrack!(settings) do
case WandererApp.Api.MapCharacterSettings.untrack(%{
map_id: settings.map_id,
character_id: settings.character_id
}) do
case untrack(settings) do
{:ok, result} -> result
{:error, error} -> raise "Failed to untrack: #{inspect(error)}"
error -> raise "Failed to untrack: #{inspect(error)}"
end
end
@@ -117,22 +100,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
end
def follow!(settings) do
case WandererApp.Api.MapCharacterSettings.follow(%{
map_id: settings.map_id,
character_id: settings.character_id
}) do
case follow(settings) do
{:ok, result} -> result
{:error, error} -> raise "Failed to follow: #{inspect(error)}"
error -> raise "Failed to follow: #{inspect(error)}"
end
end
def unfollow!(settings) do
case WandererApp.Api.MapCharacterSettings.unfollow(%{
map_id: settings.map_id,
character_id: settings.character_id
}) do
case unfollow(settings) do
{:ok, result} -> result
{:error, error} -> raise "Failed to unfollow: #{inspect(error)}"
error -> raise "Failed to unfollow: #{inspect(error)}"
end
end

View File

@@ -39,7 +39,7 @@ defmodule WandererApp.SecurityAudit do
}
# Store in database
store_audit_entry(audit_entry)
# store_audit_entry(audit_entry)
# Send to telemetry for monitoring
emit_telemetry_event(audit_entry)
@@ -489,11 +489,11 @@ defmodule WandererApp.SecurityAudit do
defp store_audit_entry(audit_entry) do
# Handle async processing if enabled
if async_enabled?() do
WandererApp.SecurityAudit.AsyncProcessor.log_event(audit_entry)
else
do_store_audit_entry(audit_entry)
end
# if async_enabled?() do
# WandererApp.SecurityAudit.AsyncProcessor.log_event(audit_entry)
# else
# do_store_audit_entry(audit_entry)
# end
end
@doc false

View File

@@ -195,7 +195,7 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
tracking_pool = WandererApp.Character.TrackingConfigUtils.get_active_pool!()
base_options = [
redirect_uri: callback_url(conn),
redirect_uri: "#{WandererApp.Env.base_url()}/auth/eve/callback",
with_wallet: with_wallet,
is_admin?: is_admin?,
tracking_pool: tracking_pool

View File

@@ -596,10 +596,24 @@ defmodule WandererAppWeb.MapAccessListAPIController do
acl -> acl.id
end)
updated_acls = current_acl_ids ++ [new_acl_id]
updated_acls =
if new_acl_id in current_acl_ids do
current_acl_ids
else
current_acl_ids ++ [new_acl_id]
end
case WandererApp.Api.Map.update_acls(loaded_map, %{acls: updated_acls}) do
{:ok, updated_map} ->
# Only broadcast if we actually added a new ACL
unless new_acl_id in current_acl_ids do
Phoenix.PubSub.broadcast(
WandererApp.PubSub,
"maps:#{loaded_map.id}",
{:map_acl_updated, [new_acl_id], []}
)
end
{:ok, updated_map}
{:error, error} ->

View File

@@ -192,6 +192,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
:acl_member_added
) do
:ok ->
broadcast_acl_updated(acl_id)
json(conn, %{data: member_to_json(new_member)})
{:error, broadcast_error} ->
@@ -199,6 +201,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
"Failed to broadcast ACL member added event: #{inspect(broadcast_error)}"
)
# Still broadcast internal message even if external broadcast fails
broadcast_acl_updated(acl_id)
json(conn, %{data: member_to_json(new_member)})
end
@@ -300,6 +305,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
:acl_member_updated
) do
:ok ->
broadcast_acl_updated(acl_id)
json(conn, %{data: member_to_json(updated_membership)})
{:error, broadcast_error} ->
@@ -307,6 +314,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
"Failed to broadcast ACL member updated event: #{inspect(broadcast_error)}"
)
# Still broadcast internal message even if external broadcast fails
broadcast_acl_updated(acl_id)
json(conn, %{data: member_to_json(updated_membership)})
end
@@ -385,6 +395,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
:acl_member_removed
) do
:ok ->
broadcast_acl_updated(acl_id)
json(conn, %{ok: true})
{:error, broadcast_error} ->
@@ -392,6 +404,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
"Failed to broadcast ACL member removed event: #{inspect(broadcast_error)}"
)
# Still broadcast internal message even if external broadcast fails
broadcast_acl_updated(acl_id)
json(conn, %{ok: true})
end
@@ -417,6 +432,14 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
# Private Helpers
# ---------------------------------------------------------------------------
defp broadcast_acl_updated(acl_id) do
Phoenix.PubSub.broadcast(
WandererApp.PubSub,
"acls:#{acl_id}",
{:acl_updated, %{acl_id: acl_id}}
)
end
@doc false
defp member_to_json(member) do
base = %{

View File

@@ -52,11 +52,7 @@ defmodule WandererAppWeb.Api.EventsController do
defp establish_sse_connection(conn, map_id, api_key, params) do
# Parse event filter if provided
event_filter =
case Map.get(params, "events") do
nil -> :all
events -> EventFilter.parse(events)
end
event_filter = EventFilter.parse(Map.get(params, "events"))
# Parse format parameter
event_format = Map.get(params, "format", "legacy")

View File

@@ -15,7 +15,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
type: :object,
properties: %{
id: %OpenApiSpex.Schema{type: :string, format: :uuid},
system_id: %OpenApiSpex.Schema{type: :string, format: :uuid},
solar_system_id: %OpenApiSpex.Schema{type: :integer},
eve_id: %OpenApiSpex.Schema{type: :string},
character_eve_id: %OpenApiSpex.Schema{type: :string},
name: %OpenApiSpex.Schema{type: :string, nullable: true},
@@ -31,13 +31,13 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
},
required: [
:id,
:system_id,
:solar_system_id,
:eve_id,
:character_eve_id
],
example: %{
id: "sig-uuid-1",
system_id: "sys-uuid-1",
solar_system_id: 30_000_142,
eve_id: "ABC-123",
character_eve_id: "123456789",
name: "Wormhole K162",
@@ -122,7 +122,15 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
{:ok, signature} ->
case WandererApp.Api.MapSystem.by_id(signature.system_id) do
{:ok, system} when system.map_id == map_id ->
json(conn, %{data: signature})
# Add solar_system_id and remove system_id
# Convert to a plain map to avoid encoder issues
signature_data =
signature
|> Map.from_struct()
|> Map.put(:solar_system_id, system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
json(conn, %{data: signature_data})
_ ->
conn |> put_status(:not_found) |> json(%{error: "Signature not found"})

View File

@@ -13,6 +13,7 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
import Plug.Conn
alias Plug.Crypto
alias WandererApp.Api.User
alias WandererApp.SecurityAudit
alias WandererApp.Audit.RequestContext
@@ -140,43 +141,60 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
defp authenticate_bearer_token(conn) do
case get_req_header(conn, "authorization") do
["Bearer " <> token] ->
validate_api_token(token)
validate_api_token(conn, token)
_ ->
{:error, "Missing or invalid authorization header"}
end
end
defp validate_api_token(token) do
# Look up the map by its public API key
case find_map_by_api_key(token) do
{:ok, map} when not is_nil(map) ->
# Get the actual owner of the map
case User.by_id(map.owner_id, load: :characters) do
{:ok, user} ->
# Return the map owner as the authenticated user
{:ok, user, map}
defp validate_api_token(conn, token) do
# Check for map identifier in path params
# According to PR feedback, routes supply params["map_identifier"]
case conn.params["map_identifier"] do
nil ->
# No map identifier in path - this might be a general API endpoint
# For now, we'll return an error since we need to validate against a specific map
{:error, "Authentication failed", :no_map_context}
identifier ->
# Resolve the identifier (could be UUID or slug)
case resolve_map_identifier(identifier) do
{:ok, map} ->
# Validate the token matches this specific map's API key
if is_binary(map.public_api_key) &&
Crypto.secure_compare(map.public_api_key, token) do
# Get the map owner
case User.by_id(map.owner_id, load: :characters) do
{:ok, user} ->
{:ok, user, map}
{:error, _} ->
{:error, "Authentication failed", :map_owner_not_found}
end
else
{:error, "Authentication failed", :invalid_token_for_map}
end
{:error, _} ->
# Return generic error with specific reason for internal logging
{:error, "Authentication failed", :map_owner_not_found}
{:error, "Authentication failed", :map_not_found}
end
_ ->
# Return generic error with specific reason for internal logging
{:error, "Authentication failed", :invalid_api_key}
end
end
defp find_map_by_api_key(api_key) do
# Import necessary modules
import Ash.Query
# Helper to resolve map by ID or slug
defp resolve_map_identifier(identifier) do
alias WandererApp.Api.Map
# Query for map with matching public API key
Map
|> filter(public_api_key == ^api_key)
|> Ash.read_one()
# Try as UUID first
case Map.by_id(identifier) do
{:ok, map} ->
{:ok, map}
_ ->
# Try as slug
Map.get_map_by_slug(identifier)
end
end
defp get_user_role(user) do

View File

@@ -15,30 +15,6 @@ defmodule WandererAppWeb.Endpoint do
max_age: 24 * 60 * 60 * 180
]
# @impl SiteEncrypt
# def certification do
# SiteEncrypt.configure(
# client: :native,
# mode: :auto,
# days_to_renew: 30,
# domains: ["dev.wanderer.deadly-w.space"],
# emails: ["dmitriypopovsamara@gmail.com"],
# db_folder: System.get_env("SITE_ENCRYPT_DB", Path.join("tmp", "site_encrypt_db")),
# backup: Path.join(Path.join("tmp", "site_encrypt_db"), "site_encrypt_backup.tgz"),
# directory_url:
# case System.get_env("CERT_MODE", "local") do
# "local" ->
# {:internal, port: 4001}
# "staging" ->
# "https://acme-staging-v02.api.letsencrypt.org/directory"
# "production" ->
# "https://acme-v02.api.letsencrypt.org/directory"
# end
# )
# end
socket "/live", Phoenix.LiveView.Socket,
websocket: [compress: true, connect_info: [session: @session_options]]

View File

@@ -75,13 +75,12 @@ defmodule WandererAppWeb.CharactersLive do
def handle_event("delete", %{"character_id" => character_id}, socket) do
WandererApp.Character.TrackerManager.stop_tracking(character_id)
{:ok, map_user_settings} =
{:ok, map_character_settings} =
WandererApp.Api.MapCharacterSettings.tracked_by_character(%{character_id: character_id})
map_user_settings
map_character_settings
|> Enum.each(fn settings ->
settings
|> WandererApp.Api.MapCharacterSettings.untrack()
{:ok, _} = WandererApp.MapCharacterSettingsRepo.untrack(settings)
end)
{:ok, updated_character} =

View File

@@ -4,7 +4,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do
require Logger
@impl true
def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do
def mount(_params, _session, socket) do
{:ok, maps} = WandererApp.Maps.get_available_maps(socket.assigns.current_user)
{:ok,
@@ -14,7 +14,6 @@ defmodule WandererAppWeb.CharactersTrackingLive do
characters: [],
selected_map: nil,
selected_map_slug: nil,
user_id: user_id,
maps: maps |> Enum.sort_by(& &1.name, :asc)
)}
end
@@ -37,24 +36,22 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|> assign(:page_title, "Characters Tracking")
end
defp apply_action(socket, :characters, %{"slug" => map_slug} = _params) do
selected_map = socket.assigns.maps |> Enum.find(&(&1.slug == map_slug))
{:ok, character_settings} =
WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
user_id = socket.assigns.user_id
defp apply_action(
%{assigns: %{current_user: current_user, maps: maps}} = socket,
:characters,
%{"slug" => map_slug} = _params
) do
selected_map = maps |> Enum.find(&(&1.slug == map_slug))
socket
|> assign(:active_page, :characters_tracking)
|> assign(:page_title, "Characters Tracking")
|> assign(
selected_map: selected_map,
selected_map_slug: map_slug,
character_settings: character_settings
selected_map_slug: map_slug
)
|> assign_async(:characters, fn ->
WandererApp.Maps.load_characters(selected_map, character_settings, user_id)
WandererApp.Maps.load_characters(selected_map, current_user.id)
end)
end
@@ -71,55 +68,36 @@ defmodule WandererAppWeb.CharactersTrackingLive do
end
@impl true
def handle_event("toggle_track", %{"character_id" => character_id}, socket) do
def handle_event(
"toggle_track",
%{"character_id" => character_id},
%{assigns: %{current_user: current_user}} = socket
) do
selected_map = socket.assigns.selected_map
character_settings = socket.assigns.character_settings
case character_settings |> Enum.find(&(&1.character_id == character_id)) do
nil ->
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: selected_map.id,
tracked: true
})
{:noreply, socket}
character_setting ->
case character_setting.tracked do
true ->
character_setting
|> WandererApp.MapCharacterSettingsRepo.untrack!()
WandererApp.Map.Server.untrack_characters(selected_map.id, [
character_setting.character_id
])
_ ->
character_setting
|> WandererApp.MapCharacterSettingsRepo.track!()
end
end
%{result: characters} = socket.assigns.characters
{:ok, character_settings} =
WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
case characters |> Enum.find(&(&1.id == character_id)) do
%{tracked: false} ->
WandererApp.MapCharacterSettingsRepo.track(%{
character_id: character_id,
map_id: selected_map.id
})
characters =
characters
|> Enum.map(fn c ->
WandererApp.Maps.map_character(
c,
character_settings |> Enum.find(&(&1.character_id == c.id))
)
end)
%{tracked: true} ->
WandererApp.MapCharacterSettingsRepo.untrack(%{
character_id: character_id,
map_id: selected_map.id
})
WandererApp.Map.Server.untrack_characters(selected_map.id, [
character_id
])
end
{:noreply,
socket
|> assign(character_settings: character_settings)
|> assign_async(:characters, fn ->
{:ok, %{characters: characters}}
WandererApp.Maps.load_characters(selected_map, current_user.id)
end)}
end

View File

@@ -333,21 +333,18 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
def needs_tracking_setup?(
only_tracked_characters,
characters,
character_settings,
user_permissions
) do
tracked_count =
characters
|> Enum.count(fn char ->
setting = Enum.find(character_settings, &(&1.character_id == char.id))
setting && setting.tracked
char.tracked
end)
untracked_count =
characters
|> Enum.count(fn char ->
setting = Enum.find(character_settings, &(&1.character_id == char.id))
setting == nil || !setting.tracked
!char.tracked
end)
user_permissions.track_character &&

View File

@@ -422,14 +422,11 @@ defmodule WandererAppWeb.MapCoreEventHandler do
current_user_characters |> Enum.map(& &1.id)
),
{:ok, map_user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
{:ok, character_settings} <-
WandererApp.Character.Activity.get_map_character_settings(map_id),
{:ok, %{characters: available_map_characters}} =
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
WandererApp.Maps.load_characters(map, current_user_id) do
tracked_data =
get_tracked_data(
available_map_characters,
character_settings,
user_permissions,
only_tracked_characters
)
@@ -473,15 +470,13 @@ defmodule WandererAppWeb.MapCoreEventHandler do
defp get_tracked_data(
available_map_characters,
character_settings,
user_permissions,
only_tracked_characters
) do
tracked_characters =
available_map_characters
|> Enum.filter(fn char ->
setting = Enum.find(character_settings, &(&1.character_id == char.id))
setting != nil && setting.tracked == true
char.tracked
end)
all_tracked? =
@@ -492,7 +487,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
MapCharactersEventHandler.needs_tracking_setup?(
only_tracked_characters,
available_map_characters,
character_settings,
user_permissions
)

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.76.4"
@version "1.77.4"
def project do
[

View File

@@ -100,7 +100,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
test "creates a new signature with valid parameters", %{conn: conn, map: map} do
signature_params = %{
"system_id" => Ecto.UUID.generate(),
"solar_system_id" => 30_000_142,
"eve_id" => "ABC-123",
"character_eve_id" => "123456789",
"name" => "Test Signature",
@@ -132,7 +132,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
test "handles signature creation with minimal required fields", %{conn: conn, map: map} do
minimal_params = %{
"system_id" => Ecto.UUID.generate(),
"solar_system_id" => 30_000_143,
"eve_id" => "XYZ-456",
"character_eve_id" => "987654321"
}
@@ -152,7 +152,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
test "handles signature creation with all optional fields", %{conn: conn, map: map} do
complete_params = %{
"system_id" => Ecto.UUID.generate(),
"solar_system_id" => 30_000_144,
"eve_id" => "DEF-789",
"character_eve_id" => "456789123",
"name" => "Complete Signature",
@@ -181,7 +181,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
map = Factory.insert(:map)
signature_params = %{
"system_id" => Ecto.UUID.generate(),
"solar_system_id" => 30_000_145,
"eve_id" => "ABC-123",
"character_eve_id" => "123456789"
}
@@ -392,11 +392,11 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
test "validates signature creation with invalid data types", %{conn: conn, map: map} do
invalid_params = [
%{"system_id" => "not-a-uuid", "eve_id" => "ABC", "character_eve_id" => "123"},
%{"system_id" => Ecto.UUID.generate(), "eve_id" => 123, "character_eve_id" => "123"},
%{"system_id" => Ecto.UUID.generate(), "eve_id" => "ABC", "character_eve_id" => 123},
%{"solar_system_id" => "not-an-integer", "eve_id" => "ABC", "character_eve_id" => "123"},
%{"solar_system_id" => 30_000_142, "eve_id" => 123, "character_eve_id" => "123"},
%{"solar_system_id" => 30_000_142, "eve_id" => "ABC", "character_eve_id" => 123},
%{
"system_id" => Ecto.UUID.generate(),
"solar_system_id" => 30_000_142,
"eve_id" => "ABC",
"character_eve_id" => "123",
"linked_system_id" => "not-an-integer"
@@ -426,7 +426,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
long_string = String.duplicate("a", 1000)
long_params = %{
"system_id" => Ecto.UUID.generate(),
"solar_system_id" => 30_000_146,
"eve_id" => "LONG-123",
"character_eve_id" => "123456789",
"name" => long_string,
@@ -448,7 +448,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
test "handles special characters in signature data", %{conn: conn, map: map} do
special_params = %{
"system_id" => Ecto.UUID.generate(),
"solar_system_id" => 30_000_147,
"eve_id" => "ABC-123",
"character_eve_id" => "123456789",
"name" => "Special chars: àáâãäåæçèéêë",
@@ -470,7 +470,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
test "handles empty string values", %{conn: conn, map: map} do
empty_params = %{
"system_id" => Ecto.UUID.generate(),
"solar_system_id" => 30_000_148,
"eve_id" => "",
"character_eve_id" => "",
"name" => "",
@@ -537,7 +537,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
if length(data) > 0 do
signature = List.first(data)
assert Map.has_key?(signature, "id")
assert Map.has_key?(signature, "system_id")
assert Map.has_key?(signature, "solar_system_id")
assert Map.has_key?(signature, "eve_id")
assert Map.has_key?(signature, "character_eve_id")
end
@@ -564,7 +564,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
test "created signature response structure", %{conn: conn, map: map} do
signature_params = %{
"system_id" => Ecto.UUID.generate(),
"solar_system_id" => 30_000_149,
"eve_id" => "TEST-001",
"character_eve_id" => "123456789",
"name" => "Test Signature"
@@ -582,7 +582,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
case response do
%{"data" => data} ->
# Should have signature structure
assert Map.has_key?(data, "id") or Map.has_key?(data, "system_id")
assert Map.has_key?(data, "id") or Map.has_key?(data, "solar_system_id")
%{"error" => _error} ->
# Error response is also valid

View File

@@ -8,7 +8,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
describe "parameter validation" do
test "validates missing connection assigns for create_signature" do
conn = %{assigns: %{}}
params = %{"solar_system_id" => "30000142"}
params = %{"solar_system_id" => 30_000_142}
result = Signatures.create_signature(conn, params)
assert {:error, :missing_params} = result
@@ -209,7 +209,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
}
# Test with minimal required parameters
params = %{"solar_system_id" => "30000142"}
params = %{"solar_system_id" => 30_000_142}
MapTestHelpers.expect_map_server_error(fn ->
result = Signatures.create_signature(conn, params)
@@ -416,7 +416,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
nil
]
params = %{"solar_system_id" => "30000142"}
params = %{"solar_system_id" => 30_000_142}
Enum.each(malformed_conns, fn conn ->
# This should either crash (expected) or return error
@@ -462,17 +462,16 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
tasks =
Enum.map(1..3, fn i ->
Task.async(fn ->
MapTestHelpers.expect_map_server_error(fn ->
params = %{"solar_system_id" => "3000014#{i}"}
Signatures.create_signature(conn, params)
end)
params = %{"solar_system_id" => 30_000_140 + i}
result = Signatures.create_signature(conn, params)
# We expect either system_not_found (system doesn't exist in test)
# or the MapTestHelpers would have caught the map server error
assert {:error, :system_not_found} = result
end)
end)
# All tasks should complete without crashing
Enum.each(tasks, fn task ->
assert Task.await(task) == :ok
end)
Enum.each(tasks, &Task.await/1)
end
end
@@ -669,7 +668,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
}
]
params = %{"solar_system_id" => "30000142"}
params = %{"solar_system_id" => 30_000_142}
Enum.each(assign_variations, fn assigns ->
conn = %{assigns: assigns}