mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-01-28 09:36:03 +00:00
Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5850b5a8d | ||
|
|
9f6849209b | ||
|
|
7bd295cbad | ||
|
|
078e5fc19e | ||
|
|
3877e121c3 | ||
|
|
dcb2a0cdb2 | ||
|
|
f5294eee84 | ||
|
|
a5c87b6fa4 | ||
|
|
eae275f515 | ||
|
|
68ae6706dd | ||
|
|
a34b30af15 | ||
|
|
38b49266ed | ||
|
|
049884bb4c | ||
|
|
3c75b2b59f | ||
|
|
4ad5d191a3 | ||
|
|
2499c24cc1 | ||
|
|
6f0043205c | ||
|
|
597741fa60 | ||
|
|
d313ae8cd2 | ||
|
|
06d5d8072e | ||
|
|
f2d112df5c | ||
|
|
716604fa84 | ||
|
|
cae958a1e6 | ||
|
|
283b36c882 | ||
|
|
051e71f1a6 | ||
|
|
20a50e8db0 | ||
|
|
79d7f7ce7d | ||
|
|
6c4b65c446 | ||
|
|
2b07af5e12 | ||
|
|
d0901eecb4 | ||
|
|
ee85d29c54 | ||
|
|
a237d6513d | ||
|
|
02979588c1 | ||
|
|
3abe40855f | ||
|
|
d0d9418a89 | ||
|
|
3ce742eb01 | ||
|
|
ae566fb907 | ||
|
|
fa32c62f63 | ||
|
|
6880be11c5 | ||
|
|
5289893264 | ||
|
|
f15370a3df | ||
|
|
2cb2dc526c | ||
|
|
c3de3c4e35 | ||
|
|
4585c3a94b | ||
|
|
46a1898be9 | ||
|
|
e7219e0eec |
94
CHANGELOG.md
94
CHANGELOG.md
@@ -2,6 +2,100 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.90.5](https://github.com/wanderer-industries/wanderer/compare/v1.90.4...v1.90.5) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes
|
||||
|
||||
## [v1.90.4](https://github.com/wanderer-industries/wanderer/compare/v1.90.3...v1.90.4) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes & signatures clean up behaviour
|
||||
|
||||
## [v1.90.3](https://github.com/wanderer-industries/wanderer/compare/v1.90.2...v1.90.3) (2025-12-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added pagination for long ACL lists
|
||||
|
||||
## [v1.90.2](https://github.com/wanderer-industries/wanderer/compare/v1.90.1...v1.90.2) (2025-12-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added system position updates to SSE
|
||||
|
||||
## [v1.90.1](https://github.com/wanderer-industries/wanderer/compare/v1.90.0...v1.90.1) (2025-12-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed connections and signatures remove issues, added comprehensive audit log for auto removed connections and signatures
|
||||
|
||||
## [v1.90.0](https://github.com/wanderer-industries/wanderer/compare/v1.89.6...v1.90.0) (2025-12-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* core: Added several map scopes support (Wh, Hi, Low, Null, Pochven)
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed clean up for linked signatures
|
||||
|
||||
* core: fixed issue with default select mode
|
||||
|
||||
* apiV1 default fields updates
|
||||
|
||||
## [v1.89.6](https://github.com/wanderer-industries/wanderer/compare/v1.89.5...v1.89.6) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* kills: fixed zkb links (added "allow-popups-to-escape-sandbox" to CSP)
|
||||
|
||||
## [v1.89.5](https://github.com/wanderer-industries/wanderer/compare/v1.89.4...v1.89.5) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.89.4](https://github.com/wanderer-industries/wanderer/compare/v1.89.3...v1.89.4) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed acl character update issues
|
||||
|
||||
## [v1.89.3](https://github.com/wanderer-industries/wanderer/compare/v1.89.2...v1.89.3) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed tracking issues
|
||||
|
||||
## [v1.89.2](https://github.com/wanderer-industries/wanderer/compare/v1.89.1...v1.89.2) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
50
Makefile
50
Makefile
@@ -32,6 +32,56 @@ format f:
|
||||
test t:
|
||||
MIX_ENV=test mix test
|
||||
|
||||
# Run tests in 4 parallel partitions (useful for CI or faster local runs)
|
||||
test-parallel tp:
|
||||
@echo "Running tests in 4 parallel partitions..."
|
||||
@mkdir -p /tmp/wanderer_test_results
|
||||
@rm -f /tmp/wanderer_test_results/partition_*.txt /tmp/wanderer_test_results/exit_*.txt
|
||||
@for i in 1 2 3 4; do \
|
||||
(MIX_TEST_PARTITION=$$i MIX_ENV=test mix test --partitions 4 2>&1; echo $$? > /tmp/wanderer_test_results/exit_$$i.txt) | \
|
||||
tee /tmp/wanderer_test_results/partition_$$i.txt | sed "s/^/[P$$i] /" & \
|
||||
done; \
|
||||
wait
|
||||
@echo ""
|
||||
@echo "========================================"
|
||||
@echo " TEST RESULTS SUMMARY"
|
||||
@echo "========================================"
|
||||
@total_tests=0; total_failures=0; total_excluded=0; all_passed=true; \
|
||||
for i in 1 2 3 4; do \
|
||||
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
|
||||
if [ "$$exit_code" != "0" ]; then all_passed=false; fi; \
|
||||
summary=$$(grep -E "^[0-9]+ (tests?|doctest)" /tmp/wanderer_test_results/partition_$$i.txt | tail -1 || echo "No results"); \
|
||||
tests=$$(echo "$$summary" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
failures=$$(echo "$$summary" | grep -oE "[0-9]+ failures?" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
excluded=$$(echo "$$summary" | grep -oE "[0-9]+ excluded" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
total_tests=$$((total_tests + tests)); \
|
||||
total_failures=$$((total_failures + failures)); \
|
||||
total_excluded=$$((total_excluded + excluded)); \
|
||||
if [ "$$exit_code" = "0" ]; then \
|
||||
echo "Partition $$i: ✓ $$summary"; \
|
||||
else \
|
||||
echo "Partition $$i: ✗ $$summary (exit code: $$exit_code)"; \
|
||||
fi; \
|
||||
done; \
|
||||
echo "========================================"; \
|
||||
echo "TOTAL: $$total_tests tests, $$total_failures failures, $$total_excluded excluded"; \
|
||||
echo "========================================"; \
|
||||
if [ "$$all_passed" = "true" ]; then \
|
||||
echo "✓ All partitions passed!"; \
|
||||
else \
|
||||
echo "✗ Some partitions failed. Details below:"; \
|
||||
echo ""; \
|
||||
for i in 1 2 3 4; do \
|
||||
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
|
||||
if [ "$$exit_code" != "0" ]; then \
|
||||
echo "======== PARTITION $$i FAILURES ========"; \
|
||||
grep -A 50 "Failures:" /tmp/wanderer_test_results/partition_$$i.txt 2>/dev/null || cat /tmp/wanderer_test_results/partition_$$i.txt; \
|
||||
echo ""; \
|
||||
fi; \
|
||||
done; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
coverage cover co:
|
||||
MIX_ENV=test mix test --cover
|
||||
|
||||
|
||||
@@ -264,7 +264,7 @@ config :logger,
|
||||
case config_env() do
|
||||
:prod -> "info"
|
||||
:dev -> "info"
|
||||
:test -> "debug"
|
||||
:test -> "warning"
|
||||
end
|
||||
)
|
||||
)
|
||||
|
||||
@@ -16,6 +16,11 @@ defmodule WandererApp.Api.AccessList do
|
||||
|
||||
includes([:owner, :members])
|
||||
|
||||
default_fields([
|
||||
:name,
|
||||
:description
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -79,12 +84,15 @@ defmodule WandererApp.Api.AccessList do
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :description, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
# Note: api_key intentionally not public for security
|
||||
attribute :api_key, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
@@ -16,6 +16,14 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
|
||||
includes([:access_list])
|
||||
|
||||
default_fields([
|
||||
:name,
|
||||
:eve_character_id,
|
||||
:eve_corporation_id,
|
||||
:eve_alliance_id,
|
||||
:role
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -89,22 +97,27 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_character_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_corporation_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_alliance_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :role, :atom do
|
||||
default "viewer"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
|
||||
@@ -19,9 +19,10 @@ defmodule WandererApp.Api.Changes.InjectMapFromActor do
|
||||
_other ->
|
||||
# nil or unexpected return shape - check for direct map_id
|
||||
# Check params (input), arguments, and attributes (in that order)
|
||||
map_id = Map.get(changeset.params, :map_id) ||
|
||||
Ash.Changeset.get_argument(changeset, :map_id) ||
|
||||
Ash.Changeset.get_attribute(changeset, :map_id)
|
||||
map_id =
|
||||
Map.get(changeset.params, :map_id) ||
|
||||
Ash.Changeset.get_argument(changeset, :map_id) ||
|
||||
Ash.Changeset.get_attribute(changeset, :map_id)
|
||||
|
||||
case map_id do
|
||||
nil ->
|
||||
|
||||
@@ -13,6 +13,8 @@ defmodule WandererApp.Api.Map do
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("maps_v1")
|
||||
|
||||
migration_defaults scopes: "'{wormholes}'"
|
||||
end
|
||||
|
||||
json_api do
|
||||
@@ -111,6 +113,7 @@ defmodule WandererApp.Api.Map do
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:scopes,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
@@ -135,6 +138,7 @@ defmodule WandererApp.Api.Map do
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:scopes,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
@@ -209,7 +213,7 @@ defmodule WandererApp.Api.Map do
|
||||
end
|
||||
|
||||
create :duplicate do
|
||||
accept [:name, :description, :scope, :only_tracked_characters]
|
||||
accept [:name, :description, :scope, :scopes, :only_tracked_characters]
|
||||
argument :source_map_id, :uuid, allow_nil?: false
|
||||
argument :copy_acls, :boolean, default: true
|
||||
argument :copy_user_settings, :boolean, default: true
|
||||
@@ -225,9 +229,14 @@ defmodule WandererApp.Api.Map do
|
||||
description =
|
||||
Ash.Changeset.get_attribute(changeset, :description) || source_map.description
|
||||
|
||||
# Use provided scopes or fall back to source map scopes
|
||||
scopes =
|
||||
Ash.Changeset.get_attribute(changeset, :scopes) || source_map.scopes
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.change_attribute(:description, description)
|
||||
|> Ash.Changeset.change_attribute(:scope, source_map.scope)
|
||||
|> Ash.Changeset.change_attribute(:scopes, scopes)
|
||||
|> Ash.Changeset.change_attribute(
|
||||
:only_tracked_characters,
|
||||
source_map.only_tracked_characters
|
||||
@@ -359,6 +368,24 @@ defmodule WandererApp.Api.Map do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :scopes, {:array, :atom} do
|
||||
default([:wormholes])
|
||||
allow_nil?(true)
|
||||
public?(true)
|
||||
|
||||
constraints(
|
||||
items: [
|
||||
one_of: [
|
||||
:wormholes,
|
||||
:hi,
|
||||
:low,
|
||||
:null,
|
||||
:pochven
|
||||
]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
@@ -27,6 +27,11 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
includes([:map, :character])
|
||||
|
||||
default_fields([
|
||||
:tracked,
|
||||
:followed
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -219,14 +224,17 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
attribute :tracked, :boolean do
|
||||
default false
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :followed, :boolean do
|
||||
default false
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
# Note: These attributes are encrypted (AshCloak) and intentionally not public
|
||||
attribute :solar_system_id, :integer
|
||||
attribute :structure_id, :integer
|
||||
attribute :station_id, :integer
|
||||
|
||||
@@ -22,6 +22,19 @@ defmodule WandererApp.Api.MapConnection do
|
||||
|
||||
includes([:map])
|
||||
|
||||
default_fields([
|
||||
:solar_system_source,
|
||||
:solar_system_target,
|
||||
:mass_status,
|
||||
:time_status,
|
||||
:ship_size_type,
|
||||
:type,
|
||||
:wormhole_type,
|
||||
:count_of_passage,
|
||||
:locked,
|
||||
:custom_info
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -197,15 +210,20 @@ defmodule WandererApp.Api.MapConnection do
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :solar_system_source, :integer
|
||||
attribute :solar_system_target, :integer
|
||||
attribute :solar_system_source, :integer do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_target, :integer do
|
||||
public? true
|
||||
end
|
||||
|
||||
# where 0 - greater than half
|
||||
# where 1 - less than half
|
||||
# where 2 - critical less than 10%
|
||||
attribute :mass_status, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -218,7 +236,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# 6 - EOL 48h
|
||||
attribute :time_status, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -229,7 +247,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# where 4 - Capital
|
||||
attribute :ship_size_type, :integer do
|
||||
default(2)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -238,21 +256,26 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# where 2 - Bridge
|
||||
attribute :type, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :wormhole_type, :string
|
||||
attribute :wormhole_type, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :count_of_passage, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :locked, :boolean
|
||||
attribute :locked, :boolean do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :custom_info, :string do
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
||||
:updated_by
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:settings
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_default_settings")
|
||||
|
||||
@@ -93,6 +97,7 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
||||
|
||||
attribute :settings, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
constraints min_length: 2
|
||||
description "JSON string containing the default map settings"
|
||||
end
|
||||
|
||||
@@ -18,6 +18,15 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
:map
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:plan,
|
||||
:status,
|
||||
:characters_limit,
|
||||
:hubs_limit,
|
||||
:active_till,
|
||||
:auto_renew?
|
||||
])
|
||||
|
||||
# Enable automatic filtering and sorting
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
@@ -135,6 +144,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :plan, :atom do
|
||||
default "alpha"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -150,6 +160,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :status, :atom do
|
||||
default "active"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -164,22 +175,24 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :characters_limit, :integer do
|
||||
default(100)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :hubs_limit, :integer do
|
||||
default(10)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :active_till, :utc_datetime do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :auto_renew?, :boolean do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
|
||||
@@ -19,6 +19,10 @@ defmodule WandererApp.Api.MapSystemComment do
|
||||
:character
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:text
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_system_comments")
|
||||
|
||||
@@ -73,6 +77,7 @@ defmodule WandererApp.Api.MapSystemComment do
|
||||
|
||||
attribute :text, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
|
||||
@@ -16,6 +16,20 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
|
||||
includes([:system])
|
||||
|
||||
default_fields([
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:description,
|
||||
:temporary_name,
|
||||
:type,
|
||||
:linked_system_id,
|
||||
:kind,
|
||||
:group,
|
||||
:custom_info,
|
||||
:deleted
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -184,42 +198,56 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
|
||||
attribute :eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :description, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :temporary_name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :type, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :linked_system_id, :integer do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :kind, :string
|
||||
attribute :group, :string
|
||||
attribute :kind, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :group, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :custom_info, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :deleted, :boolean do
|
||||
allow_nil? false
|
||||
default false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :update_forced_at, :utc_datetime do
|
||||
|
||||
@@ -41,6 +41,21 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
:system
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
])
|
||||
|
||||
# Enable automatic filtering and sorting
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
@@ -151,50 +166,62 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
|
||||
attribute :structure_type_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :structure_type, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_id, :integer do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :notes, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_ticker, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :status, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :end_time, :utc_datetime_usec do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp :inserted_at
|
||||
|
||||
@@ -24,6 +24,13 @@ defmodule WandererApp.Api.MapUserSettings do
|
||||
:user
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:settings,
|
||||
:main_character_eve_id,
|
||||
:following_character_eve_id,
|
||||
:hubs
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_user_settings")
|
||||
|
||||
@@ -85,19 +92,22 @@ defmodule WandererApp.Api.MapUserSettings do
|
||||
|
||||
attribute :settings, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :main_character_eve_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :following_character_eve_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :hubs, {:array, :string} do
|
||||
allow_nil?(true)
|
||||
|
||||
public? true
|
||||
default([])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +31,13 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
includes([:character, :user])
|
||||
|
||||
default_fields([
|
||||
:entity_id,
|
||||
:entity_type,
|
||||
:event_type,
|
||||
:event_data
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -86,10 +93,12 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
attribute :entity_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :entity_type, :atom do
|
||||
default "map"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -104,6 +113,7 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
attribute :event_type, :atom do
|
||||
default "custom"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -153,7 +163,9 @@ defmodule WandererApp.Api.UserActivity do
|
||||
allow_nil?(false)
|
||||
end
|
||||
|
||||
attribute :event_data, :string
|
||||
attribute :event_data, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
|
||||
@@ -45,7 +45,7 @@ defmodule WandererApp.Cache do
|
||||
def insert({id, key}, value, opts) when is_binary(id) and (is_binary(key) or is_atom(key)),
|
||||
do: insert("#{id}:#{key}", value, opts)
|
||||
|
||||
def insert(key, nil, opts) when is_binary(key) or is_atom(key), do: delete(key)
|
||||
def insert(key, nil, _opts) when is_binary(key) or is_atom(key), do: delete(key)
|
||||
def insert(key, value, opts) when is_binary(key) or is_atom(key), do: put(key, value, opts)
|
||||
|
||||
def insert_or_update(key, value, update_fn, opts \\ [])
|
||||
|
||||
@@ -598,9 +598,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :skipped}
|
||||
end
|
||||
|
||||
_ ->
|
||||
@@ -799,7 +796,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
corporation_id
|
||||
|> WandererApp.Esi.get_corporation_info()
|
||||
|> case do
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker}} ->
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
@@ -1002,7 +999,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
defp maybe_update_active_maps(
|
||||
%{character_id: character_id, active_maps: active_maps} =
|
||||
state,
|
||||
%{map_id: map_id, track: true} = track_settings
|
||||
%{map_id: map_id, track: true}
|
||||
) do
|
||||
if not Enum.member?(active_maps, map_id) do
|
||||
WandererApp.Cache.put(
|
||||
|
||||
@@ -40,10 +40,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
||||
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
|
||||
|
||||
Logger.debug("[TrackerManager] Initialized with intervals: " <>
|
||||
"garbage_collection=#{div(@garbage_collection_interval, 60_000)}min, " <>
|
||||
"untrack=#{div(@untrack_characters_interval, 60_000)}min, " <>
|
||||
"inactive_timeout=#{div(@inactive_character_timeout, 60_000)}min")
|
||||
Logger.debug(
|
||||
"[TrackerManager] Initialized with intervals: " <>
|
||||
"garbage_collection=#{div(@garbage_collection_interval, 60_000)}min, " <>
|
||||
"untrack=#{div(@untrack_characters_interval, 60_000)}min, " <>
|
||||
"inactive_timeout=#{div(@inactive_character_timeout, 60_000)}min"
|
||||
)
|
||||
|
||||
%{
|
||||
characters: [],
|
||||
@@ -57,7 +59,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
WandererApp.Cache.insert("tracked_characters", [])
|
||||
|
||||
if length(tracked_characters) > 0 do
|
||||
Logger.debug("[TrackerManager] Restoring #{length(tracked_characters)} tracked characters from cache")
|
||||
Logger.debug(
|
||||
"[TrackerManager] Restoring #{length(tracked_characters)} tracked characters from cache"
|
||||
)
|
||||
end
|
||||
|
||||
tracked_characters
|
||||
@@ -197,6 +201,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
[],
|
||||
fn untrack_queue ->
|
||||
original_length = length(untrack_queue)
|
||||
|
||||
filtered =
|
||||
untrack_queue
|
||||
|> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)
|
||||
|
||||
@@ -88,15 +88,4 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
|
||||
{:ok, pid}
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_child(uuid) do
|
||||
case Registry.lookup(@registry, uuid) do
|
||||
[{pid, _}] ->
|
||||
GenServer.cast(pid, :stop)
|
||||
|
||||
_ ->
|
||||
Logger.warn("Unable to locate pool assigned to #{inspect(uuid)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,7 +38,7 @@ defmodule WandererApp.Character.TrackingConfigUtils do
|
||||
%{id: "default", title: "Default", value: default_count}
|
||||
]
|
||||
|
||||
{:ok, pools_count} =
|
||||
{:ok, _pools_count} =
|
||||
Cachex.get(
|
||||
:esi_auth_cache,
|
||||
"configs_total_count"
|
||||
|
||||
@@ -56,13 +56,7 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
Only includes characters that have actual tracking permission.
|
||||
"""
|
||||
def build_tracking_data(map_id, current_user_id) do
|
||||
with {:ok, map} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
),
|
||||
with {:ok, map} <- WandererApp.MapRepo.get(map_id),
|
||||
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
||||
@@ -75,7 +69,11 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
build_character_tracking_data(characters_with_tracking_permission)
|
||||
|
||||
{:ok, main_character} =
|
||||
get_main_character(user_settings, characters_with_tracking_permission, characters_with_tracking_permission)
|
||||
get_main_character(
|
||||
user_settings,
|
||||
characters_with_tracking_permission,
|
||||
characters_with_tracking_permission
|
||||
)
|
||||
|
||||
following_character_eve_id =
|
||||
case user_settings do
|
||||
@@ -195,7 +193,13 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
{true, settings_result} ->
|
||||
case check_character_tracking_permission(character, map_id) do
|
||||
{:ok, :allowed} ->
|
||||
do_update_character_tracking_impl(character, map_id, track, caller_pid, settings_result)
|
||||
do_update_character_tracking_impl(
|
||||
character,
|
||||
map_id,
|
||||
track,
|
||||
caller_pid,
|
||||
settings_result
|
||||
)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning(
|
||||
|
||||
@@ -8,7 +8,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
@ttl :timer.hours(1)
|
||||
|
||||
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
|
||||
@req_esi_options [base_url: "https://esi.evetech.net", finch: WandererApp.Finch]
|
||||
|
||||
@cache_opts [cache: true]
|
||||
@retry_opts [retry: false, retry_log_level: :warning]
|
||||
@@ -74,7 +73,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
)
|
||||
|
||||
def get_routes_eve(hubs, origin, params, opts),
|
||||
def get_routes_eve(hubs, origin, _params, _opts),
|
||||
do:
|
||||
{:ok,
|
||||
hubs
|
||||
@@ -101,33 +100,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end)}
|
||||
|
||||
defp do_get_routes_eve(origin, destination, params, opts) do
|
||||
esi_params =
|
||||
Map.merge(params, %{
|
||||
connections: params.connections |> Enum.join(","),
|
||||
avoid: params.avoid |> Enum.join(",")
|
||||
})
|
||||
|
||||
do_get(
|
||||
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|> case do
|
||||
{:ok, result} ->
|
||||
%{
|
||||
"origin" => origin,
|
||||
"destination" => destination,
|
||||
"systems" => result,
|
||||
"success" => true
|
||||
}
|
||||
|
||||
error ->
|
||||
Logger.warning("Error getting routes: #{inspect(error)}")
|
||||
%{"origin" => origin, "destination" => destination, "systems" => [], "success" => false}
|
||||
end
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "group-info-#{group_id}",
|
||||
@@ -273,6 +245,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
# Note: search_val and categories_val are used by the @decorate cacheable annotation above
|
||||
_unused = {search_val, categories_val}
|
||||
get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
end
|
||||
|
||||
@@ -348,7 +322,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
defp with_cache_opts(opts),
|
||||
do: opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
|
||||
|
||||
defp do_get(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
defp do_get(path, api_opts, opts, pool \\ @general_pool) do
|
||||
case Cachex.get(:api_cache, path) do
|
||||
{:ok, cached_data} when not is_nil(cached_data) ->
|
||||
{:ok, cached_data}
|
||||
@@ -358,7 +332,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_request(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
defp do_get_request(path, api_opts, opts, pool) do
|
||||
try do
|
||||
req_options_for_pool(pool)
|
||||
|> Req.new()
|
||||
@@ -448,7 +422,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, %{status: status} = _error} when status in [401, 403] ->
|
||||
do_get_retry(path, api_opts, opts)
|
||||
|
||||
{:ok, %{status: status, headers: headers}} ->
|
||||
{:ok, %{status: status}} ->
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
|
||||
{:error, %Mint.TransportError{reason: :timeout}} ->
|
||||
@@ -832,10 +806,10 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp handle_refresh_token_result(
|
||||
{:error, %OAuth2.Error{reason: :econnrefused} = error},
|
||||
character,
|
||||
_character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
_scopes
|
||||
) do
|
||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||
|
||||
@@ -393,9 +393,6 @@ defmodule WandererApp.EveDataService do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_solar_system_name(solar_system_name, wormhole_class) do
|
||||
end
|
||||
|
||||
defp get_triglavian_data(default_data, triglavian_systems, solar_system_id) do
|
||||
case Enum.find(triglavian_systems, fn system -> system.solar_system_id == solar_system_id end) do
|
||||
nil ->
|
||||
@@ -413,8 +410,12 @@ defmodule WandererApp.EveDataService do
|
||||
|
||||
defp get_security(security) do
|
||||
case security do
|
||||
nil -> {:ok, ""}
|
||||
_ -> {:ok, String.to_float(security) |> get_true_security() |> Float.to_string(decimals: 1)}
|
||||
nil ->
|
||||
{:ok, ""}
|
||||
|
||||
_ ->
|
||||
{:ok,
|
||||
String.to_float(security) |> get_true_security() |> :erlang.float_to_binary(decimals: 1)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -496,23 +497,23 @@ defmodule WandererApp.EveDataService do
|
||||
do: {:ok, 10_100}
|
||||
|
||||
defp get_wormhole_class_id(systems, region_id, constellation_id, solar_system_id) do
|
||||
with region <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||
end),
|
||||
constellation <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
||||
end),
|
||||
solar_system <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||
end),
|
||||
wormhole_class_id <- get_wormhole_class_id(region, constellation, solar_system) do
|
||||
{:ok, wormhole_class_id}
|
||||
else
|
||||
_ -> {:ok, -1}
|
||||
end
|
||||
region =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||
end)
|
||||
|
||||
constellation =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
||||
end)
|
||||
|
||||
solar_system =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||
end)
|
||||
|
||||
wormhole_class_id = get_wormhole_class_id(region, constellation, solar_system)
|
||||
{:ok, wormhole_class_id}
|
||||
end
|
||||
|
||||
defp get_wormhole_class_id(_region, _constellation, solar_system)
|
||||
|
||||
@@ -178,6 +178,10 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_payload(payload, visited) when is_map(payload) do
|
||||
Map.new(payload, fn {k, v} -> {to_string(k), serialize_value(v, visited)} end)
|
||||
end
|
||||
|
||||
# Get allowed fields based on struct type
|
||||
defp get_allowed_fields(module) do
|
||||
module_name = module |> Module.split() |> List.last()
|
||||
@@ -192,10 +196,6 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_payload(payload, visited) when is_map(payload) do
|
||||
Map.new(payload, fn {k, v} -> {to_string(k), serialize_value(v, visited)} end)
|
||||
end
|
||||
|
||||
defp serialize_fields(fields, visited) do
|
||||
Enum.reduce(fields, %{}, fn {k, v}, acc ->
|
||||
if is_nil(v) do
|
||||
|
||||
@@ -98,8 +98,8 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
|
||||
"id" => payload["system_id"] || payload[:system_id],
|
||||
"attributes" => %{
|
||||
"locked" => payload["locked"] || payload[:locked],
|
||||
"x" => payload["x"] || payload[:x],
|
||||
"y" => payload["y"] || payload[:y],
|
||||
"position_x" => payload["position_x"] || payload[:position_x],
|
||||
"position_y" => payload["position_y"] || payload[:position_y],
|
||||
"updated_at" => event.timestamp
|
||||
},
|
||||
"relationships" => %{
|
||||
|
||||
@@ -182,7 +182,7 @@ defmodule WandererApp.Kills.Client do
|
||||
end
|
||||
|
||||
# Guard against duplicate disconnection events
|
||||
def handle_info({:disconnected, reason}, %{connected: false, connecting: false} = state) do
|
||||
def handle_info({:disconnected, _reason}, %{connected: false, connecting: false} = state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -566,7 +566,7 @@ defmodule WandererApp.Kills.Client do
|
||||
end
|
||||
end
|
||||
|
||||
defp check_health(%{socket_pid: pid} = state) do
|
||||
defp check_health(%{socket_pid: pid}) do
|
||||
if socket_alive?(pid) do
|
||||
:healthy
|
||||
else
|
||||
@@ -590,22 +590,6 @@ defmodule WandererApp.Kills.Client do
|
||||
Process.send_after(self(), :health_check, @health_check_interval)
|
||||
end
|
||||
|
||||
defp handle_connection_lost(%{connected: false} = _state) do
|
||||
Logger.debug("[Client] Connection already lost, skipping cleanup")
|
||||
end
|
||||
|
||||
defp handle_connection_lost(state) do
|
||||
Logger.warning("[Client] Connection lost, cleaning up and reconnecting")
|
||||
|
||||
# Clean up existing socket
|
||||
if state.socket_pid do
|
||||
disconnect_socket(state.socket_pid)
|
||||
end
|
||||
|
||||
# Reset state and trigger reconnection
|
||||
send(self(), {:disconnected, :connection_lost})
|
||||
end
|
||||
|
||||
# Handler module for WebSocket events
|
||||
defmodule Handler do
|
||||
@moduledoc """
|
||||
@@ -640,7 +624,7 @@ defmodule WandererApp.Kills.Client do
|
||||
}
|
||||
|
||||
case GenSocketClient.join(transport, "killmails:lobby", join_params) do
|
||||
{:ok, response} ->
|
||||
{:ok, _response} ->
|
||||
send(state.parent, {:connected, self()})
|
||||
# Reset disconnected flag on successful connection
|
||||
{:ok, %{state | disconnected: false}}
|
||||
|
||||
@@ -46,7 +46,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(%{event: :map_server_started, payload: map_info}, state) do
|
||||
def handle_info(%{event: :map_server_started, payload: _map_info}, state) do
|
||||
{:noreply, schedule_subscription_update(state)}
|
||||
end
|
||||
|
||||
@@ -191,7 +191,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
||||
# Client is not connected, retry with backoff
|
||||
schedule_retry_update(state)
|
||||
|
||||
error ->
|
||||
_error ->
|
||||
schedule_retry_update(state)
|
||||
end
|
||||
rescue
|
||||
|
||||
@@ -177,7 +177,7 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
|
||||
def list_hubs(map_id, hubs) do
|
||||
{:ok, map} = map_id |> get_map()
|
||||
{:ok, _map} = map_id |> get_map()
|
||||
|
||||
{:ok, hubs}
|
||||
end
|
||||
@@ -315,7 +315,7 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
end
|
||||
|
||||
def update_subscription_settings!(%{map_id: map_id} = map, %{
|
||||
def update_subscription_settings!(%{map_id: map_id} = _map, %{
|
||||
characters_limit: characters_limit,
|
||||
hubs_limit: hubs_limit
|
||||
}) do
|
||||
@@ -326,7 +326,7 @@ defmodule WandererApp.Map do
|
||||
|> get_map!()
|
||||
end
|
||||
|
||||
def update_options!(%{map_id: map_id} = map, options) do
|
||||
def update_options!(%{map_id: map_id} = _map, options) do
|
||||
map_id
|
||||
|> update_map(%{options: options})
|
||||
|
||||
|
||||
@@ -76,11 +76,6 @@ defmodule WandererApp.Map.Operations do
|
||||
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
||||
defdelegate create_connection(map_id, attrs, char_id), to: Connections
|
||||
|
||||
@doc "Create a connection from a Plug.Conn"
|
||||
@spec create_connection(Plug.Conn.t(), map()) ::
|
||||
{:ok, :created} | {:skip, :exists} | {:error, atom()}
|
||||
defdelegate create_connection(conn, attrs), to: Connections
|
||||
|
||||
@doc "Update a connection"
|
||||
@spec update_connection(String.t(), String.t(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
|
||||
@@ -329,6 +329,9 @@ defmodule WandererApp.Map.MapPool do
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||
|
||||
defp do_start_map(map_id, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||
if map_id in map_ids do
|
||||
# Map already started
|
||||
@@ -344,8 +347,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
[map_id | r_map_ids]
|
||||
end)
|
||||
|
||||
completed_operations = [:registry | completed_operations]
|
||||
|
||||
case registry_result do
|
||||
{new_value, _old_value} when is_list(new_value) ->
|
||||
:ok
|
||||
@@ -363,13 +364,9 @@ defmodule WandererApp.Map.MapPool do
|
||||
raise "Failed to add to cache: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
completed_operations = [:cache | completed_operations]
|
||||
|
||||
# Step 3: Start the map server using extracted helper
|
||||
do_initialize_map_server(map_id)
|
||||
|
||||
completed_operations = [:map_server | completed_operations]
|
||||
|
||||
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
||||
new_state = %{state | map_ids: [map_id | map_ids]}
|
||||
|
||||
@@ -445,8 +442,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
r_map_ids |> Enum.reject(fn id -> id == map_id end)
|
||||
end)
|
||||
|
||||
completed_operations = [:registry | completed_operations]
|
||||
|
||||
case registry_result do
|
||||
{new_value, _old_value} when is_list(new_value) ->
|
||||
:ok
|
||||
@@ -464,14 +459,10 @@ defmodule WandererApp.Map.MapPool do
|
||||
raise "Failed to delete from cache: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
completed_operations = [:cache | completed_operations]
|
||||
|
||||
# Step 3: Stop the map server (clean up all map resources)
|
||||
map_id
|
||||
|> Server.Impl.stop_map()
|
||||
|
||||
completed_operations = [:map_server | completed_operations]
|
||||
|
||||
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
||||
new_state = %{state | map_ids: map_ids |> Enum.reject(fn id -> id == map_id end)}
|
||||
|
||||
@@ -560,9 +551,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
# and the cleanup operations are safe to leave in a "stopped" state
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||
|
||||
@impl true
|
||||
def handle_info(:backup_state, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||
|
||||
@@ -179,15 +179,4 @@ defmodule WandererApp.Map.MapPoolDynamicSupervisor do
|
||||
{:ok, pid}
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_child(uuid) do
|
||||
case Registry.lookup(@registry, uuid) do
|
||||
[{pid, _}] ->
|
||||
GenServer.cast(pid, :stop)
|
||||
|
||||
_ ->
|
||||
Logger.warn("Unable to locate pool assigned to #{inspect(uuid)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,7 +77,7 @@ defmodule WandererApp.Map.Routes do
|
||||
end
|
||||
end
|
||||
|
||||
def find(_map_id, hubs, origin, routes_settings, true) do
|
||||
def find(_map_id, hubs, origin, _routes_settings, true) do
|
||||
origin = origin |> String.to_integer()
|
||||
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
|
||||
|
||||
|
||||
@@ -93,10 +93,8 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines the ship size for a connection, applying wormhole‑specific rules
|
||||
for C1, C13, and C4⇄NS links, falling back to the caller’s provided size or Large.
|
||||
"""
|
||||
# Determines the ship size for a connection, applying wormhole-specific rules
|
||||
# for C1, C13, and C4⇄NS links, falling back to the caller's provided size or Large.
|
||||
defp resolve_ship_size(type_val, ship_size_val, src_info, tgt_info) do
|
||||
case parse_type(type_val) do
|
||||
@connection_type_wormhole ->
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererApp.Map.Operations.Duplication do
|
||||
"""
|
||||
|
||||
require Logger
|
||||
import Ash.Query, only: [filter: 2]
|
||||
|
||||
alias WandererApp.Api
|
||||
alias WandererApp.Api.{MapSystem, MapConnection, MapSystemSignature, MapCharacterSettings}
|
||||
|
||||
@@ -814,21 +814,24 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
do: :ok
|
||||
|
||||
defp update_location(
|
||||
%{map: %{scope: scope}, map_id: map_id, map_opts: map_opts} =
|
||||
%{map: map, map_id: map_id, map_opts: map_opts} =
|
||||
_state,
|
||||
character_id,
|
||||
location,
|
||||
old_location
|
||||
) do
|
||||
scopes = get_effective_scopes(map)
|
||||
|
||||
ConnectionsImpl.is_connection_valid(
|
||||
scope,
|
||||
scopes,
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id
|
||||
)
|
||||
|> case do
|
||||
true ->
|
||||
# Add new location system
|
||||
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts) do
|
||||
# Connection is valid (at least one system matches scopes)
|
||||
# Add systems that match the map's scopes - individual system filtering by maybe_add_system
|
||||
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts, scopes) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
@@ -838,8 +841,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
)
|
||||
end
|
||||
|
||||
# Add old location system (in case it wasn't on map)
|
||||
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts) do
|
||||
# Add old location system (in case it wasn't on map) - only if it matches scopes
|
||||
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts, scopes) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
@@ -879,6 +882,24 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
defp is_character_in_space?(%{station_id: station_id, structure_id: structure_id} = _location),
|
||||
do: is_nil(structure_id) && is_nil(station_id)
|
||||
|
||||
@doc """
|
||||
Get effective scopes from map, with fallback to legacy scope.
|
||||
Returns the scopes array that should be used for filtering.
|
||||
"""
|
||||
def get_effective_scopes(%{scopes: scopes}) when is_list(scopes) and scopes != [], do: scopes
|
||||
|
||||
def get_effective_scopes(%{scope: scope}) when is_atom(scope),
|
||||
do: legacy_scope_to_scopes(scope)
|
||||
|
||||
def get_effective_scopes(_), do: [:wormholes]
|
||||
|
||||
# Legacy scope to new scopes array conversion
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
defp add_character(
|
||||
map_id,
|
||||
%{id: character_id} = map_character,
|
||||
|
||||
@@ -57,6 +57,12 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
@known_space [@hs, @ls, @ns, @pochven]
|
||||
|
||||
# Individual space type lists for granular scope matching
|
||||
@hi_space [@hs]
|
||||
@low_space [@ls]
|
||||
@null_space [@ns]
|
||||
@pochven_space [@pochven]
|
||||
|
||||
@prohibited_systems [@jita]
|
||||
@prohibited_system_classes [
|
||||
@a1,
|
||||
@@ -100,7 +106,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
@connection_type_wormhole 0
|
||||
@connection_type_stargate 1
|
||||
@connection_type_bridge 2
|
||||
# @connection_type_bridge 2 # reserved for future use
|
||||
@medium_ship_size 1
|
||||
|
||||
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
|
||||
@@ -343,6 +349,27 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
solar_system_source: solar_system_source_id,
|
||||
solar_system_target: solar_system_target_id
|
||||
} ->
|
||||
# Emit telemetry for connection auto-deletion
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :connection_cleanup, :delete],
|
||||
%{system_time: System.system_time()},
|
||||
%{
|
||||
map_id: map_id,
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id,
|
||||
reason: :auto_cleanup
|
||||
}
|
||||
)
|
||||
|
||||
# Log auto-deletion for audit trail (no user/character context for auto-cleanup)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id
|
||||
})
|
||||
|
||||
delete_connection(map_id, %{
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id
|
||||
@@ -403,7 +430,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
time_status: time_status,
|
||||
solar_system_source: solar_system_source,
|
||||
solar_system_target: solar_system_target
|
||||
} = updated_connection
|
||||
} = _updated_connection
|
||||
) do
|
||||
with source_system when not is_nil(source_system) <-
|
||||
WandererApp.Map.find_system_by_location(
|
||||
@@ -644,31 +671,49 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
start_time
|
||||
)
|
||||
|
||||
def can_add_location(_scope, nil), do: false
|
||||
def can_add_location(_scopes, nil), do: false
|
||||
|
||||
def can_add_location(:none, _solar_system_id), do: false
|
||||
def can_add_location([], _solar_system_id), do: false
|
||||
|
||||
def can_add_location(scope, solar_system_id) do
|
||||
def can_add_location(scopes, solar_system_id) when is_list(scopes) do
|
||||
{:ok, system_static_info} = get_system_static_info(solar_system_id)
|
||||
|
||||
case scope do
|
||||
:wormholes ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
|
||||
@wh_space |> Enum.member?(system_static_info.system_class)
|
||||
|
||||
:stargates ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
@known_space |> Enum.member?(system_static_info.system_class)
|
||||
|
||||
:all ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
|
||||
system_matches_any_scope?(system_static_info.system_class, scopes)
|
||||
end
|
||||
|
||||
# Legacy support for single scope atom
|
||||
def can_add_location(:none, _solar_system_id), do: false
|
||||
|
||||
def can_add_location(scope, solar_system_id) when is_atom(scope) do
|
||||
can_add_location(legacy_scope_to_scopes(scope), solar_system_id)
|
||||
end
|
||||
|
||||
# Helper function to check if a system class matches any of the selected scopes
|
||||
defp system_matches_any_scope?(_system_class, []), do: false
|
||||
|
||||
defp system_matches_any_scope?(system_class, scopes) do
|
||||
Enum.any?(scopes, fn scope ->
|
||||
system_matches_scope?(system_class, scope)
|
||||
end)
|
||||
end
|
||||
|
||||
# Individual scope matching functions
|
||||
defp system_matches_scope?(system_class, :wormholes), do: system_class in @wh_space
|
||||
defp system_matches_scope?(system_class, :hi), do: system_class in @hi_space
|
||||
defp system_matches_scope?(system_class, :low), do: system_class in @low_space
|
||||
defp system_matches_scope?(system_class, :null), do: system_class in @null_space
|
||||
defp system_matches_scope?(system_class, :pochven), do: system_class in @pochven_space
|
||||
defp system_matches_scope?(_system_class, _), do: false
|
||||
|
||||
# Legacy scope to new scopes array conversion
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
def is_prohibited_system_class?(system_class) do
|
||||
@prohibited_system_classes |> Enum.member?(system_class)
|
||||
end
|
||||
@@ -688,17 +733,59 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
)
|
||||
)
|
||||
|
||||
def is_connection_valid(_scope, from_solar_system_id, to_solar_system_id)
|
||||
def is_connection_valid(_scopes, from_solar_system_id, to_solar_system_id)
|
||||
when is_nil(from_solar_system_id) or is_nil(to_solar_system_id),
|
||||
do: false
|
||||
|
||||
def is_connection_valid([], _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
# New array-based scopes support
|
||||
def is_connection_valid(scopes, from_solar_system_id, to_solar_system_id)
|
||||
when is_list(scopes) and from_solar_system_id != to_solar_system_id do
|
||||
with {: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
|
||||
# First check: neither system is prohibited
|
||||
not_prohibited =
|
||||
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))
|
||||
|
||||
if not_prohibited do
|
||||
from_is_wormhole = from_system_static_info.system_class in @wh_space
|
||||
to_is_wormhole = to_system_static_info.system_class in @wh_space
|
||||
wormholes_enabled = :wormholes in scopes
|
||||
|
||||
# Wormhole border behavior: if wormholes scope is enabled AND at least one
|
||||
# system is a wormhole, allow the connection (adds border k-space systems)
|
||||
# Otherwise: BOTH systems must match the configured scopes
|
||||
if wormholes_enabled and (from_is_wormhole or to_is_wormhole) do
|
||||
# At least one system matches (wormhole matches :wormholes, or other matches its scope)
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) or
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
else
|
||||
# Non-wormhole movement: both systems must match scopes
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) and
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
end
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy support: :all scope
|
||||
def is_connection_valid(:all, from_solar_system_id, to_solar_system_id),
|
||||
do: from_solar_system_id != to_solar_system_id
|
||||
|
||||
# Legacy support: :none scope
|
||||
def is_connection_valid(:none, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
# Legacy support: single atom scope (including :stargates which is used for connection type detection)
|
||||
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
|
||||
when from_solar_system_id != to_solar_system_id do
|
||||
when is_atom(scope) and from_solar_system_id != to_solar_system_id do
|
||||
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
|
||||
@@ -712,7 +799,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
:stargates ->
|
||||
# For stargates, we need to check:
|
||||
# 1. Both systems are in known space (HS, LS, NS)
|
||||
# 1. Both systems are in known space (HS, LS, NS, Pochven)
|
||||
# 2. There is a known jump between them
|
||||
# 3. Neither system is prohibited
|
||||
from_system_static_info.system_class in @known_space and
|
||||
@@ -720,13 +807,21 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
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?())
|
||||
|
||||
_ ->
|
||||
# For other legacy scopes, convert to array and use new logic
|
||||
is_connection_valid(
|
||||
legacy_scope_to_scopes(scope),
|
||||
from_solar_system_id,
|
||||
to_solar_system_id
|
||||
)
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def is_connection_valid(_scope, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
def is_connection_valid(_scopes, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
def get_connection_mark_eol_time(map_id, connection_id, default \\ DateTime.utc_now()) do
|
||||
WandererApp.Cache.get("map_#{map_id}:conn_#{connection_id}:mark_eol_time")
|
||||
@@ -901,9 +996,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_time_status(_source_solar_system_id, _target_solar_system_id, _ship_size_type),
|
||||
do: @connection_time_status_default
|
||||
|
||||
defp get_new_time_status(_start_time, @connection_time_status_default),
|
||||
do: @connection_time_status_eol_24
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Logger.error("Cannot start map #{map_id}: map not loaded")
|
||||
{:error, :map_not_loaded}
|
||||
|
||||
map ->
|
||||
_map ->
|
||||
with :ok <- AclsImpl.track_acls(acls |> Enum.map(& &1.access_list_id)) do
|
||||
@pubsub_client.subscribe(
|
||||
WandererApp.PubSub,
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
|
||||
alias WandererApp.Map.Server.Impl
|
||||
|
||||
@ping_auto_expire_timeout :timer.minutes(15)
|
||||
# @ping_auto_expire_timeout :timer.minutes(15) # reserved for future use
|
||||
|
||||
def add_ping(
|
||||
map_id,
|
||||
|
||||
@@ -129,8 +129,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
def remove_system_comment(
|
||||
map_id,
|
||||
comment_id,
|
||||
user_id,
|
||||
character_id
|
||||
_user_id,
|
||||
_character_id
|
||||
) do
|
||||
{:ok, %{system_id: system_id} = comment} =
|
||||
WandererApp.MapSystemCommentRepo.get_by_id(comment_id)
|
||||
@@ -309,7 +309,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
map_id
|
||||
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
|
||||
|> case do
|
||||
{:ok, result} ->
|
||||
{:ok, _result} ->
|
||||
:ok = WandererApp.Map.remove_system(map_id, solar_system_id)
|
||||
@ddrt.delete([solar_system_id], "rtree_#{map_id}")
|
||||
Impl.broadcast!(map_id, :systems_removed, [solar_system_id])
|
||||
@@ -383,6 +383,16 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|> Enum.each(fn connection ->
|
||||
try do
|
||||
Logger.debug(fn -> "Removing connection from map: #{inspect(connection)}" end)
|
||||
|
||||
# Audit logging for cascade deletion (no user/character context)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_source_id: connection.solar_system_source,
|
||||
solar_system_target_id: connection.solar_system_target
|
||||
})
|
||||
|
||||
:ok = WandererApp.MapConnectionRepo.destroy(map_id, connection)
|
||||
:ok = WandererApp.Map.remove_connection(map_id, connection)
|
||||
Impl.broadcast!(map_id, :remove_connections, [connection])
|
||||
@@ -393,35 +403,76 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end)
|
||||
end
|
||||
|
||||
# When destination systems are deleted, unlink signatures instead of destroying them.
|
||||
# This preserves the user's scan data while removing the stale link.
|
||||
defp cleanup_linked_signatures(map_id, removed_solar_system_ids) do
|
||||
removed_solar_system_ids
|
||||
|> Enum.map(fn solar_system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq_by(& &1.system_id)
|
||||
|> Enum.each(fn s ->
|
||||
try do
|
||||
{:ok, %{eve_id: eve_id, system: system}} = s |> Ash.load([:system])
|
||||
:ok = Ash.destroy!(s)
|
||||
# Group signatures by their source system for efficient broadcasting
|
||||
signatures_by_system =
|
||||
removed_solar_system_ids
|
||||
|> Enum.flat_map(fn solar_system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
|
||||
end)
|
||||
|> Enum.uniq_by(& &1.id)
|
||||
|> Enum.group_by(fn sig -> sig.system_id end)
|
||||
|
||||
# Handle case where parent system was already deleted
|
||||
case system do
|
||||
nil ->
|
||||
Logger.warning(
|
||||
"[cleanup_linked_signatures] signature #{eve_id} destroyed (parent system already deleted)"
|
||||
)
|
||||
signatures_by_system
|
||||
|> Enum.each(fn {_system_id, signatures} ->
|
||||
signatures
|
||||
|> Enum.each(fn sig ->
|
||||
try do
|
||||
{:ok, %{eve_id: eve_id, system: system}} = sig |> Ash.load([:system])
|
||||
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Logger.warning(
|
||||
"[cleanup_linked_signatures] for system #{solar_system_id}: #{inspect(eve_id)}"
|
||||
)
|
||||
# Clear the linked_system_id instead of destroying the signature
|
||||
case WandererApp.Api.MapSystemSignature.update_linked_system(sig, %{
|
||||
linked_system_id: nil
|
||||
}) do
|
||||
{:ok, _updated_sig} ->
|
||||
case system do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] signature #{eve_id} unlinked (parent system already deleted)"
|
||||
end)
|
||||
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] unlinked signature #{eve_id} in system #{solar_system_id}"
|
||||
end)
|
||||
|
||||
# Audit logging for cascade unlink (no user/character context)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_unlinked, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
signatures: [eve_id]
|
||||
})
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"[cleanup_linked_signatures] Failed to unlink signature #{sig.eve_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to cleanup linked signature: #{inspect(e)}")
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to cleanup linked signature: #{inspect(e)}")
|
||||
end)
|
||||
|
||||
# Broadcast once per source system after all its signatures are processed
|
||||
case List.first(signatures) do
|
||||
%{system: %{solar_system_id: solar_system_id}} ->
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
|
||||
_ ->
|
||||
# Try to get the system info if not preloaded
|
||||
case List.first(signatures) |> Ash.load([:system]) do
|
||||
{:ok, %{system: %{solar_system_id: solar_system_id}}} ->
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -446,8 +497,32 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end)
|
||||
end
|
||||
|
||||
def maybe_add_system(map_id, location, old_location, map_opts)
|
||||
def maybe_add_system(map_id, location, old_location, map_opts, scopes \\ nil)
|
||||
|
||||
def maybe_add_system(map_id, location, old_location, map_opts, scopes)
|
||||
when not is_nil(location) do
|
||||
alias WandererApp.Map.Server.ConnectionsImpl
|
||||
|
||||
# Check if the system matches the map's configured scopes before adding
|
||||
should_add =
|
||||
case scopes do
|
||||
nil -> true
|
||||
[] -> true
|
||||
scopes when is_list(scopes) ->
|
||||
ConnectionsImpl.can_add_location(scopes, location.solar_system_id)
|
||||
end
|
||||
|
||||
if should_add do
|
||||
do_add_system_from_location(map_id, location, old_location, map_opts)
|
||||
else
|
||||
# System filtered out by scope settings - this is expected behavior
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_add_system(_map_id, _location, _old_location, _map_opts, _scopes), do: :ok
|
||||
|
||||
defp do_add_system_from_location(map_id, location, old_location, map_opts) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :system_addition, :start],
|
||||
%{system_time: System.system_time()},
|
||||
@@ -526,12 +601,14 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|> case do
|
||||
{:ok, solar_system_info} ->
|
||||
# Use upsert instead of create - handles race conditions gracefully
|
||||
# visible: true ensures previously-deleted systems become visible again
|
||||
WandererApp.MapSystemRepo.upsert(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: location.solar_system_id,
|
||||
name: solar_system_info.solar_system_name,
|
||||
position_x: position.x,
|
||||
position_y: position.y
|
||||
position_y: position.y,
|
||||
visible: true
|
||||
})
|
||||
|> case do
|
||||
{:ok, system} ->
|
||||
@@ -653,8 +730,6 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_add_system(_map_id, _location, _old_location, _map_opts), do: :ok
|
||||
|
||||
defp do_add_system(
|
||||
map_id,
|
||||
%{
|
||||
@@ -679,7 +754,11 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|
||||
_ ->
|
||||
%{x: x, y: y} =
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts)
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(
|
||||
nil,
|
||||
rtree_name,
|
||||
map_opts
|
||||
)
|
||||
|
||||
%{"x" => x, "y" => y}
|
||||
end
|
||||
@@ -742,7 +821,10 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
})
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to get system static info for #{solar_system_id}: #{inspect(reason)}")
|
||||
Logger.error(
|
||||
"Failed to get system static info for #{solar_system_id}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, :system_info_not_found}
|
||||
end
|
||||
end
|
||||
@@ -775,7 +857,10 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
:ok
|
||||
|
||||
{:error, reason} = error ->
|
||||
Logger.error("Failed to add system #{solar_system_id} to map #{map_id}: #{inspect(reason)}")
|
||||
Logger.error(
|
||||
"Failed to add system #{solar_system_id} to map #{map_id}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
error
|
||||
end
|
||||
else
|
||||
@@ -863,10 +948,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
updated_system
|
||||
end
|
||||
|
||||
defp maybe_update_labels(system, _labels), do: system
|
||||
|
||||
defp maybe_update_labels(
|
||||
%{name: old_labels} = system,
|
||||
%{labels: old_labels} = system,
|
||||
labels
|
||||
)
|
||||
when not is_nil(labels) and old_labels != labels do
|
||||
@@ -980,12 +1063,16 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
# ADDITIVE: Also broadcast to external event system (webhooks/WebSocket)
|
||||
# This may fail if the relay is not available (e.g., in tests), which is fine
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :system_metadata_changed, %{
|
||||
system_id: updated_system.id,
|
||||
solar_system_id: updated_system.solar_system_id,
|
||||
name: updated_system.name,
|
||||
temporary_name: updated_system.temporary_name,
|
||||
labels: updated_system.labels,
|
||||
description: updated_system.description,
|
||||
status: updated_system.status
|
||||
status: updated_system.status,
|
||||
locked: updated_system.locked,
|
||||
position_x: updated_system.position_x,
|
||||
position_y: updated_system.position_y
|
||||
})
|
||||
|
||||
:ok
|
||||
|
||||
@@ -128,7 +128,7 @@ defmodule WandererApp.Maps do
|
||||
tracked: tracked
|
||||
}
|
||||
|
||||
defp get_map_characters(%{id: map_id} = map) do
|
||||
defp get_map_characters(%{id: map_id} = _map) do
|
||||
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|
||||
|> case do
|
||||
nil ->
|
||||
@@ -174,9 +174,11 @@ defmodule WandererApp.Maps do
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}
|
||||
|
||||
# Cache with 5 minute TTL so ACL changes are picked up even when map server isn't running
|
||||
WandererApp.Cache.insert(
|
||||
"map_characters-#{map_id}",
|
||||
map_characters
|
||||
map_characters,
|
||||
ttl: :timer.minutes(5)
|
||||
)
|
||||
|
||||
{:ok, map_characters}
|
||||
|
||||
@@ -99,7 +99,7 @@ defmodule WandererApp.MapConnectionRepo do
|
||||
def get_by_id(map_id, id) do
|
||||
# Use read_by_map action which doesn't have the FilterConnectionsByActorMap preparation
|
||||
# that was causing "filter being false" errors in tests
|
||||
import Ash.Query
|
||||
require Ash.Query
|
||||
|
||||
WandererApp.Api.MapConnection
|
||||
|> Ash.Query.for_read(:read_by_map, %{map_id: map_id})
|
||||
|
||||
@@ -38,6 +38,4 @@ defmodule WandererApp.MapPingsRepo do
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def destroy(_ping_id), do: :ok
|
||||
end
|
||||
|
||||
@@ -84,7 +84,7 @@ defmodule WandererApp.MapRepo do
|
||||
end
|
||||
end
|
||||
|
||||
error in Ash.Error.Query.NotFound ->
|
||||
_error in Ash.Error.Query.NotFound ->
|
||||
Logger.debug("Map not found with slug: #{slug}")
|
||||
{:error, :not_found}
|
||||
|
||||
|
||||
@@ -487,15 +487,6 @@ defmodule WandererApp.SecurityAudit do
|
||||
|
||||
# Private functions
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@doc false
|
||||
def do_store_audit_entry(audit_entry) do
|
||||
# Ensure event_type is properly formatted
|
||||
@@ -631,11 +622,6 @@ defmodule WandererApp.SecurityAudit do
|
||||
end
|
||||
end
|
||||
|
||||
defp async_enabled? do
|
||||
Application.get_env(:wanderer_app, __MODULE__, [])
|
||||
|> Keyword.get(:async, false)
|
||||
end
|
||||
|
||||
defp emit_telemetry_event(audit_entry) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :security_audit],
|
||||
|
||||
@@ -5,7 +5,11 @@ defmodule WandererApp.Test.Logger do
|
||||
"""
|
||||
|
||||
@callback info(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback info(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback error(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback error(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback warning(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback warning(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback debug(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback debug(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
end
|
||||
|
||||
@@ -9,12 +9,24 @@ defmodule WandererApp.Test.LoggerStub do
|
||||
@impl true
|
||||
def info(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def info(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def error(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def error(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def warning(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def warning(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def debug(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def debug(_message, _metadata), do: :ok
|
||||
end
|
||||
|
||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Vault do
|
||||
end)
|
||||
end
|
||||
|
||||
defp find_fallback_module_to_decrypt(config, ciphertext) do
|
||||
defp find_fallback_module_to_decrypt(config, _ciphertext) do
|
||||
Enum.find(config[:ciphers], fn {label, _} ->
|
||||
label == :fallback
|
||||
end)
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererAppWeb.ApiRouter do
|
||||
"""
|
||||
|
||||
use Phoenix.Router
|
||||
import WandererAppWeb.ApiRouterHelpers
|
||||
alias WandererAppWeb.{ApiRoutes, ApiRouter.RouteSpec}
|
||||
require Logger
|
||||
|
||||
@@ -171,7 +170,7 @@ defmodule WandererAppWeb.ApiRouter do
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp find_similar_routes(path_info, version) do
|
||||
defp find_similar_routes(path_info, _version) do
|
||||
# Find routes with similar paths in current or other versions
|
||||
all_routes = ApiRoutes.table()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule WandererAppWeb.ApiSpec do
|
||||
@behaviour OpenApiSpex.OpenApi
|
||||
|
||||
alias OpenApiSpex.{OpenApi, Info, Paths, Components, SecurityScheme, Server, Schema}
|
||||
alias OpenApiSpex.{OpenApi, Info, Paths, Components, SecurityScheme, Server}
|
||||
alias WandererAppWeb.{Endpoint, Router}
|
||||
alias WandererAppWeb.Schemas.ApiSchemas
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
"""
|
||||
attr(:type, :string, default: nil)
|
||||
attr(:class, :string, default: nil)
|
||||
attr(:data, :any, default: nil)
|
||||
attr(:rest, :global, include: ~w(disabled form name value))
|
||||
|
||||
slot(:inner_block, required: true)
|
||||
@@ -296,6 +297,7 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
"phx-submit-loading:opacity-75 p-button p-component p-button-outlined p-button-sm",
|
||||
@class
|
||||
]}
|
||||
data={@data}
|
||||
{@rest}
|
||||
>
|
||||
{render_slot(@inner_block)}
|
||||
@@ -614,7 +616,7 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
attr(:empty_label, :string, default: nil)
|
||||
attr(:rows, :list, required: true)
|
||||
attr(:row_id, :any, default: nil, doc: "the function for generating the row id")
|
||||
attr(:row_selected, :boolean, default: false, doc: "the function for generating the row id")
|
||||
attr(:row_selected, :any, default: false, doc: "the function for generating the row id")
|
||||
attr(:row_click, :any, default: nil, doc: "the function for handling phx-click on each row")
|
||||
|
||||
attr(:row_item, :any,
|
||||
@@ -703,13 +705,21 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:field, :any, required: true)
|
||||
attr(:placeholder, :string, default: nil)
|
||||
attr(:label, :string, default: nil)
|
||||
attr(:label_class, :string, default: nil)
|
||||
attr(:input_class, :string, default: nil)
|
||||
attr(:dropdown_extra_class, :string, default: nil)
|
||||
attr(:option_extra_class, :string, default: nil)
|
||||
attr(:mode, :atom, default: :single)
|
||||
attr(:options, :list, default: [])
|
||||
attr(:debounce, :integer, default: nil)
|
||||
attr(:update_min_len, :integer, default: nil)
|
||||
attr(:available_option_class, :string, default: nil)
|
||||
attr(:value_mapper, :any, default: nil)
|
||||
slot(:inner_block)
|
||||
slot(:option)
|
||||
|
||||
def live_select(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||
assigns =
|
||||
|
||||
@@ -433,6 +433,10 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
# Invalidate map_characters cache for all maps using this ACL
|
||||
# This ensures the tracking page shows updated members even when map server isn't running
|
||||
invalidate_map_characters_cache(acl_id)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
@@ -440,6 +444,23 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
)
|
||||
end
|
||||
|
||||
defp invalidate_map_characters_cache(acl_id) do
|
||||
case Ash.read(
|
||||
WandererApp.Api.MapAccessList
|
||||
|> Ash.Query.for_read(:read_by_acl, %{acl_id: acl_id})
|
||||
) do
|
||||
{:ok, map_acls} ->
|
||||
Enum.each(map_acls, fn %{map_id: map_id} ->
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
"Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp member_to_json(member) do
|
||||
base = %{
|
||||
|
||||
@@ -42,12 +42,18 @@ defmodule WandererAppWeb.AuthController do
|
||||
|
||||
WandererApp.Character.update_character(character.id, character_update)
|
||||
|
||||
# Update corporation/alliance data from ESI to ensure access control is current
|
||||
update_character_affiliation(character)
|
||||
|
||||
{:ok, character}
|
||||
|
||||
{:error, _error} ->
|
||||
{:ok, character} = WandererApp.Api.Character.create(character_data)
|
||||
:telemetry.execute([:wanderer_app, :user, :character, :registered], %{count: 1})
|
||||
|
||||
# Fetch initial corporation/alliance data for new characters
|
||||
update_character_affiliation(character)
|
||||
|
||||
{:ok, character}
|
||||
end
|
||||
|
||||
@@ -113,4 +119,102 @@ defmodule WandererAppWeb.AuthController do
|
||||
end
|
||||
|
||||
def maybe_update_character_user_id(_character, _user_id), do: :ok
|
||||
|
||||
# Updates character's corporation and alliance data from ESI.
|
||||
# This ensures ACL-based access control uses current corporation membership,
|
||||
# even for characters not actively being tracked on any map.
|
||||
defp update_character_affiliation(%{id: character_id, eve_id: eve_id} = character) do
|
||||
# Run async to not block the SSO callback
|
||||
Task.start(fn ->
|
||||
character_eve_id = eve_id |> String.to_integer()
|
||||
|
||||
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
|
||||
{:ok, [affiliation_info]} when is_map(affiliation_info) ->
|
||||
new_corporation_id = Map.get(affiliation_info, "corporation_id")
|
||||
new_alliance_id = Map.get(affiliation_info, "alliance_id")
|
||||
|
||||
# Check if corporation changed
|
||||
corporation_changed = character.corporation_id != new_corporation_id
|
||||
alliance_changed = character.alliance_id != new_alliance_id
|
||||
|
||||
if corporation_changed or alliance_changed do
|
||||
update_affiliation_data(character_id, character, new_corporation_id, new_alliance_id)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
"[AuthController] Failed to fetch affiliation for character #{character_id}: #{inspect(error)}"
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp update_character_affiliation(_character), do: :ok
|
||||
|
||||
defp update_affiliation_data(character_id, character, corporation_id, alliance_id) do
|
||||
# Fetch corporation info
|
||||
corporation_update =
|
||||
case WandererApp.Esi.get_corporation_info(corporation_id) do
|
||||
{:ok, %{"name" => corp_name, "ticker" => corp_ticker}} ->
|
||||
%{
|
||||
corporation_id: corporation_id,
|
||||
corporation_name: corp_name,
|
||||
corporation_ticker: corp_ticker
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{corporation_id: corporation_id}
|
||||
end
|
||||
|
||||
# Fetch alliance info if present
|
||||
alliance_update =
|
||||
case alliance_id do
|
||||
nil ->
|
||||
%{alliance_id: nil, alliance_name: nil, alliance_ticker: nil}
|
||||
|
||||
_ ->
|
||||
case WandererApp.Esi.get_alliance_info(alliance_id) do
|
||||
{:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} ->
|
||||
%{
|
||||
alliance_id: alliance_id,
|
||||
alliance_name: alliance_name,
|
||||
alliance_ticker: alliance_ticker
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{alliance_id: alliance_id}
|
||||
end
|
||||
end
|
||||
|
||||
full_update = Map.merge(corporation_update, alliance_update)
|
||||
|
||||
# Update database
|
||||
case character.corporation_id != corporation_id do
|
||||
true ->
|
||||
{:ok, _} = WandererApp.Api.Character.update_corporation(character, corporation_update)
|
||||
|
||||
false ->
|
||||
:ok
|
||||
end
|
||||
|
||||
case character.alliance_id != alliance_id do
|
||||
true ->
|
||||
{:ok, _} = WandererApp.Api.Character.update_alliance(character, alliance_update)
|
||||
|
||||
false ->
|
||||
:ok
|
||||
end
|
||||
|
||||
# Update cache
|
||||
WandererApp.Character.update_character(character_id, full_update)
|
||||
|
||||
Logger.info(
|
||||
"[AuthController] Updated affiliation for character #{character_id}: " <>
|
||||
"corp #{character.corporation_id} -> #{corporation_id}, " <>
|
||||
"alliance #{character.alliance_id} -> #{alliance_id}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -123,12 +123,6 @@ defmodule WandererAppWeb.LicenseApiController do
|
||||
end
|
||||
end
|
||||
|
||||
def update_validity(conn, %{"id" => _license_id}) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Missing required parameter: is_valid"})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a license's expiration date.
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
import Ash.Query, only: [filter: 2]
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Api.Character
|
||||
alias WandererApp.MapSystemRepo
|
||||
alias WandererApp.MapCharacterSettingsRepo
|
||||
alias WandererApp.MapConnectionRepo
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
alias WandererAppWeb.Schemas.{ApiSchemas, ResponseSchemas}
|
||||
@@ -16,7 +14,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
# V1 API Actions (for compatibility with versioned API router)
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def index_v1(conn, params) do
|
||||
def index_v1(conn, _params) do
|
||||
# Delegate to the existing list implementation or create a basic one
|
||||
json(conn, %{
|
||||
data: [],
|
||||
@@ -43,7 +41,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def create_v1(conn, params) do
|
||||
def create_v1(conn, _params) do
|
||||
# Basic create implementation for testing
|
||||
json(conn, %{
|
||||
data: %{
|
||||
@@ -59,7 +57,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def update_v1(conn, %{"id" => id} = params) do
|
||||
def update_v1(conn, %{"id" => id} = _params) do
|
||||
# Basic update implementation for testing
|
||||
json(conn, %{
|
||||
data: %{
|
||||
@@ -82,7 +80,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
|> text("")
|
||||
end
|
||||
|
||||
def duplicate_v1(conn, %{"id" => id} = params) do
|
||||
def duplicate_v1(conn, %{"id" => id} = _params) do
|
||||
# Basic duplicate implementation for testing
|
||||
json(conn, %{
|
||||
data: %{
|
||||
@@ -99,7 +97,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def bulk_create_v1(conn, params) do
|
||||
def bulk_create_v1(conn, _params) do
|
||||
# Basic bulk create implementation for testing
|
||||
json(conn, %{
|
||||
data: [
|
||||
@@ -121,7 +119,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def bulk_update_v1(conn, params) do
|
||||
def bulk_update_v1(conn, _params) do
|
||||
# Basic bulk update implementation for testing
|
||||
json(conn, %{
|
||||
data: [
|
||||
@@ -325,13 +323,6 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
# Helper functions for the API controller
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
defp get_map_id_by_slug(slug) do
|
||||
case WandererApp.Api.Map.get_map_by_slug(slug) do
|
||||
{:ok, map} -> {:ok, map.id}
|
||||
{:error, error} -> {:error, "Map not found for slug: #{slug}, error: #{inspect(error)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_map_identifier(params) do
|
||||
case Map.get(params, "map_identifier") do
|
||||
nil ->
|
||||
|
||||
@@ -4,8 +4,7 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Api
|
||||
|
||||
alias WandererAppWeb.UserActivityItem
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
@@ -155,10 +154,10 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
|
||||
result
|
||||
|> Map.put(:character, WandererAppWeb.MapEventHandler.map_ui_character_stat(character))
|
||||
|> Map.put(:event_name, WandererAppWeb.UserActivity.get_event_name(event_type))
|
||||
|> Map.put(:event_name, WandererAppWeb.UserActivityItem.get_event_name(event_type))
|
||||
|> Map.put(
|
||||
:event_data,
|
||||
WandererAppWeb.UserActivity.get_event_data(
|
||||
WandererAppWeb.UserActivityItem.get_event_data(
|
||||
event_type,
|
||||
Jason.decode!(event_data) |> Map.drop(["character_id"])
|
||||
)
|
||||
|
||||
@@ -65,24 +65,6 @@ defmodule WandererAppWeb.MapEventsAPIController do
|
||||
items: @event_schema
|
||||
})
|
||||
|
||||
@events_list_params %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
since: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
format: :date_time,
|
||||
description: "Return events after this timestamp (ISO8601)"
|
||||
},
|
||||
limit: %OpenApiSpex.Schema{
|
||||
type: :integer,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 100,
|
||||
description: "Maximum number of events to return"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# OpenApiSpex Operations
|
||||
# -----------------------------------------------------------------
|
||||
@@ -173,7 +155,7 @@ defmodule WandererAppWeb.MapEventsAPIController do
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Invalid 'limit' parameter. Must be between 1 and 100."})
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Internal server error"})
|
||||
@@ -184,7 +166,7 @@ defmodule WandererAppWeb.MapEventsAPIController do
|
||||
# Private Functions
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
defp get_map(conn, map_identifier) do
|
||||
defp get_map(conn, _map_identifier) do
|
||||
# The map should already be loaded by the CheckMapApiKey plug
|
||||
case conn.assigns[:map] do
|
||||
nil -> {:error, :map_not_found}
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule WandererAppWeb.Plugs.JsonApiPerformanceMonitor do
|
||||
conn
|
||||
|> register_before_send(fn conn ->
|
||||
end_time = System.monotonic_time(:millisecond)
|
||||
duration = end_time - start_time
|
||||
_duration = end_time - start_time
|
||||
|
||||
# Extract response metadata
|
||||
response_metadata = extract_response_metadata(conn, request_metadata)
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererAppWeb.Plugs.LicenseAuth do
|
||||
require Logger
|
||||
|
||||
alias WandererApp.License.LicenseManager
|
||||
alias WandererApp.Helpers.Config
|
||||
|
||||
@doc """
|
||||
Authenticates requests using the LM_AUTH_KEY.
|
||||
@@ -21,7 +20,7 @@ defmodule WandererAppWeb.Plugs.LicenseAuth do
|
||||
"""
|
||||
def authenticate_lm(conn, _opts) do
|
||||
auth_header = get_req_header(conn, "authorization")
|
||||
lm_auth_key = Config.get_env(:wanderer_app, :lm_auth_key)
|
||||
lm_auth_key = Application.get_env(:wanderer_app, :lm_auth_key)
|
||||
|
||||
case auth_header do
|
||||
["Bearer " <> token] ->
|
||||
|
||||
@@ -37,7 +37,7 @@ defmodule WandererAppWeb.UserAuth do
|
||||
nil ->
|
||||
{:halt, redirect_require_login(socket)}
|
||||
|
||||
%User{characters: characters} ->
|
||||
%User{characters: _characters} ->
|
||||
{:cont, new_socket}
|
||||
end
|
||||
|
||||
@@ -112,13 +112,6 @@ defmodule WandererAppWeb.UserAuth do
|
||||
|> LiveView.redirect(to: ~p"/")
|
||||
end
|
||||
|
||||
defp track_characters([]), do: :ok
|
||||
|
||||
defp track_characters([%{id: character_id} | characters]) do
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
track_characters(characters)
|
||||
end
|
||||
|
||||
defp maybe_store_return_to(%{method: "GET"} = conn) do
|
||||
%{request_path: request_path, query_string: query_string} = conn
|
||||
return_to = if query_string == "", do: request_path, else: request_path <> "?" <> query_string
|
||||
|
||||
@@ -2,8 +2,11 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
use WandererAppWeb, :live_view
|
||||
|
||||
alias WandererApp.ExternalEvents.AclEventBroadcaster
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
@members_per_page 50
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do
|
||||
{:ok, characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
@@ -24,7 +27,9 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
user_id: user_id,
|
||||
access_lists: access_lists |> Enum.map(fn acl -> map_ui_acl(acl, nil) end),
|
||||
characters: characters,
|
||||
members: []
|
||||
members: [],
|
||||
members_page: 1,
|
||||
members_per_page: @members_per_page
|
||||
)}
|
||||
end
|
||||
|
||||
@@ -38,7 +43,9 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
allow_acl_creation: false,
|
||||
access_lists: [],
|
||||
characters: [],
|
||||
members: []
|
||||
members: [],
|
||||
members_page: 1,
|
||||
members_per_page: @members_per_page
|
||||
)}
|
||||
end
|
||||
|
||||
@@ -92,10 +99,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> assign(:page_title, "Access Lists - Members")
|
||||
|> assign(:selected_acl_id, acl_id)
|
||||
|> assign(:access_list, access_list)
|
||||
|> assign(
|
||||
:members,
|
||||
members
|
||||
)
|
||||
|> assign(:members, members)
|
||||
|> assign(:members_page, 1)
|
||||
else
|
||||
_ ->
|
||||
socket
|
||||
@@ -281,11 +286,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> Enum.find(&(&1.id == member_id))
|
||||
|> WandererApp.Api.AccessListMember.destroy!()
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{socket.assigns.selected_acl_id}",
|
||||
{:acl_updated, %{acl_id: socket.assigns.selected_acl_id}}
|
||||
)
|
||||
broadcast_acl_updated(socket.assigns.selected_acl_id)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -327,6 +328,20 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("members_prev_page", _, socket) do
|
||||
new_page = max(1, socket.assigns.members_page - 1)
|
||||
{:noreply, assign(socket, :members_page, new_page)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("members_next_page", _, socket) do
|
||||
total_members = length(socket.assigns.members)
|
||||
max_page = max(1, ceil(total_members / socket.assigns.members_per_page))
|
||||
new_page = min(max_page, socket.assigns.members_page + 1)
|
||||
{:noreply, assign(socket, :members_page, new_page)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("noop", _, socket) do
|
||||
{:noreply, socket}
|
||||
@@ -444,11 +459,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :update], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{socket.assigns.selected_acl_id}",
|
||||
{:acl_updated, %{acl_id: socket.assigns.selected_acl_id}}
|
||||
)
|
||||
broadcast_acl_updated(socket.assigns.selected_acl_id)
|
||||
|
||||
socket
|
||||
|> assign(
|
||||
@@ -574,11 +585,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{access_list_id}",
|
||||
{:acl_updated, %{acl_id: access_list_id}}
|
||||
)
|
||||
broadcast_acl_updated(access_list_id)
|
||||
|
||||
{:ok, member}
|
||||
|
||||
@@ -613,11 +620,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{access_list_id}",
|
||||
{:acl_updated, %{acl_id: access_list_id}}
|
||||
)
|
||||
broadcast_acl_updated(access_list_id)
|
||||
|
||||
{:ok, member}
|
||||
|
||||
@@ -653,11 +656,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{access_list_id}",
|
||||
{:acl_updated, %{acl_id: access_list_id}}
|
||||
)
|
||||
broadcast_acl_updated(access_list_id)
|
||||
|
||||
{:ok, member}
|
||||
|
||||
@@ -688,7 +687,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
"""
|
||||
end
|
||||
|
||||
slot(:option)
|
||||
attr(:option, :any, required: true)
|
||||
|
||||
def search_member_item(assigns) do
|
||||
~H"""
|
||||
@@ -737,4 +736,44 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
defp map_ui_acl(acl, selected_id) do
|
||||
acl |> Map.put(:selected, acl.id == selected_id)
|
||||
end
|
||||
|
||||
defp paginated_members(members, page, per_page) do
|
||||
members
|
||||
|> Enum.sort_by(&{&1.role, &1.name}, &<=/2)
|
||||
|> Enum.drop((page - 1) * per_page)
|
||||
|> Enum.take(per_page)
|
||||
end
|
||||
|
||||
defp total_pages(members, per_page) do
|
||||
max(1, ceil(length(members) / per_page))
|
||||
end
|
||||
|
||||
# Broadcast ACL update and invalidate map_characters cache for all maps using this ACL
|
||||
# This ensures the tracking page shows updated members even when map server isn't running
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
invalidate_map_characters_cache(acl_id)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
{:acl_updated, %{acl_id: acl_id}}
|
||||
)
|
||||
end
|
||||
|
||||
defp invalidate_map_characters_cache(acl_id) do
|
||||
case Ash.read(
|
||||
WandererApp.Api.MapAccessList
|
||||
|> Ash.Query.for_read(:read_by_acl, %{acl_id: acl_id})
|
||||
) do
|
||||
{:ok, map_acls} ->
|
||||
Enum.each(map_acls, fn %{map_id: map_id} ->
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
"Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,11 +82,14 @@
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="dropzone droppable draggable-dropzone--occupied flex flex-col gap-1 w-full rounded-none h-[calc(100vh-211px)] !overflow-y-auto"
|
||||
class={[
|
||||
"dropzone droppable draggable-dropzone--occupied flex flex-col gap-1 w-full rounded-none h-[calc(100vh-191px)] !overflow-y-auto",
|
||||
classes("!h-[calc(100vh-240px)]": length(@members) > @members_per_page)
|
||||
]}
|
||||
id="acl_members"
|
||||
>
|
||||
<div
|
||||
:for={member <- @members |> Enum.sort_by(&{&1.role, &1.name}, &<=/2)}
|
||||
:for={member <- paginated_members(@members, @members_page, @members_per_page)}
|
||||
draggable="true"
|
||||
id={member.id}
|
||||
class="draggable !p-1 h-10 cursor-move bg-black bg-opacity-25 hover:text-white"
|
||||
@@ -113,15 +116,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div :if={length(@members) > @members_per_page} class="flex items-center justify-between px-3 py-2 border-t border-gray-500 bg-black bg-opacity-25">
|
||||
<span class="text-sm text-gray-400">
|
||||
Page {@members_page} of {total_pages(@members, @members_per_page)} ({length(@members)} members)
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
phx-click="members_prev_page"
|
||||
disabled={@members_page <= 1}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page <= 1, do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-left" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="members_next_page"
|
||||
disabled={@members_page >= total_pages(@members, @members_per_page)}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page >= total_pages(@members, @members_per_page), do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-right" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.link
|
||||
disabled={@selected_acl_id == "" or not can_add_members?(@access_list, @current_user)}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none"
|
||||
:if={@selected_acl_id != "" and can_add_members?(@access_list, @current_user)}
|
||||
class="btn w-full btn-neutral rounded-none"
|
||||
patch={~p"/access-lists/#{@selected_acl_id}/add-members"}
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</.link>
|
||||
<div
|
||||
:if={@selected_acl_id == "" or not can_add_members?(@access_list, @current_user)}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none btn-disabled"
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -146,10 +179,10 @@
|
||||
placeholder="Select an owner"
|
||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||
/>
|
||||
|
||||
|
||||
<!-- Divider between above inputs and the API key section -->
|
||||
<hr class="my-4 border-gray-600" />
|
||||
|
||||
|
||||
<!-- API Key Section with grid layout -->
|
||||
<div class="mt-2">
|
||||
<label class="block text-sm font-medium text-gray-200 mb-1">ACL API key</label>
|
||||
|
||||
@@ -4,8 +4,6 @@ defmodule WandererAppWeb.AdminLive do
|
||||
require Logger
|
||||
alias BetterNumber, as: Number
|
||||
|
||||
@invite_link_ttl :timer.hours(24)
|
||||
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket)
|
||||
when not is_nil(user_id) do
|
||||
WandererApp.StartCorpWalletTrackerTask.maybe_start_corp_wallet_tracker(
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
rows={@transactions}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={transaction}>
|
||||
<:col :let={_transaction}>
|
||||
<div class=" text-22">
|
||||
<.icon name="hero-credit-card-solid" class="h-5 w-5" />
|
||||
</div>
|
||||
@@ -267,7 +267,7 @@
|
||||
rows={@active_map_subscriptions}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={subscription}>
|
||||
<:col :let={_subscription}>
|
||||
<div class=" text-22">
|
||||
<.icon name="hero-check-badge-solid" class="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Character.TrackingUtils
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, maps} = WandererApp.Maps.get_available_maps(socket.assigns.current_user)
|
||||
@@ -18,22 +20,19 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(characters: [], selected_map: nil, maps: [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
# Unsubscribe from previous map if any
|
||||
socket = maybe_unsubscribe_from_map(socket)
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :characters_tracking)
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
|> assign(selected_map: nil, selected_map_slug: nil)
|
||||
end
|
||||
|
||||
defp apply_action(
|
||||
@@ -43,6 +42,10 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
) do
|
||||
selected_map = maps |> Enum.find(&(&1.slug == map_slug))
|
||||
|
||||
# Unsubscribe from previous map and subscribe to new one
|
||||
socket = maybe_unsubscribe_from_map(socket)
|
||||
socket = maybe_subscribe_to_map(socket, selected_map)
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :characters_tracking)
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
@@ -55,6 +58,27 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
end)
|
||||
end
|
||||
|
||||
# Subscribe to map PubSub channel to receive ACL update notifications
|
||||
defp maybe_subscribe_to_map(socket, nil), do: socket
|
||||
|
||||
defp maybe_subscribe_to_map(socket, %{id: map_id}) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id)
|
||||
end
|
||||
|
||||
socket
|
||||
end
|
||||
|
||||
# Unsubscribe from previous map's PubSub channel
|
||||
defp maybe_unsubscribe_from_map(%{assigns: %{selected_map: nil}} = socket), do: socket
|
||||
|
||||
defp maybe_unsubscribe_from_map(%{assigns: %{selected_map: %{id: map_id}}} = socket) do
|
||||
Phoenix.PubSub.unsubscribe(WandererApp.PubSub, map_id)
|
||||
socket
|
||||
end
|
||||
|
||||
defp maybe_unsubscribe_from_map(socket), do: socket
|
||||
|
||||
@impl true
|
||||
def handle_event("select_map_" <> map_slug, _, socket) do
|
||||
{:noreply,
|
||||
@@ -77,21 +101,30 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
case characters |> Enum.find(&(&1.id == character_id)) do
|
||||
%{tracked: false} ->
|
||||
WandererApp.MapCharacterSettingsRepo.track(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
%{tracked: current_tracked, eve_id: eve_id} ->
|
||||
# Use TrackingUtils.update_tracking to properly set/unset the tracking_start_time
|
||||
# cache key, which is required for the character to appear in get_tracked_character_ids
|
||||
case TrackingUtils.update_tracking(
|
||||
selected_map.id,
|
||||
eve_id,
|
||||
current_user.id,
|
||||
not current_tracked,
|
||||
self(),
|
||||
false
|
||||
) do
|
||||
{:ok, _tracking_data, _event} ->
|
||||
:ok
|
||||
|
||||
%{tracked: true} ->
|
||||
WandererApp.MapCharacterSettingsRepo.untrack(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
{:error, reason} ->
|
||||
Logger.error(
|
||||
"Failed to toggle tracking for character #{character_id} on map #{selected_map.id}: #{inspect(reason)}"
|
||||
)
|
||||
end
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(selected_map.id, [
|
||||
character_id
|
||||
])
|
||||
nil ->
|
||||
Logger.warning(
|
||||
"Character #{character_id} not found in available characters for map #{selected_map.id}"
|
||||
)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
@@ -111,6 +144,20 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Handle ACL members changed event - reload characters list
|
||||
@impl true
|
||||
def handle_info(
|
||||
%{event: :acl_members_changed},
|
||||
%{assigns: %{selected_map: selected_map, current_user: current_user}} = socket
|
||||
)
|
||||
when not is_nil(selected_map) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_async(:characters, fn ->
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
end)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_event, socket), do: {:noreply, socket}
|
||||
end
|
||||
|
||||
@@ -79,7 +79,7 @@ defmodule WandererAppWeb.MapSubscription do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp get_title(%{plan: plan, auto_renew?: auto_renew?, active_till: active_till} = subscription) do
|
||||
defp get_title(%{plan: plan, auto_renew?: auto_renew?, active_till: active_till}) do
|
||||
if plan != :alpha do
|
||||
"Active subscription: omega \nActive till: #{Calendar.strftime(active_till, "%m/%d/%Y")} \nAuto renew: #{auto_renew?}"
|
||||
else
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule WandererAppWeb.UserActivity do
|
||||
defmodule WandererAppWeb.UserActivityItem do
|
||||
use WandererAppWeb, :live_component
|
||||
use LiveViewEvents
|
||||
|
||||
@@ -300,13 +300,13 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
%{"character_eve_id" => character_eve_id},
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: %{id: current_user_id}
|
||||
map_id: _map_id,
|
||||
current_user: %{id: _current_user_id}
|
||||
}
|
||||
} = socket
|
||||
)
|
||||
when not is_nil(character_eve_id) do
|
||||
{:ok, character} = WandererApp.Character.get_by_eve_id("#{character_eve_id}")
|
||||
{:ok, _character} = WandererApp.Character.get_by_eve_id("#{character_eve_id}")
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
@@ -338,12 +338,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
station_id: character.station_id
|
||||
}
|
||||
|
||||
defp get_map_with_acls(map_id) do
|
||||
with {:ok, map} <- WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, Ash.load!(map, :acls)}
|
||||
end
|
||||
end
|
||||
|
||||
def needs_tracking_setup?(
|
||||
only_tracked_characters,
|
||||
characters,
|
||||
|
||||
@@ -120,10 +120,16 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
{:ok, signatures} =
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id(solar_system_target_id)
|
||||
|
||||
signatures
|
||||
|> Enum.filter(fn s ->
|
||||
s.system_id == source_system.id
|
||||
end)
|
||||
filtered_signatures =
|
||||
signatures
|
||||
|> Enum.filter(fn s ->
|
||||
s.system_id == source_system.id
|
||||
end)
|
||||
|
||||
# Collect eve_ids for audit logging
|
||||
deleted_eve_ids = Enum.map(filtered_signatures, & &1.eve_id)
|
||||
|
||||
filtered_signatures
|
||||
|> Enum.each(fn s ->
|
||||
if not is_nil(s.temporary_name) && s.temporary_name == target_system.temporary_name do
|
||||
map_id
|
||||
@@ -143,6 +149,17 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
|> WandererApp.Api.MapSystemSignature.destroy!()
|
||||
end)
|
||||
|
||||
# Audit log signatures deleted with connection
|
||||
if deleted_eve_ids != [] do
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_removed, %{
|
||||
character_id: main_character_id,
|
||||
user_id: current_user_id,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_source_id,
|
||||
signatures: deleted_eve_ids
|
||||
})
|
||||
end
|
||||
|
||||
WandererApp.Map.Server.Impl.broadcast!(
|
||||
map_id,
|
||||
:signatures_updated,
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule WandererAppWeb.MapKillsEventHandler do
|
||||
|
||||
def handle_server_event(
|
||||
%{event: :init_kills},
|
||||
%{assigns: %{map_id: map_id} = assigns} = socket
|
||||
%{assigns: %{map_id: map_id} = _assigns} = socket
|
||||
) do
|
||||
# Get kill counts from cache
|
||||
case WandererApp.Map.get_map(map_id) do
|
||||
|
||||
@@ -3,7 +3,7 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
use Phoenix.Component
|
||||
require Logger
|
||||
|
||||
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler, MapSystemsEventHandler}
|
||||
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler}
|
||||
|
||||
def handle_server_event(
|
||||
%{
|
||||
|
||||
@@ -168,7 +168,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
current_user: %{id: current_user_id},
|
||||
map_id: map_id,
|
||||
main_character_id: main_character_id,
|
||||
map_user_settings: map_user_settings,
|
||||
map_user_settings: _map_user_settings,
|
||||
user_permissions: %{update_system: true}
|
||||
} = assigns
|
||||
} = socket
|
||||
@@ -380,7 +380,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
|
||||
def handle_ui_event(
|
||||
"undo_delete_signatures",
|
||||
%{"system_id" => solar_system_id, "eve_ids" => eve_ids} = payload,
|
||||
%{"system_id" => solar_system_id, "eve_ids" => eve_ids} = _payload,
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
|
||||
@@ -97,7 +97,7 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
%{"solarSystemId" => solar_system_id} = _event,
|
||||
%{
|
||||
assigns: %{
|
||||
current_user: current_user,
|
||||
current_user: _current_user,
|
||||
has_tracked_characters?: true,
|
||||
map_id: map_id,
|
||||
user_permissions: %{add_system: true}
|
||||
@@ -109,7 +109,7 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
|> case do
|
||||
%{id: system_id} = system when not is_nil(system_id) ->
|
||||
%{id: system_id} = _system when not is_nil(system_id) ->
|
||||
{:ok, comments} = WandererApp.MapSystemCommentRepo.get_by_system(system_id)
|
||||
|
||||
{:reply,
|
||||
|
||||
@@ -3,10 +3,6 @@ defmodule WandererAppWeb.MapAuditLive do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererAppWeb.UserActivity
|
||||
|
||||
@active_subscription_periods ["2M", "3M"]
|
||||
|
||||
def mount(
|
||||
%{"slug" => map_slug, "period" => period, "activity" => activity} = _params,
|
||||
_session,
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
/>
|
||||
</div>
|
||||
<.live_component
|
||||
module={UserActivity}
|
||||
module={WandererAppWeb.UserActivityItem}
|
||||
id="user-activity"
|
||||
notify_to={self()}
|
||||
can_undo_types={@can_undo_types}
|
||||
|
||||
@@ -157,7 +157,7 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
|> assign(:groups, groups)
|
||||
end
|
||||
|
||||
defp map_ui_character(map_id, character) do
|
||||
defp map_ui_character(_map_id, character) do
|
||||
character
|
||||
|> Map.take([
|
||||
:id,
|
||||
|
||||
@@ -230,6 +230,7 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
def handle_event(socket, {:DOWN, ref, :process, _pid, reason}) when is_reference(ref) do
|
||||
# Task failed, log the error and update the client
|
||||
Logger.error("Task failed: #{inspect(reason)}")
|
||||
socket
|
||||
end
|
||||
|
||||
def handle_event(socket, event),
|
||||
|
||||
@@ -112,13 +112,6 @@ defmodule WandererAppWeb.MapLive do
|
||||
|> WandererAppWeb.MapEventHandler.handle_event(info)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(info, socket),
|
||||
do:
|
||||
{:noreply,
|
||||
socket
|
||||
|> WandererAppWeb.MapEventHandler.handle_event(info)}
|
||||
|
||||
@impl true
|
||||
def handle_event("change_subscription_tab", %{"tab" => tab}, socket),
|
||||
do: {:noreply, socket |> assign(active_subscription_tab: tab)}
|
||||
|
||||
@@ -5,7 +5,6 @@ defmodule WandererAppWeb.Maps.MapBalanceComponent do
|
||||
require Logger
|
||||
|
||||
alias BetterNumber, as: Number
|
||||
alias WandererApp.License.LicenseManager
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
@@ -99,7 +98,7 @@ defmodule WandererAppWeb.Maps.MapBalanceComponent do
|
||||
type: :in
|
||||
})
|
||||
|
||||
{:ok, user} =
|
||||
{:ok, _user} =
|
||||
user
|
||||
|> WandererApp.Api.User.update_balance(%{
|
||||
balance: (user_balance || 0.0) - amount
|
||||
|
||||
@@ -77,7 +77,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
|> assign(:active_page, :maps)
|
||||
|> assign(:uri, URI.parse(url) |> Map.put(:path, ~p"/"))
|
||||
|> assign(:page_title, "Maps - Create")
|
||||
|> assign(:scopes, ["wormholes", "stargates", "none", "all"])
|
||||
|> assign(:available_scopes, available_scopes())
|
||||
|> assign(
|
||||
:form,
|
||||
AshPhoenix.Form.for_create(WandererApp.Api.Map, :new,
|
||||
@@ -86,7 +86,8 @@ defmodule WandererAppWeb.MapsLive do
|
||||
],
|
||||
prepare_source: fn form ->
|
||||
form
|
||||
|> Map.put("scope", "wormholes")
|
||||
# Default to wormholes scope for new maps
|
||||
|> Map.put("scopes", [:wormholes])
|
||||
end
|
||||
)
|
||||
)
|
||||
@@ -115,6 +116,9 @@ defmodule WandererAppWeb.MapsLive do
|
||||
_ -> map |> map_map()
|
||||
end
|
||||
|
||||
# Auto-initialize scopes from legacy scope if scopes is empty/nil
|
||||
map = maybe_initialize_scopes_from_legacy(map)
|
||||
|
||||
# Add owner to characters list, filtering out nil values
|
||||
characters =
|
||||
[map.owner |> map_character() | socket.assigns.characters]
|
||||
@@ -125,7 +129,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
|> assign(:active_page, :maps)
|
||||
|> assign(:uri, URI.parse(url) |> Map.put(:path, ~p"/"))
|
||||
|> assign(:page_title, "Maps - Edit")
|
||||
|> assign(:scopes, ["wormholes", "stargates", "none", "all"])
|
||||
|> assign(:available_scopes, available_scopes())
|
||||
|> assign(:map_slug, map_slug)
|
||||
|> assign(:characters, characters)
|
||||
|> assign(
|
||||
@@ -215,13 +219,6 @@ defmodule WandererAppWeb.MapsLive do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("set-default-scope", %{"id" => id}, socket) do
|
||||
send_update(LiveSelect.Component, options: ["wormholes", "stargates", "none", "all"], id: id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("generate-map-api-key", _params, socket) do
|
||||
new_api_key = UUID.uuid4()
|
||||
|
||||
@@ -257,27 +254,25 @@ defmodule WandererAppWeb.MapsLive do
|
||||
@impl true
|
||||
def handle_event(
|
||||
"live_select_change",
|
||||
%{"id" => id, "text" => text} = _change_event,
|
||||
%{"id" => id, "text" => _text} = _change_event,
|
||||
socket
|
||||
) do
|
||||
options =
|
||||
if text == "" do
|
||||
socket.assigns.scopes
|
||||
else
|
||||
socket.assigns.scopes
|
||||
end
|
||||
|
||||
send_update(LiveSelect.Component, options: options, id: id)
|
||||
# This handler is for ACL live_select component
|
||||
send_update(LiveSelect.Component, options: socket.assigns.acls, id: id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"form" => form} = _params, socket) do
|
||||
# Process scopes from checkbox form data
|
||||
scopes = parse_scopes_from_form(form)
|
||||
|
||||
form =
|
||||
AshPhoenix.Form.validate(
|
||||
socket.assigns.form,
|
||||
form
|
||||
|> Map.put("acls", form["acls"] || [])
|
||||
|> Map.put("scopes", scopes)
|
||||
|> Map.put(
|
||||
"only_tracked_characters",
|
||||
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
|
||||
@@ -293,15 +288,10 @@ defmodule WandererAppWeb.MapsLive do
|
||||
%{assigns: %{current_user: current_user}} = socket
|
||||
)
|
||||
when not is_nil(current_user) do
|
||||
scope =
|
||||
form
|
||||
|> Map.get("scope")
|
||||
|> case do
|
||||
"" -> "wormholes"
|
||||
scope -> scope
|
||||
end
|
||||
# Process scopes from checkbox form data
|
||||
scopes = parse_scopes_from_form(form)
|
||||
|
||||
form = form |> Map.put("scope", scope)
|
||||
form = form |> Map.put("scopes", scopes)
|
||||
|
||||
case WandererApp.Api.Map.new(form) do
|
||||
{:ok, new_map} ->
|
||||
@@ -426,18 +416,13 @@ defmodule WandererAppWeb.MapsLive do
|
||||
# Successfully found the map, proceed with loading and updating
|
||||
{:ok, map_with_acls} = Ash.load(map, :acls)
|
||||
|
||||
scope =
|
||||
form
|
||||
|> Map.get("scope")
|
||||
|> case do
|
||||
"" -> "wormholes"
|
||||
scope -> scope
|
||||
end
|
||||
# Process scopes from checkbox form data
|
||||
scopes = parse_scopes_from_form(form)
|
||||
|
||||
form =
|
||||
form
|
||||
|> Map.put("acls", form["acls"] || [])
|
||||
|> Map.put("scope", scope)
|
||||
|> Map.put("scopes", scopes)
|
||||
|> Map.put(
|
||||
"only_tracked_characters",
|
||||
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
|
||||
@@ -820,4 +805,74 @@ defmodule WandererAppWeb.MapsLive do
|
||||
map
|
||||
|> Map.put(:acls, acls |> Enum.map(&map_acl/1))
|
||||
end
|
||||
|
||||
defp available_scopes do
|
||||
[
|
||||
%{value: "wormholes", label: "Wormholes", description: "J-space systems"},
|
||||
%{value: "hi", label: "High-Sec", description: "Security 0.5 - 1.0"},
|
||||
%{value: "low", label: "Low-Sec", description: "Security 0.1 - 0.4"},
|
||||
%{value: "null", label: "Null-Sec", description: "Security 0.0 and below"},
|
||||
%{value: "pochven", label: "Pochven", description: "Triglavian space"}
|
||||
]
|
||||
end
|
||||
|
||||
# Auto-initialize scopes from legacy scope setting if scopes is empty/nil
|
||||
defp maybe_initialize_scopes_from_legacy(%{scopes: scopes} = map)
|
||||
when is_list(scopes) and scopes != [] do
|
||||
# Scopes already set, don't override
|
||||
map
|
||||
end
|
||||
|
||||
defp maybe_initialize_scopes_from_legacy(%{scope: scope} = map) do
|
||||
# Convert legacy scope to new scopes format
|
||||
scopes = legacy_scope_to_scopes(scope)
|
||||
Map.put(map, :scopes, scopes)
|
||||
end
|
||||
|
||||
defp maybe_initialize_scopes_from_legacy(map) do
|
||||
# No scope field, default to wormholes
|
||||
Map.put(map, :scopes, [:wormholes])
|
||||
end
|
||||
|
||||
# Convert legacy scope atom to new scopes list
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
defp parse_scopes_from_form(form) do
|
||||
# Extract selected scopes from form data
|
||||
# Form sends scopes as "scopes" => %{"wormholes" => "true", "hi" => "true", ...}
|
||||
form
|
||||
|> Map.get("scopes", %{})
|
||||
|> case do
|
||||
scopes when is_map(scopes) ->
|
||||
scopes
|
||||
|> Enum.filter(fn {_key, value} -> value == "true" end)
|
||||
|> Enum.map(fn {key, _value} -> String.to_existing_atom(key) end)
|
||||
|
||||
scopes when is_list(scopes) ->
|
||||
# Already a list of atoms/strings
|
||||
scopes
|
||||
|> Enum.map(fn
|
||||
scope when is_atom(scope) -> scope
|
||||
scope when is_binary(scope) -> String.to_existing_atom(scope)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to get current scopes from form for checkbox state
|
||||
def get_current_scopes(form) do
|
||||
scopes = Phoenix.HTML.Form.input_value(form, :scopes) || []
|
||||
|
||||
scopes
|
||||
|> Enum.map(fn
|
||||
scope when is_atom(scope) -> Atom.to_string(scope)
|
||||
scope when is_binary(scope) -> scope
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -151,15 +151,66 @@
|
||||
placeholder="Select a map owner"
|
||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||
/>
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:scope]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
wrapper_class="mt-2"
|
||||
label="Map scope"
|
||||
placeholder="Select a map scope"
|
||||
options={Enum.map(@scopes, fn scope -> {scope, scope} end)}
|
||||
/>
|
||||
<!-- Map Scopes Section -->
|
||||
<div class="mt-2 border border-dashed border-stone-600 rounded p-3">
|
||||
<p class="text-xs text-stone-400 mb-2">
|
||||
Select which space types to automatically track on the map
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<%= for scope_option <- @available_scopes do %>
|
||||
<% is_checked = scope_option.value in (get_current_scopes(f) || []) %>
|
||||
<label class="flex items-center gap-2 cursor-pointer py-1 px-2 rounded hover:bg-stone-800">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class={[
|
||||
"checkboxRoot sizeM p-checkbox p-component",
|
||||
if(is_checked, do: "p-highlight", else: "")
|
||||
]}
|
||||
data-p-highlight={is_checked}
|
||||
data-p-disabled="false"
|
||||
data-pc-name="checkbox"
|
||||
data-pc-section="root"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={"form[scopes][#{scope_option.value}]"}
|
||||
value="true"
|
||||
checked={is_checked}
|
||||
class="p-checkbox-input"
|
||||
aria-invalid="false"
|
||||
data-pc-section="input"
|
||||
/>
|
||||
<div
|
||||
class="p-checkbox-box"
|
||||
data-p-highlight={is_checked}
|
||||
data-p-disabled="false"
|
||||
data-pc-section="box"
|
||||
>
|
||||
<svg
|
||||
:if={is_checked}
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="p-icon p-checkbox-icon"
|
||||
aria-hidden="true"
|
||||
data-pc-section="icon"
|
||||
>
|
||||
<path
|
||||
d="M4.86199 11.5948C4.78717 11.5923 4.71366 11.5745 4.64596 11.5426C4.57826 11.5107 4.51779 11.4652 4.46827 11.4091L0.753985 7.69483C0.683167 7.64891 0.623706 7.58751 0.580092 7.51525C0.536478 7.44299 0.509851 7.36177 0.502221 7.27771C0.49459 7.19366 0.506156 7.10897 0.536046 7.03004C0.565935 6.95111 0.613367 6.88 0.674759 6.82208C0.736151 6.76416 0.8099 6.72095 0.890436 6.69571C0.970973 6.67046 1.05619 6.66385 1.13966 6.67635C1.22313 6.68886 1.30266 6.72017 1.37226 6.76792C1.44186 6.81567 1.4997 6.8786 1.54141 6.95197L4.86199 10.2503L12.6397 2.49483C12.7444 2.42694 12.8689 2.39617 12.9932 2.40745C13.1174 2.41873 13.2343 2.47141 13.3251 2.55705C13.4159 2.64268 13.4753 2.75632 13.4938 2.87973C13.5123 3.00315 13.4888 3.1292 13.4271 3.23768L5.2557 11.4091C5.20618 11.4652 5.14571 11.5107 5.07801 11.5426C5.01031 11.5745 4.9368 11.5923 4.86199 11.5948Z"
|
||||
fill="currentColor"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs select-none">{scope_option.label}</span>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:only_tracked_characters]}
|
||||
@@ -170,7 +221,10 @@
|
||||
type="checkbox"
|
||||
field={f[:create_default_acl]}
|
||||
label="Create default access list"
|
||||
checked={Phoenix.HTML.Form.normalize_value("checkbox", f[:create_default_acl].value) == true or is_nil(f[:create_default_acl].value)}
|
||||
checked={
|
||||
Phoenix.HTML.Form.normalize_value("checkbox", f[:create_default_acl].value) == true or
|
||||
is_nil(f[:create_default_acl].value)
|
||||
}
|
||||
/>
|
||||
<.live_select
|
||||
field={f[:acls]}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
rows={@transactions}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={transaction}>
|
||||
<:col :let={_transaction}>
|
||||
<div class=" text-22">
|
||||
<.icon name="hero-credit-card-solid" class="h-5 w-5" />
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@
|
||||
rows={@invoices}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={invoice}>
|
||||
<:col :let={_invoice}>
|
||||
<div class=" text-22">
|
||||
Map subscription
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,6 @@ defmodule WandererAppWeb.OpenApiV1Spec do
|
||||
|
||||
@behaviour OpenApiSpex.OpenApi
|
||||
|
||||
alias OpenApiSpex.{OpenApi, Info, Server, Components}
|
||||
|
||||
@impl OpenApiSpex.OpenApi
|
||||
def spec do
|
||||
# This is called by the modify_open_api option in the router
|
||||
|
||||
@@ -220,7 +220,7 @@ defmodule WandererAppWeb.Plugs.RequestValidator do
|
||||
|
||||
defp validate_params(_params, _max_length, _max_depth, _current_depth), do: :ok
|
||||
|
||||
defp validate_param_value(key, value, max_length, max_depth, current_depth)
|
||||
defp validate_param_value(key, value, max_length, _max_depth, _current_depth)
|
||||
when is_binary(value) do
|
||||
cond do
|
||||
String.length(value) > max_length ->
|
||||
|
||||
@@ -94,7 +94,7 @@ defmodule WandererAppWeb.Plugs.ResponseSanitizer do
|
||||
case Application.get_env(:wanderer_app, :environment) do
|
||||
:dev ->
|
||||
nonce = generate_nonce()
|
||||
conn = put_private(conn, :csp_nonce, nonce)
|
||||
_conn = put_private(conn, :csp_nonce, nonce)
|
||||
|
||||
base_policy
|
||||
|> Enum.map(fn directive ->
|
||||
|
||||
@@ -82,7 +82,8 @@ defmodule WandererAppWeb.Router do
|
||||
"allow-modals",
|
||||
"allow-same-origin",
|
||||
"allow-downloads",
|
||||
"allow-popups"
|
||||
"allow-popups",
|
||||
"allow-popups-to-escape-sandbox"
|
||||
]
|
||||
|
||||
pipeline :admin_bauth do
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.89.2"
|
||||
@version "1.90.5"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapScopesExtensions1 do
|
||||
@moduledoc """
|
||||
Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("ALTER FUNCTION ash_raise_error(jsonb) STABLE;")
|
||||
execute("ALTER FUNCTION ash_raise_error(jsonb, ANYCOMPATIBLE) STABLE")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION uuid_generate_v7()
|
||||
RETURNS UUID
|
||||
AS $$
|
||||
DECLARE
|
||||
timestamp TIMESTAMPTZ;
|
||||
microseconds INT;
|
||||
BEGIN
|
||||
timestamp = clock_timestamp();
|
||||
microseconds = (cast(extract(microseconds FROM timestamp)::INT - (floor(extract(milliseconds FROM timestamp))::INT * 1000) AS DOUBLE PRECISION) * 4.096)::INT;
|
||||
|
||||
RETURN encode(
|
||||
set_byte(
|
||||
set_byte(
|
||||
overlay(uuid_send(gen_random_uuid()) placing substring(int8send(floor(extract(epoch FROM timestamp) * 1000)::BIGINT) FROM 3) FROM 1 FOR 6
|
||||
),
|
||||
6, (b'0111' || (microseconds >> 8)::bit(4))::bit(8)::int
|
||||
),
|
||||
7, microseconds::bit(8)::int
|
||||
),
|
||||
'hex')::UUID;
|
||||
END
|
||||
$$
|
||||
LANGUAGE PLPGSQL
|
||||
SET search_path = ''
|
||||
VOLATILE;
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION timestamp_from_uuid_v7(_uuid uuid)
|
||||
RETURNS TIMESTAMP WITHOUT TIME ZONE
|
||||
AS $$
|
||||
SELECT to_timestamp(('x0000' || substr(_uuid::TEXT, 1, 8) || substr(_uuid::TEXT, 10, 4))::BIT(64)::BIGINT::NUMERIC / 1000);
|
||||
$$
|
||||
LANGUAGE SQL
|
||||
SET search_path = ''
|
||||
IMMUTABLE PARALLEL SAFE STRICT;
|
||||
""")
|
||||
end
|
||||
|
||||
def down do
|
||||
# Uncomment this if you actually want to uninstall the extensions
|
||||
# when this migration is rolled back:
|
||||
execute("ALTER FUNCTION ash_raise_error(jsonb) VOLATILE;")
|
||||
execute("ALTER FUNCTION ash_raise_error(jsonb, ANYCOMPATIBLE) VOLATILE")
|
||||
end
|
||||
end
|
||||
36
priv/repo/migrations/20251129123128_add_map_scopes.exs
Normal file
36
priv/repo/migrations/20251129123128_add_map_scopes.exs
Normal file
@@ -0,0 +1,36 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapScopes do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:maps_v1) do
|
||||
add :scopes, {:array, :text}, default: fragment("'{wormholes}'")
|
||||
end
|
||||
|
||||
create_if_not_exists unique_index(:maps_v1, [:public_api_key],
|
||||
name: "maps_v1_unique_public_api_key_index"
|
||||
)
|
||||
|
||||
drop_if_exists index(:map_system_v1, [:map_id], name: "map_system_v1_map_id_visible_index")
|
||||
end
|
||||
|
||||
def down do
|
||||
create_if_not_exists index(:map_system_v1, [:map_id],
|
||||
name: "map_system_v1_map_id_visible_index",
|
||||
where: "visible = true"
|
||||
)
|
||||
|
||||
drop_if_exists unique_index(:maps_v1, [:public_api_key],
|
||||
name: "maps_v1_unique_public_api_key_index"
|
||||
)
|
||||
|
||||
alter table(:maps_v1) do
|
||||
remove :scopes
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapScopesDefault do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:maps_v1) do
|
||||
modify :scopes, {:array, :text}, default: nil
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:maps_v1) do
|
||||
modify :scopes, {:array, :text}, default: []
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"ash_functions_version": 4,
|
||||
"ash_functions_version": 5,
|
||||
"installed": [
|
||||
"ash-functions"
|
||||
]
|
||||
|
||||
273
priv/resource_snapshots/repo/map_system_v1/20251129123128.json
Normal file
273
priv/resource_snapshots/repo/map_system_v1/20251129123128.json
Normal file
@@ -0,0 +1,273 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "solar_system_id",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "custom_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "tag",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "temporary_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "labels",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "status",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "true",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "visible",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "locked",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "position_x",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "0",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "position_y",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "added_at",
|
||||
"type": "utc_datetime"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "linked_sig_eve_id",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "map_system_v1_map_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "maps_v1"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "map_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "09F2BB9547840BDD02F03DC583DB070F6E9C20DFA88A11E1909A1474DBD618E1",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "map_system_v1_map_solar_system_id_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "map_id"
|
||||
},
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "solar_system_id"
|
||||
}
|
||||
],
|
||||
"name": "map_solar_system_id",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "map_system_v1"
|
||||
}
|
||||
277
priv/resource_snapshots/repo/maps_v1/20251129123128.json
Normal file
277
priv/resource_snapshots/repo/maps_v1/20251129123128.json
Normal file
@@ -0,0 +1,277 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "slug",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "personal_note",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "public_api_key",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "[]",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "hubs",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"wormholes\"",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "scope",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "deleted",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "only_tracked_characters",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "options",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "webhooks_enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "sse_enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "[]",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "scopes",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "maps_v1_owner_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "character_v1"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "owner_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "3D9473E444C93E264B56CF72A88A65B07EAFF29061F29883CAC32E2460C7EC81",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "maps_v1_unique_slug_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "slug"
|
||||
}
|
||||
],
|
||||
"name": "unique_slug",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
},
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "maps_v1_unique_public_api_key_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "public_api_key"
|
||||
}
|
||||
],
|
||||
"name": "unique_public_api_key",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "maps_v1"
|
||||
}
|
||||
277
priv/resource_snapshots/repo/maps_v1/20251129124843.json
Normal file
277
priv/resource_snapshots/repo/maps_v1/20251129124843.json
Normal file
@@ -0,0 +1,277 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "slug",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "personal_note",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "public_api_key",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "[]",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "hubs",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"wormholes\"",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "scope",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "deleted",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "only_tracked_characters",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "options",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "webhooks_enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "sse_enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "scopes",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "maps_v1_owner_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "character_v1"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "owner_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "3DAC7357D834955BDE6456A48FDA7610EE7C2D9C6AB17704563807682F99EBAB",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "maps_v1_unique_public_api_key_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "public_api_key"
|
||||
}
|
||||
],
|
||||
"name": "unique_public_api_key",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
},
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "maps_v1_unique_slug_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "slug"
|
||||
}
|
||||
],
|
||||
"name": "unique_slug",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "maps_v1"
|
||||
}
|
||||
454
test/integration/acl_member_cache_invalidation_test.exs
Normal file
454
test/integration/acl_member_cache_invalidation_test.exs
Normal file
@@ -0,0 +1,454 @@
|
||||
defmodule WandererApp.AclMemberCacheInvalidationTest do
|
||||
@moduledoc """
|
||||
Integration tests for ACL member cache invalidation.
|
||||
|
||||
These tests verify that when ACL members are added, updated, or deleted,
|
||||
the map_characters cache is properly invalidated so that:
|
||||
1. New characters appear in the tracking page immediately after page reload
|
||||
2. The cache doesn't serve stale data after ACL changes
|
||||
3. Both API controller and LiveView paths properly invalidate the cache
|
||||
|
||||
Related files:
|
||||
- lib/wanderer_app/maps.ex (get_map_characters cache)
|
||||
- lib/wanderer_app_web/controllers/access_list_member_api_controller.ex
|
||||
- lib/wanderer_app_web/live/access_lists/access_lists_live.ex
|
||||
- lib/wanderer_app_web/live/characters/characters_tracking_live.ex
|
||||
"""
|
||||
|
||||
use WandererApp.IntegrationCase, async: false
|
||||
|
||||
import Mox
|
||||
import WandererApp.MapTestHelpers
|
||||
import WandererAppWeb.Factory
|
||||
|
||||
require Ash.Query
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
@test_character_eve_id_1 4_100_000_001
|
||||
@test_character_eve_id_2 4_100_000_002
|
||||
@test_character_eve_id_3 4_100_000_003
|
||||
|
||||
setup do
|
||||
# Setup system static info cache for test systems
|
||||
setup_system_static_info_cache()
|
||||
|
||||
# Setup DDRT (R-tree) mock stubs for system positioning
|
||||
setup_ddrt_mocks()
|
||||
|
||||
# Create map owner user and character
|
||||
owner_user =
|
||||
create_user(%{
|
||||
name: "ACL Cache Test Owner",
|
||||
hash: "acl_cache_owner_#{:rand.uniform(1_000_000)}"
|
||||
})
|
||||
|
||||
owner_character =
|
||||
create_character(%{
|
||||
eve_id: "#{@test_character_eve_id_1}",
|
||||
name: "ACL Cache Test Owner Character",
|
||||
user_id: owner_user.id,
|
||||
scopes: "esi-location.read_location.v1 esi-location.read_ship_type.v1",
|
||||
tracking_pool: "default"
|
||||
})
|
||||
|
||||
# Create a second user with a character that will be added to ACL
|
||||
member_user =
|
||||
create_user(%{
|
||||
name: "ACL Cache Test Member",
|
||||
hash: "acl_cache_member_#{:rand.uniform(1_000_000)}"
|
||||
})
|
||||
|
||||
member_character =
|
||||
create_character(%{
|
||||
eve_id: "#{@test_character_eve_id_2}",
|
||||
name: "ACL Cache Test Member Character",
|
||||
user_id: member_user.id,
|
||||
scopes: "esi-location.read_location.v1 esi-location.read_ship_type.v1",
|
||||
tracking_pool: "default"
|
||||
})
|
||||
|
||||
# Create a third character for additional tests
|
||||
member_character2 =
|
||||
create_character(%{
|
||||
eve_id: "#{@test_character_eve_id_3}",
|
||||
name: "ACL Cache Test Member Character 2",
|
||||
user_id: member_user.id,
|
||||
scopes: "esi-location.read_location.v1 esi-location.read_ship_type.v1",
|
||||
tracking_pool: "default"
|
||||
})
|
||||
|
||||
# Create test map owned by first character
|
||||
map =
|
||||
create_map(%{
|
||||
name: "ACL Cache Test Map",
|
||||
slug: "acl-cache-test-#{:rand.uniform(1_000_000)}",
|
||||
owner_id: owner_character.id,
|
||||
scope: :all,
|
||||
only_tracked_characters: false
|
||||
})
|
||||
|
||||
# Create an access list owned by the owner character
|
||||
acl = create_access_list(owner_character.id, %{name: "Test ACL for Cache"})
|
||||
|
||||
# Associate the ACL with the map
|
||||
create_map_access_list(map.id, acl.id)
|
||||
|
||||
on_exit(fn ->
|
||||
cleanup_test_data(map.id)
|
||||
cleanup_character_caches(map.id, owner_character.id)
|
||||
cleanup_character_caches(map.id, member_character.id)
|
||||
cleanup_character_caches(map.id, member_character2.id)
|
||||
# Clean up map_characters cache
|
||||
WandererApp.Cache.delete("map_characters-#{map.id}")
|
||||
end)
|
||||
|
||||
{:ok,
|
||||
owner_user: owner_user,
|
||||
owner_character: owner_character,
|
||||
member_user: member_user,
|
||||
member_character: member_character,
|
||||
member_character2: member_character2,
|
||||
map: map,
|
||||
acl: acl}
|
||||
end
|
||||
|
||||
describe "cache invalidation when ACL members change" do
|
||||
@tag :integration
|
||||
test "adding a character member and broadcasting invalidates map_characters cache", %{
|
||||
map: map,
|
||||
acl: acl,
|
||||
member_character: member_character
|
||||
} do
|
||||
cache_key = "map_characters-#{map.id}"
|
||||
|
||||
# Warm up the cache by loading characters
|
||||
{:ok, _} = WandererApp.Maps.load_characters(map, member_character.user_id)
|
||||
|
||||
# Verify cache is populated
|
||||
cached_data = WandererApp.Cache.lookup!(cache_key)
|
||||
assert not is_nil(cached_data), "Cache should be populated after load_characters"
|
||||
|
||||
# Verify member character is NOT in the cached data (not yet added to ACL)
|
||||
refute member_character.eve_id in cached_data.map_member_eve_ids,
|
||||
"Member character should not be in cache before being added to ACL"
|
||||
|
||||
# Add member directly to database (simulating what API controller does)
|
||||
create_access_list_member(acl.id, %{
|
||||
name: member_character.name,
|
||||
role: "member",
|
||||
eve_character_id: member_character.eve_id
|
||||
})
|
||||
|
||||
# Simulate what the API controller does - broadcast ACL updated
|
||||
# This triggers the cache invalidation
|
||||
invalidate_map_characters_cache_for_acl(acl.id)
|
||||
|
||||
# Verify cache was invalidated
|
||||
cached_data_after = WandererApp.Cache.lookup!(cache_key)
|
||||
assert is_nil(cached_data_after), "Cache should be invalidated after adding member"
|
||||
|
||||
# Reload characters - should get fresh data with new member
|
||||
{:ok, %{characters: characters}} =
|
||||
WandererApp.Maps.load_characters(map, member_character.user_id)
|
||||
|
||||
# Verify member character is now available
|
||||
character_eve_ids = Enum.map(characters, & &1.eve_id)
|
||||
|
||||
assert member_character.eve_id in character_eve_ids,
|
||||
"Member character should be available after being added to ACL"
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
test "adding a corporation member and broadcasting invalidates map_characters cache", %{
|
||||
map: map,
|
||||
acl: acl,
|
||||
member_character: member_character
|
||||
} do
|
||||
cache_key = "map_characters-#{map.id}"
|
||||
|
||||
# Warm up the cache
|
||||
{:ok, _} = WandererApp.Maps.load_characters(map, member_character.user_id)
|
||||
|
||||
# Verify cache is populated
|
||||
cached_data = WandererApp.Cache.lookup!(cache_key)
|
||||
assert not is_nil(cached_data)
|
||||
|
||||
# Add corporation member
|
||||
create_access_list_member(acl.id, %{
|
||||
name: "Test Corporation",
|
||||
role: "viewer",
|
||||
eve_corporation_id: "98000001"
|
||||
})
|
||||
|
||||
# Simulate cache invalidation
|
||||
invalidate_map_characters_cache_for_acl(acl.id)
|
||||
|
||||
# Verify cache was invalidated
|
||||
cached_data_after = WandererApp.Cache.lookup!(cache_key)
|
||||
assert is_nil(cached_data_after), "Cache should be invalidated after adding corporation member"
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
test "adding an alliance member and broadcasting invalidates map_characters cache", %{
|
||||
map: map,
|
||||
acl: acl,
|
||||
member_character: member_character
|
||||
} do
|
||||
cache_key = "map_characters-#{map.id}"
|
||||
|
||||
# Warm up the cache
|
||||
{:ok, _} = WandererApp.Maps.load_characters(map, member_character.user_id)
|
||||
|
||||
# Verify cache is populated
|
||||
cached_data = WandererApp.Cache.lookup!(cache_key)
|
||||
assert not is_nil(cached_data)
|
||||
|
||||
# Add alliance member
|
||||
create_access_list_member(acl.id, %{
|
||||
name: "Test Alliance",
|
||||
role: "viewer",
|
||||
eve_alliance_id: "99000001"
|
||||
})
|
||||
|
||||
# Simulate cache invalidation
|
||||
invalidate_map_characters_cache_for_acl(acl.id)
|
||||
|
||||
# Verify cache was invalidated
|
||||
cached_data_after = WandererApp.Cache.lookup!(cache_key)
|
||||
assert is_nil(cached_data_after), "Cache should be invalidated after adding alliance member"
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
test "deleting a member and broadcasting invalidates map_characters cache", %{
|
||||
map: map,
|
||||
acl: acl,
|
||||
member_character: member_character
|
||||
} do
|
||||
cache_key = "map_characters-#{map.id}"
|
||||
|
||||
# First add a member
|
||||
member =
|
||||
create_access_list_member(acl.id, %{
|
||||
name: member_character.name,
|
||||
role: "viewer",
|
||||
eve_character_id: member_character.eve_id
|
||||
})
|
||||
|
||||
# Clear cache and warm it up again
|
||||
WandererApp.Cache.delete(cache_key)
|
||||
{:ok, _} = WandererApp.Maps.load_characters(map, member_character.user_id)
|
||||
|
||||
# Verify cache is populated and member is included
|
||||
cached_data = WandererApp.Cache.lookup!(cache_key)
|
||||
assert not is_nil(cached_data)
|
||||
|
||||
assert member_character.eve_id in cached_data.map_member_eve_ids,
|
||||
"Member should be in cache before deletion"
|
||||
|
||||
# Delete the member
|
||||
WandererApp.Api.AccessListMember.destroy!(member)
|
||||
|
||||
# Simulate cache invalidation
|
||||
invalidate_map_characters_cache_for_acl(acl.id)
|
||||
|
||||
# Verify cache was invalidated
|
||||
cached_data_after = WandererApp.Cache.lookup!(cache_key)
|
||||
assert is_nil(cached_data_after), "Cache should be invalidated after deleting member"
|
||||
|
||||
# Reload and verify member is no longer in the list
|
||||
{:ok, %{characters: characters}} =
|
||||
WandererApp.Maps.load_characters(map, member_character.user_id)
|
||||
|
||||
character_eve_ids = Enum.map(characters, & &1.eve_id)
|
||||
|
||||
refute member_character.eve_id in character_eve_ids,
|
||||
"Member character should not be available after being removed from ACL"
|
||||
end
|
||||
end
|
||||
|
||||
describe "cache TTL functionality" do
|
||||
@tag :integration
|
||||
test "map_characters cache has TTL and can be invalidated", %{
|
||||
map: map,
|
||||
member_character: member_character
|
||||
} do
|
||||
cache_key = "map_characters-#{map.id}"
|
||||
|
||||
# Warm up the cache
|
||||
{:ok, _} = WandererApp.Maps.load_characters(map, member_character.user_id)
|
||||
|
||||
# Verify cache is populated
|
||||
cached_data = WandererApp.Cache.lookup!(cache_key)
|
||||
assert not is_nil(cached_data), "Cache should be populated"
|
||||
|
||||
# Note: We can't easily test the 5-minute TTL in a unit test,
|
||||
# but we can verify the cache entry exists and can be manually invalidated
|
||||
WandererApp.Cache.delete(cache_key)
|
||||
|
||||
# After deletion, cache should be nil
|
||||
cached_data_after = WandererApp.Cache.lookup!(cache_key)
|
||||
assert is_nil(cached_data_after), "Cache should be nil after deletion"
|
||||
end
|
||||
end
|
||||
|
||||
describe "multiple maps with same ACL" do
|
||||
@tag :integration
|
||||
test "adding member invalidates cache for all maps using the ACL", %{
|
||||
map: map1,
|
||||
owner_character: owner_character,
|
||||
member_character: member_character,
|
||||
acl: acl
|
||||
} do
|
||||
# Create a second map that also uses the same ACL
|
||||
map2 =
|
||||
create_map(%{
|
||||
name: "ACL Cache Test Map 2",
|
||||
slug: "acl-cache-test-2-#{:rand.uniform(1_000_000)}",
|
||||
owner_id: owner_character.id,
|
||||
scope: :all,
|
||||
only_tracked_characters: false
|
||||
})
|
||||
|
||||
# Associate the same ACL with the second map
|
||||
create_map_access_list(map2.id, acl.id)
|
||||
|
||||
cache_key1 = "map_characters-#{map1.id}"
|
||||
cache_key2 = "map_characters-#{map2.id}"
|
||||
|
||||
# Warm up both caches
|
||||
{:ok, _} = WandererApp.Maps.load_characters(map1, member_character.user_id)
|
||||
{:ok, _} = WandererApp.Maps.load_characters(map2, member_character.user_id)
|
||||
|
||||
# Verify both caches are populated
|
||||
cached_data1 = WandererApp.Cache.lookup!(cache_key1)
|
||||
cached_data2 = WandererApp.Cache.lookup!(cache_key2)
|
||||
assert not is_nil(cached_data1), "First map cache should be populated"
|
||||
assert not is_nil(cached_data2), "Second map cache should be populated"
|
||||
|
||||
# Add member
|
||||
create_access_list_member(acl.id, %{
|
||||
name: member_character.name,
|
||||
role: "member",
|
||||
eve_character_id: member_character.eve_id
|
||||
})
|
||||
|
||||
# Simulate cache invalidation (what the API controller does)
|
||||
invalidate_map_characters_cache_for_acl(acl.id)
|
||||
|
||||
# Verify both caches were invalidated
|
||||
cached_data1_after = WandererApp.Cache.lookup!(cache_key1)
|
||||
cached_data2_after = WandererApp.Cache.lookup!(cache_key2)
|
||||
|
||||
assert is_nil(cached_data1_after),
|
||||
"First map cache should be invalidated when member is added to shared ACL"
|
||||
|
||||
assert is_nil(cached_data2_after),
|
||||
"Second map cache should be invalidated when member is added to shared ACL"
|
||||
|
||||
# Cleanup
|
||||
on_exit(fn ->
|
||||
cleanup_test_data(map2.id)
|
||||
WandererApp.Cache.delete(cache_key2)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
describe "load_characters returns fresh data after cache invalidation" do
|
||||
@tag :integration
|
||||
test "newly added member appears in load_characters result", %{
|
||||
map: map,
|
||||
acl: acl,
|
||||
member_user: member_user,
|
||||
member_character: member_character
|
||||
} do
|
||||
# Initially, member character should not be available (not in ACL)
|
||||
{:ok, %{characters: initial_characters}} =
|
||||
WandererApp.Maps.load_characters(map, member_user.id)
|
||||
|
||||
initial_eve_ids = Enum.map(initial_characters, & &1.eve_id)
|
||||
|
||||
refute member_character.eve_id in initial_eve_ids,
|
||||
"Member character should not be available before being added to ACL"
|
||||
|
||||
# Add member to ACL
|
||||
create_access_list_member(acl.id, %{
|
||||
name: member_character.name,
|
||||
role: "member",
|
||||
eve_character_id: member_character.eve_id
|
||||
})
|
||||
|
||||
# Simulate cache invalidation
|
||||
invalidate_map_characters_cache_for_acl(acl.id)
|
||||
|
||||
# Now load_characters should return the new member
|
||||
{:ok, %{characters: updated_characters}} =
|
||||
WandererApp.Maps.load_characters(map, member_user.id)
|
||||
|
||||
updated_eve_ids = Enum.map(updated_characters, & &1.eve_id)
|
||||
|
||||
assert member_character.eve_id in updated_eve_ids,
|
||||
"Member character should be available after being added to ACL and cache invalidation"
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
test "removed member disappears from load_characters result", %{
|
||||
map: map,
|
||||
acl: acl,
|
||||
member_user: member_user,
|
||||
member_character: member_character
|
||||
} do
|
||||
# Add member to ACL first
|
||||
member =
|
||||
create_access_list_member(acl.id, %{
|
||||
name: member_character.name,
|
||||
role: "member",
|
||||
eve_character_id: member_character.eve_id
|
||||
})
|
||||
|
||||
# Clear cache to get fresh data
|
||||
WandererApp.Cache.delete("map_characters-#{map.id}")
|
||||
|
||||
# Member should be available
|
||||
{:ok, %{characters: characters_with_member}} =
|
||||
WandererApp.Maps.load_characters(map, member_user.id)
|
||||
|
||||
eve_ids_with_member = Enum.map(characters_with_member, & &1.eve_id)
|
||||
|
||||
assert member_character.eve_id in eve_ids_with_member,
|
||||
"Member character should be available when in ACL"
|
||||
|
||||
# Remove member
|
||||
WandererApp.Api.AccessListMember.destroy!(member)
|
||||
|
||||
# Simulate cache invalidation
|
||||
invalidate_map_characters_cache_for_acl(acl.id)
|
||||
|
||||
# Member should no longer be available
|
||||
{:ok, %{characters: characters_without_member}} =
|
||||
WandererApp.Maps.load_characters(map, member_user.id)
|
||||
|
||||
eve_ids_without_member = Enum.map(characters_without_member, & &1.eve_id)
|
||||
|
||||
refute member_character.eve_id in eve_ids_without_member,
|
||||
"Member character should not be available after being removed from ACL"
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function that simulates what the API controller does
|
||||
# This is the same logic as in AccessListMemberAPIController.invalidate_map_characters_cache/1
|
||||
defp invalidate_map_characters_cache_for_acl(acl_id) do
|
||||
case Ash.read(
|
||||
WandererApp.Api.MapAccessList
|
||||
|> Ash.Query.for_read(:read_by_acl, %{acl_id: acl_id})
|
||||
) do
|
||||
{:ok, map_acls} ->
|
||||
Enum.each(map_acls, fn %{map_id: map_id} ->
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
end)
|
||||
|
||||
{:error, _error} ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -285,6 +285,7 @@ defmodule WandererAppWeb.MapAccessListAPIControllerTest do
|
||||
|
||||
test "returns 404 for non-existent ACL", %{conn: _conn} do
|
||||
conn = build_conn()
|
||||
|
||||
update_params = %{
|
||||
"acl" => %{
|
||||
"name" => "Updated Name"
|
||||
|
||||
427
test/integration/characters_tracking_live_test.exs
Normal file
427
test/integration/characters_tracking_live_test.exs
Normal file
@@ -0,0 +1,427 @@
|
||||
defmodule WandererApp.CharactersTrackingLiveTest do
|
||||
@moduledoc """
|
||||
Integration tests for CharactersTrackingLive tracking functionality.
|
||||
|
||||
These tests verify the fix for the bug where enabling tracking via the
|
||||
/tracking/:slug page (CharactersTrackingLive) only updated the database
|
||||
but did NOT set the tracking_start_time cache key, causing characters
|
||||
to not actually be tracked on the map.
|
||||
|
||||
The fix ensures CharactersTrackingLive uses TrackingUtils.update_tracking()
|
||||
which properly sets:
|
||||
1. Database tracking flag (MapCharacterSettings.tracked = true)
|
||||
2. Runtime cache key (character:<id>:map:<map_id>:tracking_start_time)
|
||||
|
||||
Related files:
|
||||
- lib/wanderer_app_web/live/characters/characters_tracking_live.ex
|
||||
- lib/wanderer_app/character/tracking_utils.ex
|
||||
- lib/wanderer_app/map.ex (get_tracked_character_ids)
|
||||
"""
|
||||
|
||||
use WandererApp.IntegrationCase, async: false
|
||||
|
||||
import Mox
|
||||
import WandererApp.MapTestHelpers
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
@test_character_eve_id_1 3_100_000_001
|
||||
@test_character_eve_id_2 3_100_000_002
|
||||
|
||||
setup do
|
||||
# Setup system static info cache for test systems
|
||||
setup_system_static_info_cache()
|
||||
|
||||
# Setup DDRT (R-tree) mock stubs for system positioning
|
||||
setup_ddrt_mocks()
|
||||
|
||||
# Create test user
|
||||
user =
|
||||
create_user(%{
|
||||
name: "Tracking Live Test User",
|
||||
hash: "tracking_live_test_hash_#{:rand.uniform(1_000_000)}"
|
||||
})
|
||||
|
||||
# Create first test character
|
||||
character1 =
|
||||
create_character(%{
|
||||
eve_id: "#{@test_character_eve_id_1}",
|
||||
name: "Tracking Live Test Character 1",
|
||||
user_id: user.id,
|
||||
scopes: "esi-location.read_location.v1 esi-location.read_ship_type.v1",
|
||||
tracking_pool: "default"
|
||||
})
|
||||
|
||||
# Create second test character (to test multiple character tracking)
|
||||
character2 =
|
||||
create_character(%{
|
||||
eve_id: "#{@test_character_eve_id_2}",
|
||||
name: "Tracking Live Test Character 2",
|
||||
user_id: user.id,
|
||||
scopes: "esi-location.read_location.v1 esi-location.read_ship_type.v1",
|
||||
tracking_pool: "default"
|
||||
})
|
||||
|
||||
# Create test map owned by first character
|
||||
map =
|
||||
create_map(%{
|
||||
name: "Track Live Test",
|
||||
slug: "tracking-live-test-#{:rand.uniform(1_000_000)}",
|
||||
owner_id: character1.id,
|
||||
scope: :all,
|
||||
only_tracked_characters: false
|
||||
})
|
||||
|
||||
on_exit(fn ->
|
||||
cleanup_test_data(map.id)
|
||||
cleanup_character_caches(map.id, character1.id)
|
||||
cleanup_character_caches(map.id, character2.id)
|
||||
cleanup_tracking_caches(character1.id, map.id)
|
||||
cleanup_tracking_caches(character2.id, map.id)
|
||||
end)
|
||||
|
||||
{:ok, user: user, character1: character1, character2: character2, map: map}
|
||||
end
|
||||
|
||||
describe "TrackingUtils.update_tracking sets tracking_start_time cache key" do
|
||||
@tag :integration
|
||||
test "enabling tracking via update_tracking sets tracking_start_time immediately", %{
|
||||
map: map,
|
||||
character1: character,
|
||||
user: user
|
||||
} do
|
||||
# Verify no tracking_start_time exists initially
|
||||
tracking_key = "character:#{character.id}:map:#{map.id}:tracking_start_time"
|
||||
{:ok, initial_value} = WandererApp.Cache.lookup(tracking_key)
|
||||
assert is_nil(initial_value), "tracking_start_time should not exist initially"
|
||||
|
||||
# Call update_tracking (this is what CharactersTrackingLive now does)
|
||||
# The eve_id is passed as a string which is how it comes from the UI
|
||||
result =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character.eve_id,
|
||||
user.id,
|
||||
true,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
assert {:ok, _tracking_data, _event} = result
|
||||
|
||||
# Verify tracking_start_time is now set
|
||||
{:ok, tracking_start_time} = WandererApp.Cache.lookup(tracking_key)
|
||||
|
||||
assert not is_nil(tracking_start_time),
|
||||
"tracking_start_time should be set immediately after enabling tracking"
|
||||
|
||||
assert %DateTime{} = tracking_start_time,
|
||||
"tracking_start_time should be a DateTime"
|
||||
|
||||
# Verify the time is recent (within last 5 seconds)
|
||||
time_diff = DateTime.diff(DateTime.utc_now(), tracking_start_time, :second)
|
||||
assert time_diff >= 0 and time_diff < 5, "tracking_start_time should be recent"
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
test "enabling tracking for multiple characters sets all tracking_start_times", %{
|
||||
map: map,
|
||||
character1: character1,
|
||||
character2: character2,
|
||||
user: user
|
||||
} do
|
||||
# Verify no tracking_start_time exists initially for either character
|
||||
tracking_key1 = "character:#{character1.id}:map:#{map.id}:tracking_start_time"
|
||||
tracking_key2 = "character:#{character2.id}:map:#{map.id}:tracking_start_time"
|
||||
|
||||
{:ok, initial_value1} = WandererApp.Cache.lookup(tracking_key1)
|
||||
{:ok, initial_value2} = WandererApp.Cache.lookup(tracking_key2)
|
||||
|
||||
assert is_nil(initial_value1), "tracking_start_time should not exist initially for char1"
|
||||
assert is_nil(initial_value2), "tracking_start_time should not exist initially for char2"
|
||||
|
||||
# Enable tracking for first character
|
||||
{:ok, _, _} =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character1.eve_id,
|
||||
user.id,
|
||||
true,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
# Enable tracking for second character
|
||||
{:ok, _, _} =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character2.eve_id,
|
||||
user.id,
|
||||
true,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
# Verify both characters have tracking_start_time set
|
||||
{:ok, tracking_time1} = WandererApp.Cache.lookup(tracking_key1)
|
||||
{:ok, tracking_time2} = WandererApp.Cache.lookup(tracking_key2)
|
||||
|
||||
assert not is_nil(tracking_time1),
|
||||
"Character 1 should have tracking_start_time set"
|
||||
|
||||
assert not is_nil(tracking_time2),
|
||||
"Character 2 should have tracking_start_time set"
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
test "disabling tracking via update_tracking works correctly", %{
|
||||
map: map,
|
||||
character1: character,
|
||||
user: user
|
||||
} do
|
||||
# First enable tracking
|
||||
{:ok, _, _} =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character.eve_id,
|
||||
user.id,
|
||||
true,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
# Verify tracking is enabled
|
||||
{:ok, settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.get(map.id, character.id)
|
||||
|
||||
assert settings.tracked == true, "Character should be tracked after enabling"
|
||||
|
||||
# Now disable tracking
|
||||
{:ok, _, _} =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character.eve_id,
|
||||
user.id,
|
||||
false,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
# Verify tracking is disabled in database
|
||||
{:ok, updated_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.get(map.id, character.id)
|
||||
|
||||
assert updated_settings.tracked == false, "Character should be untracked after disabling"
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
test "toggle tracking on and off maintains correct state", %{
|
||||
map: map,
|
||||
character1: character,
|
||||
user: user
|
||||
} do
|
||||
tracking_key = "character:#{character.id}:map:#{map.id}:tracking_start_time"
|
||||
|
||||
# Initial state: not tracked
|
||||
{:ok, initial_time} = WandererApp.Cache.lookup(tracking_key)
|
||||
assert is_nil(initial_time)
|
||||
|
||||
# Toggle ON
|
||||
{:ok, _, _} =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character.eve_id,
|
||||
user.id,
|
||||
true,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
{:ok, time_after_on} = WandererApp.Cache.lookup(tracking_key)
|
||||
assert not is_nil(time_after_on), "tracking_start_time should be set after toggle ON"
|
||||
|
||||
{:ok, settings_on} = WandererApp.MapCharacterSettingsRepo.get(map.id, character.id)
|
||||
assert settings_on.tracked == true
|
||||
|
||||
# Toggle OFF
|
||||
{:ok, _, _} =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character.eve_id,
|
||||
user.id,
|
||||
false,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
{:ok, settings_off} = WandererApp.MapCharacterSettingsRepo.get(map.id, character.id)
|
||||
assert settings_off.tracked == false
|
||||
|
||||
# Toggle ON again
|
||||
{:ok, _, _} =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character.eve_id,
|
||||
user.id,
|
||||
true,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
{:ok, time_after_second_on} = WandererApp.Cache.lookup(tracking_key)
|
||||
|
||||
assert not is_nil(time_after_second_on),
|
||||
"tracking_start_time should be set after second toggle ON"
|
||||
|
||||
{:ok, settings_on_again} = WandererApp.MapCharacterSettingsRepo.get(map.id, character.id)
|
||||
assert settings_on_again.tracked == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "Database and cache consistency" do
|
||||
@tag :integration
|
||||
test "tracking state is consistent between database and cache", %{
|
||||
map: map,
|
||||
character1: character,
|
||||
user: user
|
||||
} do
|
||||
tracking_key = "character:#{character.id}:map:#{map.id}:tracking_start_time"
|
||||
|
||||
# Enable tracking
|
||||
{:ok, _, _} =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character.eve_id,
|
||||
user.id,
|
||||
true,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
# Check database state
|
||||
{:ok, db_settings} = WandererApp.MapCharacterSettingsRepo.get(map.id, character.id)
|
||||
assert db_settings.tracked == true, "Database should show tracked=true"
|
||||
|
||||
# Check cache state
|
||||
{:ok, cache_time} = WandererApp.Cache.lookup(tracking_key)
|
||||
assert not is_nil(cache_time), "Cache should have tracking_start_time"
|
||||
|
||||
# Both should indicate the character is being tracked
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
test "stale location caches are cleared when tracking is re-enabled", %{
|
||||
map: map,
|
||||
character1: character,
|
||||
user: user
|
||||
} do
|
||||
# Set up stale location caches (simulating previous tracking session)
|
||||
WandererApp.Cache.insert(
|
||||
"map:#{map.id}:character:#{character.id}:solar_system_id",
|
||||
30_000_142
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map:#{map.id}:character:#{character.id}:station_id",
|
||||
60_003_760
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map:#{map.id}:character:#{character.id}:structure_id",
|
||||
1_000_000_001
|
||||
)
|
||||
|
||||
# Verify stale caches exist
|
||||
{:ok, stale_system} =
|
||||
WandererApp.Cache.lookup("map:#{map.id}:character:#{character.id}:solar_system_id")
|
||||
|
||||
assert stale_system == 30_000_142
|
||||
|
||||
# Enable tracking (this should clear stale location caches)
|
||||
{:ok, _, _} =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character.eve_id,
|
||||
user.id,
|
||||
true,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
# Verify stale caches are cleared
|
||||
{:ok, cleared_system} =
|
||||
WandererApp.Cache.lookup("map:#{map.id}:character:#{character.id}:solar_system_id")
|
||||
|
||||
{:ok, cleared_station} =
|
||||
WandererApp.Cache.lookup("map:#{map.id}:character:#{character.id}:station_id")
|
||||
|
||||
{:ok, cleared_structure} =
|
||||
WandererApp.Cache.lookup("map:#{map.id}:character:#{character.id}:structure_id")
|
||||
|
||||
assert is_nil(cleared_system), "solar_system_id cache should be cleared"
|
||||
assert is_nil(cleared_station), "station_id cache should be cleared"
|
||||
assert is_nil(cleared_structure), "structure_id cache should be cleared"
|
||||
end
|
||||
end
|
||||
|
||||
describe "Error handling" do
|
||||
@tag :integration
|
||||
test "update_tracking returns error for invalid character", %{
|
||||
map: map,
|
||||
user: user
|
||||
} do
|
||||
# Try to enable tracking for a non-existent character
|
||||
result =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
"999999999999",
|
||||
user.id,
|
||||
true,
|
||||
self(),
|
||||
false
|
||||
)
|
||||
|
||||
assert {:error, _reason} = result
|
||||
end
|
||||
|
||||
@tag :integration
|
||||
test "update_tracking handles nil caller_pid gracefully", %{
|
||||
map: map,
|
||||
character1: character,
|
||||
user: user
|
||||
} do
|
||||
# Calling with nil caller_pid should return an error
|
||||
result =
|
||||
WandererApp.Character.TrackingUtils.update_tracking(
|
||||
map.id,
|
||||
character.eve_id,
|
||||
user.id,
|
||||
true,
|
||||
nil,
|
||||
false
|
||||
)
|
||||
|
||||
assert {:error, _reason} = result
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to cleanup tracking-specific caches
|
||||
defp cleanup_tracking_caches(character_id, map_id) do
|
||||
WandererApp.Cache.delete("character:#{character_id}:map:#{map_id}:tracking_start_time")
|
||||
WandererApp.Cache.delete("#{character_id}:track_requested")
|
||||
|
||||
# Clean up presence subscription cache
|
||||
WandererApp.Cache.delete("#{inspect(self())}_map_#{map_id}:character_#{character_id}:tracked")
|
||||
|
||||
# Clean up untrack queue
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"character_untrack_queue",
|
||||
[],
|
||||
fn queue ->
|
||||
Enum.reject(queue, fn {m_id, c_id} ->
|
||||
m_id == map_id and c_id == character_id
|
||||
end)
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
@@ -34,7 +34,11 @@ defmodule WandererApp.Map.CharacterTrackingEnableTest do
|
||||
setup_ddrt_mocks()
|
||||
|
||||
# Create test user
|
||||
user = create_user(%{name: "Tracking Test User", hash: "tracking_test_hash_#{:rand.uniform(1_000_000)}"})
|
||||
user =
|
||||
create_user(%{
|
||||
name: "Tracking Test User",
|
||||
hash: "tracking_test_hash_#{:rand.uniform(1_000_000)}"
|
||||
})
|
||||
|
||||
# Create test character with location tracking scopes
|
||||
character =
|
||||
|
||||
315
test/integration/map/map_scope_filtering_test.exs
Normal file
315
test/integration/map/map_scope_filtering_test.exs
Normal file
@@ -0,0 +1,315 @@
|
||||
defmodule WandererApp.Map.MapScopeFilteringTest do
|
||||
@moduledoc """
|
||||
Integration tests for map scope filtering during character location tracking.
|
||||
|
||||
These tests verify that systems are correctly filtered based on map scope settings
|
||||
when characters move between systems. The key scenarios tested:
|
||||
|
||||
1. Characters moving between systems with [:wormholes, :null] scopes:
|
||||
- Wormhole systems should be added
|
||||
- Null-sec systems should be added
|
||||
- High-sec systems should NOT be added (filtered out)
|
||||
- Low-sec systems should NOT be added (filtered out)
|
||||
|
||||
2. Wormhole border behavior:
|
||||
- When a character jumps from wormhole to k-space, the wormhole should be added
|
||||
- K-space border systems should only be added if they match the scopes
|
||||
|
||||
3. K-space only movement:
|
||||
- Characters moving within k-space should only track systems matching scopes
|
||||
- No "border system" behavior for k-space to k-space movement
|
||||
|
||||
Reference bug: Characters with [:wormholes, :null] scopes were getting
|
||||
high-sec (0.6) and low-sec (0.4) systems added to the map when traveling.
|
||||
"""
|
||||
|
||||
use WandererApp.DataCase
|
||||
|
||||
# System class constants (matching ConnectionsImpl)
|
||||
@c1 1
|
||||
@c2 2
|
||||
@hs 7
|
||||
@ls 8
|
||||
@ns 9
|
||||
|
||||
# Test solar system IDs
|
||||
# C1 wormhole
|
||||
@wh_system_j100001 31_000_001
|
||||
# C2 wormhole
|
||||
@wh_system_j100002 31_000_002
|
||||
# High-sec system (0.6)
|
||||
@hs_system_halenan 30_000_001
|
||||
# High-sec system (0.6)
|
||||
@hs_system_mili 30_000_002
|
||||
# Low-sec system (0.4)
|
||||
@ls_system_halmah 30_000_100
|
||||
# Null-sec system
|
||||
@ns_system_geminate 30_000_200
|
||||
|
||||
setup do
|
||||
# Setup system static info cache with both wormhole and k-space systems
|
||||
setup_scope_test_systems()
|
||||
:ok
|
||||
end
|
||||
|
||||
# Setup system static info for scope testing
|
||||
defp setup_scope_test_systems do
|
||||
test_systems = %{
|
||||
# C1 Wormhole
|
||||
@wh_system_j100001 => %{
|
||||
solar_system_id: @wh_system_j100001,
|
||||
solar_system_name: "J100001",
|
||||
solar_system_name_lc: "j100001",
|
||||
region_id: 11_000_001,
|
||||
constellation_id: 21_000_001,
|
||||
region_name: "A-R00001",
|
||||
constellation_name: "A-C00001",
|
||||
system_class: @c1,
|
||||
security: "-1.0",
|
||||
type_description: "Class 1",
|
||||
class_title: "C1",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: ["H121"],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# C2 Wormhole
|
||||
@wh_system_j100002 => %{
|
||||
solar_system_id: @wh_system_j100002,
|
||||
solar_system_name: "J100002",
|
||||
solar_system_name_lc: "j100002",
|
||||
region_id: 11_000_001,
|
||||
constellation_id: 21_000_001,
|
||||
region_name: "A-R00001",
|
||||
constellation_name: "A-C00001",
|
||||
system_class: @c2,
|
||||
security: "-1.0",
|
||||
type_description: "Class 2",
|
||||
class_title: "C2",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: ["D382", "L005"],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# High-sec system (Halenan 0.6)
|
||||
@hs_system_halenan => %{
|
||||
solar_system_id: @hs_system_halenan,
|
||||
solar_system_name: "Halenan",
|
||||
solar_system_name_lc: "halenan",
|
||||
region_id: 10_000_067,
|
||||
constellation_id: 20_000_901,
|
||||
region_name: "Devoid",
|
||||
constellation_name: "Devoid",
|
||||
system_class: @hs,
|
||||
security: "0.6",
|
||||
type_description: "High Security",
|
||||
class_title: "High Sec",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: [],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# High-sec system (Mili 0.6)
|
||||
@hs_system_mili => %{
|
||||
solar_system_id: @hs_system_mili,
|
||||
solar_system_name: "Mili",
|
||||
solar_system_name_lc: "mili",
|
||||
region_id: 10_000_067,
|
||||
constellation_id: 20_000_901,
|
||||
region_name: "Devoid",
|
||||
constellation_name: "Devoid",
|
||||
system_class: @hs,
|
||||
security: "0.6",
|
||||
type_description: "High Security",
|
||||
class_title: "High Sec",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: [],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# Low-sec system (Halmah 0.4)
|
||||
@ls_system_halmah => %{
|
||||
solar_system_id: @ls_system_halmah,
|
||||
solar_system_name: "Halmah",
|
||||
solar_system_name_lc: "halmah",
|
||||
region_id: 10_000_067,
|
||||
constellation_id: 20_000_901,
|
||||
region_name: "Devoid",
|
||||
constellation_name: "Devoid",
|
||||
system_class: @ls,
|
||||
security: "0.4",
|
||||
type_description: "Low Security",
|
||||
class_title: "Low Sec",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: [],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
},
|
||||
# Null-sec system
|
||||
@ns_system_geminate => %{
|
||||
solar_system_id: @ns_system_geminate,
|
||||
solar_system_name: "Geminate",
|
||||
solar_system_name_lc: "geminate",
|
||||
region_id: 10_000_029,
|
||||
constellation_id: 20_000_400,
|
||||
region_name: "Geminate",
|
||||
constellation_name: "Geminate",
|
||||
system_class: @ns,
|
||||
security: "-0.5",
|
||||
type_description: "Null Security",
|
||||
class_title: "Null Sec",
|
||||
is_shattered: false,
|
||||
effect_name: nil,
|
||||
effect_power: nil,
|
||||
statics: [],
|
||||
wandering: [],
|
||||
triglavian_invasion_status: nil,
|
||||
sun_type_id: 45041
|
||||
}
|
||||
}
|
||||
|
||||
Enum.each(test_systems, fn {solar_system_id, system_info} ->
|
||||
Cachex.put(:system_static_info_cache, solar_system_id, system_info)
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
describe "Scope filtering logic tests" do
|
||||
# These tests verify the filtering logic without full integration
|
||||
# The actual filtering is tested more comprehensively in map_scopes_test.exs
|
||||
|
||||
alias WandererApp.Map.Server.ConnectionsImpl
|
||||
alias WandererApp.Map.Server.SystemsImpl
|
||||
|
||||
test "can_add_location correctly filters high-sec with [:wormholes, :null] scopes" do
|
||||
# High-sec should NOT be allowed with [:wormholes, :null]
|
||||
refute ConnectionsImpl.can_add_location([:wormholes, :null], @hs_system_halenan),
|
||||
"High-sec should be filtered out with [:wormholes, :null] scopes"
|
||||
|
||||
refute ConnectionsImpl.can_add_location([:wormholes, :null], @hs_system_mili),
|
||||
"High-sec should be filtered out with [:wormholes, :null] scopes"
|
||||
end
|
||||
|
||||
test "can_add_location correctly filters low-sec with [:wormholes, :null] scopes" do
|
||||
# Low-sec should NOT be allowed with [:wormholes, :null]
|
||||
refute ConnectionsImpl.can_add_location([:wormholes, :null], @ls_system_halmah),
|
||||
"Low-sec should be filtered out with [:wormholes, :null] scopes"
|
||||
end
|
||||
|
||||
test "can_add_location correctly allows wormholes with [:wormholes, :null] scopes" do
|
||||
# Wormholes should be allowed
|
||||
assert ConnectionsImpl.can_add_location([:wormholes, :null], @wh_system_j100001),
|
||||
"Wormhole should be allowed with [:wormholes, :null] scopes"
|
||||
|
||||
assert ConnectionsImpl.can_add_location([:wormholes, :null], @wh_system_j100002),
|
||||
"Wormhole should be allowed with [:wormholes, :null] scopes"
|
||||
end
|
||||
|
||||
test "can_add_location correctly allows null-sec with [:wormholes, :null] scopes" do
|
||||
# Null-sec should be allowed
|
||||
assert ConnectionsImpl.can_add_location([:wormholes, :null], @ns_system_geminate),
|
||||
"Null-sec should be allowed with [:wormholes, :null] scopes"
|
||||
end
|
||||
|
||||
test "maybe_add_system filters out high-sec when scopes is [:wormholes, :null]" do
|
||||
# When scopes is [:wormholes, :null], high-sec systems should be filtered
|
||||
location = %{solar_system_id: @hs_system_halenan}
|
||||
result = SystemsImpl.maybe_add_system("map_id", location, nil, [], [:wormholes, :null])
|
||||
# Returns :ok because system was filtered out (not an error, just skipped)
|
||||
assert result == :ok
|
||||
end
|
||||
|
||||
test "maybe_add_system filters out low-sec when scopes is [:wormholes, :null]" do
|
||||
location = %{solar_system_id: @ls_system_halmah}
|
||||
result = SystemsImpl.maybe_add_system("map_id", location, nil, [], [:wormholes, :null])
|
||||
assert result == :ok
|
||||
end
|
||||
|
||||
test "is_connection_valid allows WH to HS with [:wormholes, :null] (border behavior)" do
|
||||
# The connection is valid for border behavior - but individual systems are filtered
|
||||
assert ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@wh_system_j100001,
|
||||
@hs_system_halenan
|
||||
),
|
||||
"WH to HS connection should be valid (border behavior)"
|
||||
end
|
||||
|
||||
test "is_connection_valid rejects HS to LS with [:wormholes, :null] (no border)" do
|
||||
# HS to LS should be rejected - neither system matches scopes and no wormhole involved
|
||||
refute ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@hs_system_halenan,
|
||||
@ls_system_halmah
|
||||
),
|
||||
"HS to LS connection should be rejected with [:wormholes, :null]"
|
||||
end
|
||||
|
||||
test "is_connection_valid rejects HS to HS with [:wormholes, :null]" do
|
||||
# HS to HS should be rejected
|
||||
refute ConnectionsImpl.is_connection_valid(
|
||||
[:wormholes, :null],
|
||||
@hs_system_halenan,
|
||||
@hs_system_mili
|
||||
),
|
||||
"HS to HS connection should be rejected with [:wormholes, :null]"
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_effective_scopes behavior" do
|
||||
alias WandererApp.Map.Server.CharactersImpl
|
||||
|
||||
test "get_effective_scopes returns scopes array when present" do
|
||||
# Create a map struct with scopes array
|
||||
map = %{scopes: [:wormholes, :null]}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:wormholes, :null]
|
||||
end
|
||||
|
||||
test "get_effective_scopes converts legacy :all scope" do
|
||||
map = %{scope: :all}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:wormholes, :hi, :low, :null, :pochven]
|
||||
end
|
||||
|
||||
test "get_effective_scopes converts legacy :wormholes scope" do
|
||||
map = %{scope: :wormholes}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:wormholes]
|
||||
end
|
||||
|
||||
test "get_effective_scopes converts legacy :stargates scope" do
|
||||
map = %{scope: :stargates}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:hi, :low, :null, :pochven]
|
||||
end
|
||||
|
||||
test "get_effective_scopes converts legacy :none scope" do
|
||||
map = %{scope: :none}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == []
|
||||
end
|
||||
|
||||
test "get_effective_scopes defaults to [:wormholes] when no scope" do
|
||||
map = %{}
|
||||
scopes = CharactersImpl.get_effective_scopes(map)
|
||||
assert scopes == [:wormholes]
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -14,6 +14,10 @@ defmodule WandererAppWeb.MapSystemAPIControllerSuccessTest do
|
||||
# Setup DDRT (R-tree) mock stubs for system positioning
|
||||
setup_ddrt_mocks()
|
||||
|
||||
# Setup system static info cache with test systems
|
||||
# This is required because CachedInfo reads from Cachex cache first
|
||||
setup_system_static_info_cache()
|
||||
|
||||
user = insert(:user)
|
||||
character = insert(:character, %{user_id: user.id})
|
||||
map = insert(:map, %{owner_id: character.id})
|
||||
@@ -36,7 +40,8 @@ defmodule WandererAppWeb.MapSystemAPIControllerSuccessTest do
|
||||
# Ensure it's stopped first to prevent state leakage from previous tests
|
||||
ensure_map_stopped(map.id)
|
||||
|
||||
# Seed static solar system data
|
||||
# Seed static solar system data in database
|
||||
# (Also populate cache above for CachedInfo lookups)
|
||||
insert(:solar_system, %{
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
@@ -248,6 +253,9 @@ defmodule WandererAppWeb.MapSystemAPIControllerSuccessTest do
|
||||
# Setup DDRT (R-tree) mock stubs for system positioning
|
||||
setup_ddrt_mocks()
|
||||
|
||||
# Setup system static info cache with test systems
|
||||
setup_system_static_info_cache()
|
||||
|
||||
user = insert(:user)
|
||||
character = insert(:character, %{user_id: user.id})
|
||||
map = insert(:map, %{owner_id: character.id})
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user