Compare commits

...

63 Commits

Author SHA1 Message Date
CI
3bd5db8cf3 chore: release version v1.90.10 2025-12-18 18:05:48 +00:00
Dmitry Popov
a245330ada Merge branch 'advent-challenge' 2025-12-18 19:05:10 +01:00
Dmitry Popov
1226b6abf3 chore: added advent challenge 2025-12-18 19:04:43 +01:00
Dmitry Popov
7a1f5c0966 chore: [skip ci] 2025-12-17 19:32:37 +01:00
CI
e5afa1d5bc chore: [skip ci] 2025-12-15 11:46:40 +00:00
CI
1473fe8646 chore: release version v1.90.9 2025-12-15 11:46:40 +00:00
Dmitry Popov
7039ced11e fix(core): reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-15 12:46:02 +01:00
CI
42b5bb337f chore: [skip ci] 2025-12-15 11:35:24 +00:00
CI
1dbb24f6ec chore: release version v1.90.8 2025-12-15 11:35:24 +00:00
Dmitry Popov
c242f510e0 fix(core): skip systems or connections cleanup for not started maps 2025-12-15 12:34:55 +01:00
CI
c59d51636e chore: [skip ci] 2025-12-15 00:36:18 +00:00
CI
c5a8aa1b4d chore: release version v1.90.7 2025-12-15 00:36:18 +00:00
Dmitry Popov
cba050a9e7 fix(core): fixed scopes 2025-12-15 01:35:41 +01:00
CI
59fcbef3b1 chore: [skip ci] 2025-12-12 18:49:02 +00:00
CI
2f1eb6eeaa chore: release version v1.90.6 2025-12-12 18:49:02 +00:00
Dmitry Popov
71ae326cf7 fix(core): fixed map scopes 2025-12-12 19:48:26 +01:00
CI
07829caf0f chore: [skip ci] 2025-12-12 18:36:03 +00:00
CI
a5850b5a8d chore: release version v1.90.5 2025-12-12 18:36:03 +00:00
Dmitry Popov
9f6849209b fix(core): fixed map scopes 2025-12-12 19:35:26 +01:00
CI
7bd295cbad chore: [skip ci] 2025-12-12 17:07:55 +00:00
CI
078e5fc19e chore: release version v1.90.4 2025-12-12 17:07:55 +00:00
Dmitry Popov
3877e121c3 fix(core): fixed map scopes & signatures clean up behaviour 2025-12-12 18:07:18 +01:00
CI
dcb2a0cdb2 chore: [skip ci] 2025-12-11 00:17:06 +00:00
CI
f5294eee84 chore: release version v1.90.3 2025-12-11 00:17:06 +00:00
Dmitry Popov
a5c87b6fa4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-11 01:16:27 +01:00
Dmitry Popov
eae275f515 fix(core): added pagination for long ACL lists 2025-12-11 01:16:24 +01:00
CI
68ae6706dd chore: [skip ci] 2025-12-10 23:56:28 +00:00
CI
a34b30af15 chore: release version v1.90.2 2025-12-10 23:56:28 +00:00
Dmitry Popov
38b49266ed fix(core): added system position updates to SSE
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-11 00:55:52 +01:00
CI
049884bb4c chore: [skip ci] 2025-12-08 21:56:20 +00:00
CI
3c75b2b59f chore: release version v1.90.1 2025-12-08 21:56:20 +00:00
Dmitry Popov
4ad5d191a3 fix(core): fixed connections and signatures remove issues, added comprehensive audit log for auto removed connections and signatures 2025-12-08 22:55:39 +01:00
CI
2499c24cc1 chore: [skip ci] 2025-12-06 10:58:14 +00:00
CI
6f0043205c chore: release version v1.90.0 2025-12-06 10:58:14 +00:00
Dmitry Popov
597741fa60 Merge pull request #567 from wanderer-industries/develop
Develop
2025-12-06 14:57:27 +04:00
Dmitry Popov
d313ae8cd2 fix(core): fixed clean up for linked signatures
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-12-04 11:33:42 +01:00
Dmitry Popov
06d5d8072e fix(core): fixed issue with default select mode
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-12-03 12:21:40 +01:00
CI
f2d112df5c chore: [skip ci] 2025-12-02 23:44:54 +00:00
CI
716604fa84 chore: release version v1.89.6 2025-12-02 23:44:54 +00:00
Dmitry Popov
cae958a1e6 Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-12-03 00:44:31 +01:00
Dmitry Popov
283b36c882 fix(kills): fixed zkb links (added "allow-popups-to-escape-sandbox" to CSP) 2025-12-03 00:44:23 +01:00
Dmitry Popov
051e71f1a6 Merge pull request #566 from guarzo/guarzo/sigapi
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
fix: apiV1 default fields updates
2025-12-03 00:20:13 +04:00
Guarzo
20a50e8db0 fix: apiV1 default fields updates 2025-12-02 17:55:05 +00:00
CI
79d7f7ce7d chore: [skip ci] 2025-12-02 12:46:26 +00:00
CI
6c4b65c446 chore: release version v1.89.5 2025-12-02 12:46:26 +00:00
Dmitry Popov
2b07af5e12 Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-12-02 13:45:57 +01:00
Dmitry Popov
d0901eecb4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-12-02 13:45:51 +01:00
Dmitry Popov
ee85d29c54 chore: added tests 2025-12-02 13:45:47 +01:00
Dmitry Popov
a237d6513d Merge branch 'main' into develop 2025-12-02 13:37:13 +01:00
CI
02979588c1 chore: [skip ci] 2025-12-02 12:35:31 +00:00
CI
3abe40855f chore: release version v1.89.4 2025-12-02 12:35:30 +00:00
Dmitry Popov
d0d9418a89 fix(core): fixed acl character update issues 2025-12-02 13:34:55 +01:00
CI
3ce742eb01 chore: [skip ci] 2025-11-30 22:26:08 +00:00
CI
ae566fb907 chore: release version v1.89.3 2025-11-30 22:26:08 +00:00
Dmitry Popov
fa32c62f63 Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-11-30 23:25:48 +01:00
Dmitry Popov
6880be11c5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-30 23:25:40 +01:00
Dmitry Popov
5289893264 fix(core): fixed tracking issues 2025-11-30 23:25:37 +01:00
CI
f15370a3df chore: [skip ci] 2025-11-30 18:07:05 +00:00
Dmitry Popov
2cb2dc526c Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-30 18:51:58 +01:00
Dmitry Popov
c3de3c4e35 Merge branch 'main' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
2025-11-29 20:17:14 +01:00
Dmitry Popov
4585c3a94b feat(core): Added several map scopes support (Wh, Hi, Low, Null, Pochven) 2025-11-29 14:36:45 +01:00
Dmitry Popov
46a1898be9 Merge branch 'fixed-warinings' into develop
Some checks failed
Build Test / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build Test / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build Develop / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build Develop / merge (push) Has been cancelled
Build Develop / 🏷 Notify about develop release (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
2025-11-29 12:35:36 +01:00
Dmitry Popov
e7219e0eec chore: fixed compile warnings 2025-11-29 12:34:28 +01:00
127 changed files with 4493 additions and 622 deletions

View File

@@ -2,6 +2,141 @@
<!-- changelog -->
## [v1.90.10](https://github.com/wanderer-industries/wanderer/compare/v1.90.9...v1.90.10) (2025-12-18)
## [v1.90.9](https://github.com/wanderer-industries/wanderer/compare/v1.90.8...v1.90.9) (2025-12-15)
### Bug Fixes:
* core: reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
### Bug Fixes:
* core: skip systems or connections cleanup for not started maps
## [v1.90.7](https://github.com/wanderer-industries/wanderer/compare/v1.90.6...v1.90.7) (2025-12-15)
### Bug Fixes:
* core: fixed scopes
## [v1.90.6](https://github.com/wanderer-industries/wanderer/compare/v1.90.5...v1.90.6) (2025-12-12)
### Bug Fixes:
* core: fixed map scopes
## [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)

View File

@@ -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

View File

@@ -57,7 +57,7 @@ export default {
};
refreshZone.addEventListener('click', handleUpdate);
refreshZone.addEventListener('mouseover', handleUpdate);
// refreshZone.addEventListener('mouseover', handleUpdate);
this.updated();
},

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -264,7 +264,7 @@ config :logger,
case config_env() do
:prod -> "info"
:dev -> "info"
:test -> "debug"
:test -> "warning"
end
)
)

View File

@@ -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

View File

@@ -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: [

View File

@@ -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 ->

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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 \\ [])

View File

@@ -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(

View File

@@ -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)

View File

@@ -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

View File

@@ -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"

View File

@@ -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(

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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" => %{

View File

@@ -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}}

View File

@@ -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

View File

@@ -12,6 +12,7 @@ defmodule WandererApp.Map do
defstruct map_id: nil,
name: nil,
scope: :none,
scopes: nil,
owner_id: nil,
characters: [],
systems: Map.new(),
@@ -22,11 +23,15 @@ defmodule WandererApp.Map do
characters_limit: nil,
hubs_limit: nil
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs}) do
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs} = input) do
# Extract the new scopes array field if present (nil if not set)
scopes = Map.get(input, :scopes)
map =
struct!(__MODULE__,
map_id: map_id,
scope: scope,
scopes: scopes,
owner_id: owner_id,
name: name,
acls: acls,
@@ -177,7 +182,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 +320,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 +331,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})

View File

@@ -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()}

View File

@@ -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)

View File

@@ -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

View File

@@ -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()))

View File

@@ -93,10 +93,8 @@ defmodule WandererApp.Map.Operations.Connections do
end
end
@doc """
Determines the ship size for a connection, applying wormholespecific rules
for C1, C13, and C4⇄NS links, falling back to the callers 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 ->

View File

@@ -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}

View File

@@ -814,21 +814,33 @@ 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
ConnectionsImpl.is_connection_valid(
scope,
old_location.solar_system_id,
location.solar_system_id
scopes = get_effective_scopes(map)
is_valid =
ConnectionsImpl.is_connection_valid(
scopes,
old_location.solar_system_id,
location.solar_system_id
)
Logger.debug(
"[CharacterTracking] update_location: map=#{map_id}, " <>
"from=#{old_location.solar_system_id}, to=#{location.solar_system_id}, " <>
"scopes=#{inspect(scopes)}, map.scopes=#{inspect(map[:scopes])}, " <>
"map.scope=#{inspect(map[:scope])}, is_valid=#{is_valid}"
)
|> case do
case is_valid 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 +850,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 +891,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,

View File

@@ -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()
@@ -290,6 +296,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
do: update_connection(map_id, :update_custom_info, [:custom_info], connection_update)
def cleanup_connections(map_id) do
# Defensive check: Skip cleanup if cache appears invalid
# This prevents incorrectly deleting connections when cache is empty due to
# race conditions during map restart or cache corruption
case WandererApp.Map.get_map(map_id) do
{:error, :not_found} ->
Logger.warning(
"[cleanup_connections] Skipping map #{map_id} - cache miss detected, " <>
"map data not found in cache"
)
:telemetry.execute(
[:wanderer_app, :map, :cleanup_connections, :cache_miss],
%{system_time: System.system_time()},
%{map_id: map_id}
)
:ok
{:ok, _map} ->
do_cleanup_connections(map_id)
end
end
defp do_cleanup_connections(map_id) do
connection_auto_expire_hours = get_connection_auto_expire_hours()
connection_auto_eol_hours = get_connection_auto_eol_hours()
connection_eol_expire_timeout_hours = get_eol_expire_timeout_mins() / 60
@@ -343,6 +373,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 +454,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 +695,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 +757,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 +823,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 +831,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 +1020,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

View File

@@ -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,

View File

@@ -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,

View File

@@ -256,6 +256,37 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
defp maybe_update_connection_mass_status(_map_id, _old_sig, _updated_sig), do: :ok
@doc """
Wrapper for updating a signature's linked_system_id with logging.
Logs all unlink operations (when linked_system_id is set to nil) with context
to help diagnose unexpected unlinking issues.
"""
def update_signature_linked_system(signature, %{linked_system_id: nil} = params) do
# Log all unlink operations with context for debugging
Logger.warning(
"[Signature Unlink] eve_id=#{signature.eve_id} " <>
"system_id=#{signature.system_id} " <>
"old_linked_system_id=#{signature.linked_system_id} " <>
"stacktrace=#{format_stacktrace()}"
)
MapSystemSignature.update_linked_system(signature, params)
end
def update_signature_linked_system(signature, params) do
MapSystemSignature.update_linked_system(signature, params)
end
defp format_stacktrace do
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
stacktrace
|> Enum.take(10)
|> Enum.map_join(" <- ", fn {mod, fun, arity, _} ->
"#{inspect(mod)}.#{fun}/#{arity}"
end)
end
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
ActivityTracker.track_map_event(event, %{
map_id: map_id,

View File

@@ -4,6 +4,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
require Logger
alias WandererApp.Map.Server.Impl
alias WandererApp.Map.Server.SignaturesImpl
@ddrt Application.compile_env(:wanderer_app, :ddrt)
@system_auto_expire_minutes 15
@@ -129,8 +130,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)
@@ -146,6 +147,30 @@ defmodule WandererApp.Map.Server.SystemsImpl do
end
def cleanup_systems(map_id) do
# Defensive check: Skip cleanup if cache appears invalid
# This prevents incorrectly deleting systems when cache is empty due to
# race conditions during map restart or cache corruption
case WandererApp.Map.get_map(map_id) do
{:error, :not_found} ->
Logger.warning(
"[cleanup_systems] Skipping map #{map_id} - cache miss detected, " <>
"map data not found in cache"
)
:telemetry.execute(
[:wanderer_app, :map, :cleanup_systems, :cache_miss],
%{system_time: System.system_time()},
%{map_id: map_id}
)
:ok
{:ok, _map} ->
do_cleanup_systems(map_id)
end
end
defp do_cleanup_systems(map_id) do
expired_systems =
map_id
|> WandererApp.Map.list_systems!()
@@ -309,7 +334,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 +408,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 +428,77 @@ 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
# Use the wrapper to log unlink operations
case SignaturesImpl.update_signature_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 +523,47 @@ 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) ->
# First check: does the location directly match scopes?
if ConnectionsImpl.can_add_location(scopes, location.solar_system_id) do
true
else
# Second check: wormhole border behavior
# If :wormholes scope is enabled AND old_location is a wormhole,
# allow this system to be added as a border system (so you can see
# where your wormhole exits to)
:wormholes in scopes and
not is_nil(old_location) and
ConnectionsImpl.can_add_location([:wormholes], old_location.solar_system_id)
end
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 +642,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 +771,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 +795,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 +862,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 +898,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 +989,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 +1104,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

View File

@@ -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}

View File

@@ -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})

View File

@@ -38,6 +38,4 @@ defmodule WandererApp.MapPingsRepo do
:ok
end
def destroy(_ping_id), do: :ok
end

View File

@@ -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}

View File

@@ -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],

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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 =

View File

@@ -23,6 +23,7 @@ defmodule WandererAppWeb.Layouts do
attr :app_version, :string
attr :enabled, :boolean
attr :latest_post, :any, default: nil
def new_version_banner(assigns) do
~H"""
@@ -36,27 +37,89 @@ defmodule WandererAppWeb.Layouts do
>
<div class="hs-overlay-backdrop transition duration absolute left-0 top-0 w-full h-full bg-gray-900 bg-opacity-50 dark:bg-opacity-80 dark:bg-neutral-900">
</div>
<div class="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex items-center">
<div class="rounded w-9 h-9 w-[80px] h-[66px] flex items-center justify-center relative z-20">
<.icon name="hero-chevron-double-right" class="w-9 h-9 mr-[-40px]" />
</div>
<div id="refresh-area">
<.live_component module={WandererAppWeb.MapRefresh} id="map-refresh" />
<div class="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-6">
<div class="flex items-center">
<div class="rounded w-9 h-9 w-[80px] h-[66px] flex items-center justify-center relative z-20">
<.icon name="hero-chevron-double-right" class="w-9 h-9 mr-[-40px]" />
</div>
<div id="refresh-area">
<.live_component module={WandererAppWeb.MapRefresh} id="map-refresh" />
</div>
<div class="rounded h-[66px] flex items-center justify-center relative z-20">
<div class=" flex items-center w-[200px] h-full">
<.icon name="hero-chevron-double-left" class="w-9 h-9 mr-[20px]" />
<div class=" flex flex-col items-center justify-center h-full">
<div class="text-white text-nowrap text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
Update Required
</div>
<a
href="/changelog"
target="_blank"
class="text-sm link-secondary [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
>
What's new?
</a>
</div>
</div>
</div>
</div>
<div class="rounded h-[66px] flex items-center justify-center relative z-20">
<div class=" flex items-center w-[200px] h-full">
<.icon name="hero-chevron-double-left" class="w-9 h-9 mr-[20px]" />
<div class=" flex flex-col items-center justify-center h-full">
<div class="text-white text-nowrap text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
Update Required
<div class="flex flex-row gap-6 z-20">
<div
:if={@latest_post}
class="bg-gray-800/80 rounded-lg overflow-hidden min-w-[300px] backdrop-blur-sm border border-gray-700"
>
<a href={"/news/#{@latest_post.id}"} target="_blank" class="block group/post">
<div class="relative">
<img
src={@latest_post.cover_image_uri}
class="w-[300px] h-[140px] object-cover opacity-80 group-hover/post:opacity-100 transition-opacity"
/>
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black/70">
</div>
<div class="absolute top-2 left-2 flex items-center gap-1 bg-orange-500/90 px-2 py-0.5 rounded text-xs font-semibold">
<.icon name="hero-newspaper-solid" class="w-3 h-3" />
<span>Latest News</span>
</div>
<div class="absolute bottom-0 left-0 w-full p-3">
<% [first_part | rest] = String.split(@latest_post.title, ":", parts: 2) %>
<h3 class="text-white text-sm font-bold ccp-font [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
{first_part}
</h3>
<p
:if={rest != []}
class="text-gray-200 text-xs ccp-font text-ellipsis overflow-hidden whitespace-nowrap [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
>
{List.first(rest)}
</p>
</div>
</div>
</a>
</div>
<div class="bg-gray-800/80 rounded-lg p-4 min-w-[280px] backdrop-blur-sm border border-gray-700">
<div class="flex items-center gap-2 mb-3">
<.icon name="hero-gift-solid" class="w-5 h-5 text-green-400" />
<span class="text-white font-semibold text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
Support Wanderer
</span>
</div>
<div class="text-gray-300 text-xs mb-3 [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
Buy PLEX from the official EVE Online store using our promocode to support the development.
</div>
<div class="flex items-center gap-3">
<code class="bg-gray-900/60 px-2 py-1 rounded text-green-400 text-sm font-mono border border-gray-600">
WANDERER
</code>
<a
href="/changelog"
href="https://www.eveonline.com/plex"
target="_blank"
class="text-sm link-secondary [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-sm text-green-400 hover:text-green-300 transition-colors"
>
What's new?
<span>Get PLEX</span>
<.icon name="hero-arrow-top-right-on-square-mini" class="w-4 h-4" />
</a>
</div>
</div>

View File

@@ -31,7 +31,11 @@
</div>
</aside>
<.new_version_banner app_version={@app_version} enabled={@map_subscriptions_enabled?} />
<.new_version_banner
app_version={@app_version}
enabled={true}
latest_post={@latest_post}
/>
</div>
<.live_component module={WandererAppWeb.Alerts} id="notifications" view_flash={@flash} />

View File

@@ -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 = %{

View File

@@ -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

View File

@@ -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.

View File

@@ -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 ->

View File

@@ -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"])
)

View File

@@ -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}

View File

@@ -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)

View File

@@ -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] ->

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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(

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,4 @@
defmodule WandererAppWeb.UserActivity do
defmodule WandererAppWeb.UserActivityItem do
use WandererAppWeb, :live_component
use LiveViewEvents

View File

@@ -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,

View File

@@ -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,

View File

@@ -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

View File

@@ -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(
%{

View File

@@ -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
@@ -363,8 +363,8 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
linked_sig_eve_id: nil
})
s
|> WandererApp.Api.MapSystemSignature.update_linked_system(%{
# Use the wrapper to log unlink operations
WandererApp.Map.Server.SignaturesImpl.update_signature_linked_system(s, %{
linked_system_id: nil
})
end)
@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -109,7 +109,7 @@
/>
</div>
<.live_component
module={UserActivity}
module={WandererAppWeb.UserActivityItem}
id="user-activity"
notify_to={self()}
can_undo_types={@can_undo_types}

View File

@@ -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,

View File

@@ -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),

View File

@@ -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)}

View File

@@ -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

View File

@@ -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

View File

@@ -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]}

View File

@@ -16,6 +16,8 @@ defmodule WandererAppWeb.Nav do
show_admin =
socket.assigns.current_user_role == :admin
latest_post = WandererApp.Blog.recent_posts(1) |> List.first()
{:cont,
socket
|> attach_hook(:active_tab, :handle_params, &set_active_tab/3)
@@ -25,7 +27,8 @@ defmodule WandererAppWeb.Nav do
show_admin: show_admin,
show_sidebar: true,
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(),
app_version: WandererApp.Env.vsn()
app_version: WandererApp.Env.vsn(),
latest_post: latest_post
)}
end

View File

@@ -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>

View File

@@ -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

View File

@@ -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 ->

View File

@@ -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 ->

View File

@@ -25,8 +25,8 @@ defmodule WandererAppWeb.PresenceGracePeriodManager do
require Logger
# 1 hour grace period before removing disconnected characters
@grace_period_ms :timer.hours(1)
# 15 minutes grace period before removing disconnected characters
@grace_period_ms :timer.minutes(15)
defstruct pending_removals: %{}, timers: %{}

View File

@@ -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

View File

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

View File

@@ -0,0 +1,68 @@
%{
title: "Christmas Giveaway Challenge",
author: "Wanderer Team",
cover_image_uri: "/images/news/2025/12-18-advent-giveaway/cover.jpg",
tags: ~w(event giveaway challenge christmas advent partnership),
description: "Join our Advent Christmas Giveaway Challenge! Win exclusive partnership codes every day for a week. Be the fastest to claim your reward!"
}
---
![Christmas Giveaway Challenge](/images/news/2025/12-18-advent-giveaway/cover.jpg "Christmas Giveaway Challenge")
### The Season of Giving
This holiday season, we're spreading some festive cheer with a special event for our community: the **Advent Christmas Giveaway Challenge**!
Starting next week, we'll be giving away **1 exclusive partnership code every day for 7 days**. But here's the twist — it's a challenge!
---
### How It Works
1. **Daily Giveaway:**
- Every day during the event week, a partnership code will be revealed at a specific scheduled time.
- The exact reveal time will be announced for each day.
2. **The Challenge:**
- When the code is revealed, it becomes visible to **all participants** at the exact same moment.
- **First person to activate the code wins!**
- Speed and timing are everything.
3. **One Code Per Day:**
- Each day features a single partnership code.
- Miss today? Come back tomorrow for another chance!
---
### Event Details
- **Event Name:** Advent Christmas Giveaway
- **Duration:** 1 week (7 days, 7 codes)
- **Organizer:** @Demiro (Wanderer core developer, EventCortex CTO)
- **Event Link:** [Advent Christmas Giveaway - EventCortex](https://eventcortex.com/events/invite/cYdBywu1ygfVS3UN6ZZcmDzL1q85aDmH)
---
### Tips for Participants
- **Be Ready:** Know the reveal time and be online a few minutes early.
- **Stay Alert:** The code appears for everyone simultaneously — every second counts!
- **Keep Trying:** Didn't win today? There's always tomorrow's code.
---
### Why Participate?
Partnership codes can be redeemed in EVE Online for **exclusive partnership SKINs** — unique ship skins that let you fly in style! This is your chance to grab one for free — if you're fast enough!
Good luck, and may the fastest capsuleer win!
---
Fly safe and happy holidays,
**The Wanderer Team**
---

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -1,5 +1,5 @@
{
"ash_functions_version": 4,
"ash_functions_version": 5,
"installed": [
"ash-functions"
]

Some files were not shown because too many files have changed in this diff Show More