Compare commits

...

54 Commits

Author SHA1 Message Date
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
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
CI
cfac867c0a chore: release version v1.89.2 2025-11-30 18:07:05 +00:00
Dmitry Popov
f50ea40b15 chore: updated tests for tracking 2025-11-30 19:06:15 +01:00
CI
04b2d57081 chore: [skip ci] 2025-11-30 17:52:21 +00:00
CI
b235ea52e0 chore: release version v1.89.1 2025-11-30 17:52:21 +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
f3c38ba62a fix(core): fixed tracking issues 2025-11-30 18:51:50 +01:00
CI
29473f2d3b chore: [skip ci] 2025-11-30 10:00:35 +00:00
CI
48654250e8 chore: release version v1.89.0 2025-11-30 10:00:35 +00:00
Aleksei Chichenkov
7aa24245b6 Merge pull request #564 from wanderer-industries/sig-panel
Sig panel
2025-11-30 13:00:08 +03:00
DanSylvest
6070d74684 feat: removed unnecessary command 2025-11-30 12:57:14 +03: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
CI
5c513f3e50 chore: [skip ci] 2025-11-29 19:13:30 +00:00
CI
5a980c6b89 chore: release version v1.88.13 2025-11-29 19:13:30 +00:00
Dmitry Popov
85c075c5a6 fix(core): fixed tracking issues 2025-11-29 20:12:54 +01:00
DanSylvest
f068afd16e Merge branch 'refs/heads/main' into sig-panel 2025-11-29 21:09:29 +03:00
DanSylvest
ac71b0af64 feat: rework wormholes reference 2025-11-29 21:07:48 +03:00
DanSylvest
5c515d6acd Merge remote-tracking branch 'leesolway/sig-panel-pr' into sig-panel
# Conflicts:
#	assets/js/hooks/Mapper/mapRootProvider/hooks/useMapRootHandlers.ts
2025-11-29 17:32:35 +03:00
Dmitry Popov
4585c3a94b feat(core): Added several map scopes support (Wh, Hi, Low, Null, Pochven) 2025-11-29 14:36:45 +01:00
CI
cf2c27c961 chore: [skip ci] 2025-11-29 11:35:52 +00:00
CI
f8e403025c chore: release version v1.88.12 2025-11-29 11:35:52 +00: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
25fa7c07bc fix(core): fixed c4 -> ns connections auto size issues 2025-11-29 12:35:22 +01:00
Dmitry Popov
e7219e0eec chore: fixed compile warnings 2025-11-29 12:34:28 +01:00
CI
45130fcffa chore: [skip ci] 2025-11-29 09:16:34 +00:00
CI
5f75d4440d chore: release version v1.88.11 2025-11-29 09:16:34 +00:00
Dmitry Popov
34210f63e3 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-29 10:16:02 +01:00
Dmitry Popov
5f60fd4922 chore: fix tests workflow 2025-11-29 10:15:59 +01:00
CI
47ef7dda55 chore: [skip ci] 2025-11-29 00:15:17 +00:00
CI
0f3550a687 chore: release version v1.88.10 2025-11-29 00:15:17 +00:00
Dmitry Popov
8f242f3535 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-11-29 01:14:21 +01:00
Dmitry Popov
1ce39e5394 fix(core): fixed pings cleanup 2025-11-29 01:14:17 +01:00
CI
cca7b912aa chore: [skip ci] 2025-11-29 00:11:43 +00:00
Lee Solway
be7bbe6872 Create a signature list panel + hook into live events 2025-10-04 12:04:02 +01:00
128 changed files with 5205 additions and 728 deletions

View File

@@ -19,19 +19,15 @@ env:
jobs:
test:
name: Test Suite (Partition ${{ matrix.partition }})
name: Test Suite
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
partition: [1, 2, 3, 4]
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: wanderer_test${{ matrix.partition }}
POSTGRES_DB: wanderer_test
options: >-
--health-cmd pg_isready
--health-interval 10s
@@ -43,13 +39,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Elixir/OTP
uses: erlef/setup-beam@v1
with:
elixir-version: ${{ env.ELIXIR_VERSION }}
otp-version: ${{ env.OTP_VERSION }}
- name: Cache Elixir dependencies
uses: actions/cache@v3
with:
@@ -58,12 +54,12 @@ jobs:
_build
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
restore-keys: ${{ runner.os }}-mix-
- name: Install Elixir dependencies
run: |
mix deps.get
mix deps.compile
- name: Check code formatting
id: format
run: |
@@ -75,42 +71,40 @@ jobs:
echo "count=1" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Compile code and capture warnings
id: compile
run: |
# Capture compilation output
output=$(mix compile 2>&1 || true)
echo "$output" > compile_output.txt
# Count warnings
warning_count=$(echo "$output" | grep -c "warning:" || echo "0")
# Check if compilation succeeded
if mix compile > /dev/null 2>&1; then
echo "status=✅ Success" >> $GITHUB_OUTPUT
else
echo "status=❌ Failed" >> $GITHUB_OUTPUT
fi
echo "warnings=$warning_count" >> $GITHUB_OUTPUT
echo "output<<EOF" >> $GITHUB_OUTPUT
echo "$output" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Setup database
run: |
mix ecto.create
mix ecto.migrate
- name: Run tests with coverage
id: tests
env:
MIX_TEST_PARTITION: ${{ matrix.partition }}
run: |
# Run tests with coverage using partitioning
output=$(mix test --cover --partitions 4 2>&1 || true)
# Run tests with coverage
output=$(mix test --cover 2>&1 || true)
echo "$output" > test_output.txt
# Parse test results
@@ -142,22 +136,22 @@ jobs:
exit_code=$?
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Generate coverage report
id: coverage
run: |
# Generate coverage report with GitHub format
output=$(mix coveralls.github 2>&1 || true)
echo "$output" > coverage_output.txt
# Extract coverage percentage
coverage=$(echo "$output" | grep -o '[0-9]\+\.[0-9]\+%' | head -1 | sed 's/%//' || echo "0")
if [ -z "$coverage" ]; then
coverage="0"
fi
echo "percentage=$coverage" >> $GITHUB_OUTPUT
# Determine status
if (( $(echo "$coverage >= 80" | bc -l) )); then
echo "status=✅ Excellent" >> $GITHUB_OUTPUT
@@ -167,14 +161,14 @@ jobs:
echo "status=❌ Needs Improvement" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Run Credo analysis
id: credo
run: |
# Run Credo and capture output
output=$(mix credo --strict --format=json 2>&1 || true)
echo "$output" > credo_output.txt
# Try to parse JSON output
if echo "$output" | jq . > /dev/null 2>&1; then
issues=$(echo "$output" | jq '.issues | length' 2>/dev/null || echo "0")
@@ -189,12 +183,12 @@ jobs:
normal_issues="0"
low_issues="0"
fi
echo "total_issues=$issues" >> $GITHUB_OUTPUT
echo "high_issues=$high_issues" >> $GITHUB_OUTPUT
echo "normal_issues=$normal_issues" >> $GITHUB_OUTPUT
echo "low_issues=$low_issues" >> $GITHUB_OUTPUT
# Determine status
if [ "$issues" -eq 0 ]; then
echo "status=✅ Clean" >> $GITHUB_OUTPUT
@@ -204,24 +198,24 @@ jobs:
echo "status=❌ Needs Attention" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Run Dialyzer analysis
id: dialyzer
run: |
# Ensure PLT is built
mix dialyzer --plt
# Run Dialyzer and capture output
output=$(mix dialyzer --format=github 2>&1 || true)
echo "$output" > dialyzer_output.txt
# Count warnings and errors
warnings=$(echo "$output" | grep -c "warning:" || echo "0")
errors=$(echo "$output" | grep -c "error:" || echo "0")
echo "warnings=$warnings" >> $GITHUB_OUTPUT
echo "errors=$errors" >> $GITHUB_OUTPUT
# Determine status
if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then
echo "status=✅ Clean" >> $GITHUB_OUTPUT
@@ -231,7 +225,7 @@ jobs:
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Create test results summary
id: summary
run: |
@@ -242,11 +236,11 @@ jobs:
coverage_score=${{ steps.coverage.outputs.percentage }}
credo_score=$(echo "scale=0; (100 - ${{ steps.credo.outputs.total_issues }} * 2)" | bc | sed 's/^-.*$/0/')
dialyzer_score=$(echo "scale=0; (100 - ${{ steps.dialyzer.outputs.warnings }} * 2 - ${{ steps.dialyzer.outputs.errors }} * 10)" | bc | sed 's/^-.*$/0/')
overall_score=$(echo "scale=1; ($format_score + $compile_score + $test_score + $coverage_score + $credo_score + $dialyzer_score) / 6" | bc)
echo "overall_score=$overall_score" >> $GITHUB_OUTPUT
# Determine overall status
if (( $(echo "$overall_score >= 90" | bc -l) )); then
echo "overall_status=🌟 Excellent" >> $GITHUB_OUTPUT
@@ -258,7 +252,7 @@ jobs:
echo "overall_status=❌ Poor" >> $GITHUB_OUTPUT
fi
continue-on-error: true
- name: Find existing PR comment
if: github.event_name == 'pull_request'
id: find_comment
@@ -267,7 +261,7 @@ jobs:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: '## 🧪 Test Results Summary'
- name: Create or update PR comment
if: github.event_name == 'pull_request'
uses: peter-evans/create-or-update-comment@v4
@@ -277,11 +271,11 @@ jobs:
edit-mode: replace
body: |
## 🧪 Test Results Summary
**Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }}
### 📊 Metrics Dashboard
| Category | Status | Count | Details |
|----------|---------|-------|---------|
| 📝 **Code Formatting** | ${{ steps.format.outputs.status }} | ${{ steps.format.outputs.count }} issues | `mix format --check-formatted` |
@@ -290,50 +284,50 @@ jobs:
| 📊 **Coverage** | ${{ steps.coverage.outputs.status }} | ${{ steps.coverage.outputs.percentage }}% | `mix coveralls` |
| 🎯 **Credo** | ${{ steps.credo.outputs.status }} | ${{ steps.credo.outputs.total_issues }} issues | High: ${{ steps.credo.outputs.high_issues }}, Normal: ${{ steps.credo.outputs.normal_issues }}, Low: ${{ steps.credo.outputs.low_issues }} |
| 🔍 **Dialyzer** | ${{ steps.dialyzer.outputs.status }} | ${{ steps.dialyzer.outputs.errors }} errors, ${{ steps.dialyzer.outputs.warnings }} warnings | `mix dialyzer` |
### 🎯 Quality Gates
Based on the project's quality thresholds:
- **Compilation Warnings**: ${{ steps.compile.outputs.warnings }}/148 (limit: 148)
- **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87)
- **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87)
- **Dialyzer Warnings**: ${{ steps.dialyzer.outputs.warnings }}/161 (limit: 161)
- **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%)
- **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0)
<details>
<summary>📈 Progress Toward Goals</summary>
Target goals for the project:
- ✨ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }})
- ✨ **≤10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }})
- ✨ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }})
- ✨ **≥85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%)
- ✅ **Zero test failures** (currently: ${{ steps.tests.outputs.failures }})
</details>
<details>
<summary>🔧 Quick Actions</summary>
To improve code quality:
```bash
# Fix formatting issues
mix format
# View detailed Credo analysis
mix credo --strict
# Check Dialyzer warnings
mix dialyzer
# Generate detailed coverage report
mix coveralls.html
```
</details>
---
🤖 *Auto-generated by GitHub Actions* • Updated: ${{ github.event.head_commit.timestamp }}
> **Note**: This comment will be updated automatically when new commits are pushed to this PR.
> **Note**: This comment will be updated automatically when new commits are pushed to this PR.

View File

@@ -2,6 +2,86 @@
<!-- changelog -->
## [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)
## [v1.89.1](https://github.com/wanderer-industries/wanderer/compare/v1.89.0...v1.89.1) (2025-11-30)
### Bug Fixes:
* core: fixed tracking issues
## [v1.89.0](https://github.com/wanderer-industries/wanderer/compare/v1.88.13...v1.89.0) (2025-11-30)
### Features:
* removed unnecessary command
* rework wormholes reference
## [v1.88.13](https://github.com/wanderer-industries/wanderer/compare/v1.88.12...v1.88.13) (2025-11-29)
### Bug Fixes:
* core: fixed tracking issues
## [v1.88.12](https://github.com/wanderer-industries/wanderer/compare/v1.88.11...v1.88.12) (2025-11-29)
### Bug Fixes:
* core: fixed c4 -> ns connections auto size issues
## [v1.88.11](https://github.com/wanderer-industries/wanderer/compare/v1.88.10...v1.88.11) (2025-11-29)
## [v1.88.10](https://github.com/wanderer-industries/wanderer/compare/v1.88.9...v1.88.10) (2025-11-29)
### Bug Fixes:
* core: fixed pings cleanup
## [v1.88.9](https://github.com/wanderer-industries/wanderer/compare/v1.88.8...v1.88.9) (2025-11-29)

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

@@ -9,6 +9,7 @@ import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/compone
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity';
import { WormholeSignaturesDialog } from '@/hooks/Mapper/components/mapRootContent/components/WormholeSignaturesDialog';
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
import { useMapEventListener } from '@/hooks/Mapper/events';
@@ -34,6 +35,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const [showOnTheMap, setShowOnTheMap] = useState(false);
const [showMapSettings, setShowMapSettings] = useState(false);
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
const [showWormholeList, setShowWormholeList] = useState(false);
/* Important Notice - this solution needs for use one instance of MapInterface */
const mapInterface = isReady ? <MapInterface /> : null;
@@ -41,6 +43,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
const handleShowTrackingDialog = useCallback(() => setShowTrackingDialog(true), []);
const handleShowWormholesReference = useCallback(() => setShowWormholeList(true), []);
useMapEventListener(event => {
if (event.name === Commands.showTracking) {
@@ -65,6 +68,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog}
onShowWormholesReference={handleShowWormholesReference}
additionalContent={<PingsInterface hasLeftOffset />}
/>
</div>
@@ -79,6 +83,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
onShowOnTheMap={handleShowOnTheMap}
onShowMapSettings={handleShowMapSettings}
onShowTrackingDialog={handleShowTrackingDialog}
onShowWormholesReference={handleShowWormholesReference}
/>
</div>
</Topbar>
@@ -93,6 +98,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
{showTrackingDialog && (
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
)}
<WormholeSignaturesDialog visible={showWormholeList} onHide={() => setShowWormholeList(false)} />
{hasOldSettings && <OldSettingsDialog />}
</Layout>

View File

@@ -12,9 +12,15 @@ export interface MapContextMenuProps {
onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
onShowTrackingDialog?: () => void;
onShowWormholesReference?: () => void;
}
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: MapContextMenuProps) => {
export const MapContextMenu = ({
onShowOnTheMap,
onShowMapSettings,
onShowTrackingDialog,
onShowWormholesReference,
}: MapContextMenuProps) => {
const {
outCommand,
storedSettings: { setInterfaceSettings },
@@ -52,6 +58,12 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTracki
command: onShowOnTheMap,
visible: canTrackCharacters,
},
{
label: 'Wormholes Ref.',
icon: 'pi pi-bullseye',
command: onShowWormholesReference,
visible: canTrackCharacters,
},
{ separator: true, visible: true },
{
label: 'Settings',

View File

@@ -14,6 +14,7 @@ interface RightBarProps {
onShowOnTheMap?: () => void;
onShowMapSettings?: () => void;
onShowTrackingDialog?: () => void;
onShowWormholesReference?: () => void;
additionalContent?: ReactNode;
}
@@ -21,6 +22,7 @@ export const RightBar = ({
onShowOnTheMap,
onShowMapSettings,
onShowTrackingDialog,
onShowWormholesReference,
additionalContent,
}: RightBarProps) => {
const {
@@ -90,6 +92,16 @@ export const RightBar = ({
<i className="pi pi-hashtag"></i>
</button>
</WdTooltipWrapper>
<WdTooltipWrapper content="Wormholes Reference" position={TooltipPosition.left}>
<button
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
type="button"
onClick={onShowWormholesReference}
>
<i className="pi pi-bullseye"></i>
</button>
</WdTooltipWrapper>
</div>
</>
)}

View File

@@ -1,8 +1,9 @@
import { createContext, useCallback, useContext, useRef, useState } from 'react';
import { OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
import { createContext, useCallback, useContext, useRef, useState, useEffect } from 'react';
import { Commands, OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
import { CommandInCharactersTrackingInfo } from '@/hooks/Mapper/types/commandsIn.ts';
import { useMapEventListener } from '@/hooks/Mapper/events';
type DiffTrackingInfo = { characterId: string; tracked: boolean };
@@ -122,6 +123,14 @@ export const TrackingProvider = ({ children }: WithChildren) => {
[outCommand],
);
// Listen for refresh_tracking_data event (triggered when ACL members change)
useMapEventListener(event => {
if (event.name === Commands.refreshTrackingData) {
loadTracking();
return true;
}
});
return (
<TrackingContext.Provider
value={{

View File

@@ -0,0 +1,170 @@
import { useMemo, useState } from 'react';
import { Dialog } from 'primereact/dialog';
import { DataTable } from 'primereact/datatable';
import { Column } from 'primereact/column';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { WormholeDataRaw } from '@/hooks/Mapper/types';
import { RespawnTag, WHClassView } from '@/hooks/Mapper/components/ui-kit';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
import clsx from 'clsx';
import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
import { InputIcon } from 'primereact/inputicon';
const renderSpawns = (w: WormholeDataRaw) => (
<div className="flex gap-1 flex-wrap">
{w.src.map(s => {
const group = s.split('-')[0];
const info = WORMHOLES_ADDITIONAL_INFO[group];
if (!info) {
return (
<span
key={s}
className="px-[4px] py-[1px] rounded bg-stone-800 text-stone-300 text-xs border border-stone-700"
>
{s}
</span>
);
}
const cls = WORMHOLE_CLASS_STYLES[String(info.wormholeClassID)] || '';
const label = `${info.shortName}`;
return (
<span
key={s}
className={clsx(cls, 'px-[4px] py-[1px] rounded text-xs border border-stone-700 bg-stone-900/40')}
>
{label}
</span>
);
})}
</div>
);
const renderName = (w: WormholeDataRaw) => (
<div className="flex items-center gap-2">
<WHClassView
whClassName={w.name}
noOffset
useShortTitle
classNameWh="overflow-hidden text-ellipsis whitespace-nowrap"
/>
</div>
);
const renderRespawn = (w: WormholeDataRaw) => (
<div className="flex gap-1 flex-wrap">
{w.respawn.map(r => (
<RespawnTag key={r} value={r} />
))}
</div>
);
export interface WormholeSignaturesDialogProps {
visible: boolean;
onHide: () => void;
}
export const WormholeSignaturesDialog = ({ visible, onHide }: WormholeSignaturesDialogProps) => {
const {
data: { wormholes },
} = useMapRootState();
const [filter, setFilter] = useState('');
const filtered = useMemo(() => {
const q = filter.trim().toLowerCase();
if (!q) return wormholes;
return wormholes.filter(w => {
const destInfo = WORMHOLES_ADDITIONAL_INFO[w.dest];
const spawnsLabels = w.src
.map(s => {
const group = s.split('-')[0];
const info = WORMHOLES_ADDITIONAL_INFO[group];
if (!info) return s;
return `${info.title} ${info.shortName}`.trim();
})
.join(' ');
return [
w.name,
destInfo?.title,
destInfo?.shortName,
spawnsLabels,
String(w.total_mass),
String(w.max_mass_per_jump),
w.lifetime,
w.respawn.join(','),
]
.filter(Boolean)
.join(' ')
.toLowerCase()
.includes(q);
});
}, [wormholes, filter]);
return (
<Dialog
header="Wormholes Reference"
visible={visible}
draggable={false}
resizable={false}
className="w-[950px] h-[600px]"
onHide={onHide}
contentClassName="!p-0 flex flex-col h-full"
>
<div className="p-3 flex items-center justify-between gap-2 border-b border-stone-800">
<div className="font-semibold text-sm text-stone-200">Reference list of all wormhole types</div>
<IconField iconPosition="right">
<InputIcon
className={clsx('pi pi-times', {
['cursor-pointer text-stone-400 hover:text-stone-200']: filter,
['text-stone-700 opacity-50 cursor-default']: !filter,
})}
onClick={() => filter && setFilter('')}
role="button"
aria-label="Clear search"
aria-disabled={!filter}
title={filter ? 'Clear' : 'Nothing to clear'}
/>
<InputText className="w-64" placeholder="Search" value={filter} onChange={e => setFilter(e.target.value)} />
</IconField>
</div>
<div className="flex-1 p-3 overflow-x-hidden">
<DataTable value={filtered} size="small" scrollable scrollHeight="flex" stripedRows>
<Column header="Type" body={renderName} className="w-[160px]" bodyClassName="whitespace-normal break-words" />
<Column header="Spawns In" body={renderSpawns} bodyClassName="whitespace-normal break-words text-[13px]" />
<Column
field="lifetime"
header="Lifetime"
className="w-[90px]"
bodyClassName="whitespace-normal break-words text-[13px]"
/>
<Column
header="Total Mass"
className="w-[120px]"
body={(w: WormholeDataRaw) => kgToTons(w.total_mass)}
bodyClassName="whitespace-normal break-words text-[13px]"
/>
<Column
header="Max/jump"
className="w-[120px]"
body={(w: WormholeDataRaw) => kgToTons(w.max_mass_per_jump)}
bodyClassName="whitespace-normal break-words text-[13px]"
/>
<Column
header="Respawn"
className="w-[150px]"
body={renderRespawn}
bodyClassName="whitespace-normal break-words text-[13px]"
/>
</DataTable>
</div>
</Dialog>
);
};

View File

@@ -0,0 +1 @@
export * from './WormholeSignaturesDialog';

View File

@@ -0,0 +1,20 @@
import { Respawn } from '@/hooks/Mapper/types';
import clsx from 'clsx';
export const WORMHOLE_SPAWN_CLASSES_BG = {
[Respawn.static]: 'bg-lime-400/80 text-stone-950',
[Respawn.wandering]: 'bg-stone-800',
[Respawn.reverse]: 'bg-blue-400 text-stone-950',
};
type RespawnTagProps = { value: string };
export const RespawnTag = ({ value }: RespawnTagProps) => (
<span
className={clsx(
'px-[6px] py-[0px] rounded text-stone-300 text-[12px] font-[500] border border-stone-700',
WORMHOLE_SPAWN_CLASSES_BG[value as Respawn],
)}
>
{value}
</span>
);

View File

@@ -23,3 +23,4 @@ export * from './MenuItemWithInfo';
export * from './MarkdownTextViewer.tsx';
export * from './WdButton.tsx';
export * from './constants.ts';
export * from './RespawnTag';

View File

@@ -63,127 +63,122 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
const { pingAdded, pingCancelled } = useCommandPings();
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
useImperativeHandle(
ref,
() => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.userRoutes:
mapUserRoutes(data as CommandRoutes);
break;
useImperativeHandle(ref, () => {
return {
command(type, data) {
switch (type) {
case Commands.init: // USED
mapInit(data as CommandInit);
break;
case Commands.addSystems: // USED
addSystems(data as CommandAddSystems);
break;
case Commands.updateSystems: // USED
updateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems: // USED
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections: // USED
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections: // USED
removeConnections(data as CommandRemoveConnections);
break;
case Commands.updateConnection: // USED
updateConnection(data as CommandUpdateConnection);
break;
case Commands.charactersUpdated: // USED
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded: // USED
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved: // USED
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated: // USED
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters: // USED
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.mapUpdated: // USED
mapUpdated(data as CommandMapUpdated);
break;
case Commands.routes:
mapRoutes(data as CommandRoutes);
break;
case Commands.userRoutes:
mapUserRoutes(data as CommandRoutes);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.signaturesUpdated: // USED
updateSystemSignatures(data as CommandSignaturesUpdated);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.linkSignatureToSystem: // USED
setTimeout(() => {
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
}, 200);
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.centerSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.selectSystem: // USED
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.killsUpdated:
// do nothing here
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.detailedKillsUpdated:
updateDetailedKills(data as Record<string, DetailedKill[]>);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.characterActivityData:
characterActivityData(data as CommandCharacterActivityData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.trackingCharactersData:
trackingCharactersData(data as CommandTrackingCharactersData);
break;
case Commands.updateActivity:
break;
case Commands.updateActivity:
break;
case Commands.updateTracking:
break;
case Commands.updateTracking:
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.userSettingsUpdated:
userSettingsUpdated(data as CommandUserSettingsUpdated);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentAdded:
addComment(data as CommandCommentAdd);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
case Commands.systemCommentRemoved:
removeComment(data as CommandCommentRemoved);
break;
case Commands.pingAdded:
pingAdded(data as CommandPingAdded);
break;
case Commands.pingAdded:
pingAdded(data as CommandPingAdded);
break;
case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled);
break;
case Commands.pingCancelled:
pingCancelled(data as CommandPingCancelled);
break;
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
default:
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
break;
}
emitMapEvent({ name: type, data });
},
};
},
[],
);
emitMapEvent({ name: type, data });
},
};
}, []);
};

View File

@@ -38,6 +38,7 @@ export enum Commands {
updateTracking = 'update_tracking',
userSettingsUpdated = 'user_settings_updated',
showTracking = 'show_tracking',
refreshTrackingData = 'refresh_tracking_data',
pingAdded = 'ping_added',
pingCancelled = 'ping_cancelled',
}
@@ -74,6 +75,7 @@ export type Command =
| Commands.updateActivity
| Commands.updateTracking
| Commands.showTracking
| Commands.refreshTrackingData
| Commands.pingAdded
| Commands.pingCancelled;
@@ -145,6 +147,7 @@ export type CommandUserSettingsUpdated = {
};
export type CommandShowTracking = null;
export type CommandRefreshTrackingData = Record<string, never>;
export type CommandUpdateActivity = {
characterId: number;
systemId: number;
@@ -206,6 +209,7 @@ export interface CommandData {
[Commands.systemCommentRemoved]: CommandCommentRemoved;
[Commands.systemCommentsUpdated]: unknown;
[Commands.showTracking]: CommandShowTracking;
[Commands.refreshTrackingData]: CommandRefreshTrackingData;
[Commands.pingAdded]: CommandPingAdded;
[Commands.pingCancelled]: CommandPingCancelled;
}

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
@@ -151,10 +155,23 @@ defmodule WandererApp.Character.TrackerManager.Impl do
remove_from_untrack_queue(map_id, character_id)
{:ok, character_state} =
WandererApp.Character.Tracker.update_settings(character_id, track_settings)
case WandererApp.Character.Tracker.update_settings(character_id, track_settings) do
{:ok, character_state} ->
WandererApp.Character.update_character_state(character_id, character_state)
WandererApp.Character.update_character_state(character_id, character_state)
{:error, :not_found} ->
# Tracker process not running yet - this is expected during initial tracking setup
# The tracking_start_time cache key was already set by TrackingUtils.track_character
Logger.debug(fn ->
"[TrackerManager] Tracker not yet running for character #{character_id} - " <>
"tracking will be active via cache key"
end)
{:error, reason} ->
Logger.warning(fn ->
"[TrackerManager] Failed to update settings for character #{character_id}: #{inspect(reason)}"
end)
end
else
Logger.debug(fn ->
"[TrackerManager] Queuing character #{character_id} for untracking from map #{map_id} - " <>
@@ -184,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

@@ -53,24 +53,27 @@ defmodule WandererApp.Character.TrackingUtils do
@doc """
Builds tracking data for all characters with access to a map.
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
# Filter to only characters with actual tracking permission
characters_with_tracking_permission =
filter_characters_with_tracking_permission(characters_with_access, map)
# Map characters to tracking data
{:ok, characters_data} =
build_character_tracking_data(characters_with_access)
build_character_tracking_data(characters_with_tracking_permission)
{:ok, main_character} =
get_main_character(user_settings, characters_with_access, characters_with_access)
get_main_character(
user_settings,
characters_with_tracking_permission,
characters_with_tracking_permission
)
following_character_eve_id =
case user_settings do
@@ -112,6 +115,70 @@ defmodule WandererApp.Character.TrackingUtils do
end)}
end
# Filter characters to only include those with actual tracking permission
# This prevents showing characters in the tracking dialog that will fail when toggled
defp filter_characters_with_tracking_permission(characters, %{id: map_id, owner_id: owner_id}) do
# Load ACLs with members properly (same approach as get_map_characters)
acls = load_map_acls_with_members(map_id)
Enum.filter(characters, fn character ->
has_tracking_permission?(character, owner_id, acls)
end)
end
# Load ACLs with members in the correct format for permission checking
defp load_map_acls_with_members(map_id) do
case WandererApp.Api.MapAccessList.read_by_map(%{map_id: map_id},
load: [access_list: [:owner, :members]]
) do
{:ok, map_access_lists} ->
map_access_lists
|> Enum.map(fn mal -> mal.access_list end)
|> Enum.reject(&is_nil/1)
_ ->
[]
end
end
# Check if a character has tracking permission on a map
# Returns true if the character can be tracked, false otherwise
defp has_tracking_permission?(character, owner_id, acls) do
cond do
# Map owner always has tracking permission
character.id == owner_id ->
true
# Character belongs to same user as map owner
# Note: character data from load_characters may not have user_id, so we need to load it
check_same_user_as_owner_by_id(character.id, owner_id) ->
true
# Check ACL-based permissions
true ->
case WandererApp.Permissions.check_characters_access([character], acls) do
[character_permissions] ->
map_permissions = WandererApp.Permissions.get_permissions(character_permissions)
map_permissions.track_character and map_permissions.view_system
_ ->
false
end
end
end
# Check if character belongs to the same user as the map owner (by character IDs)
defp check_same_user_as_owner_by_id(_character_id, nil), do: false
defp check_same_user_as_owner_by_id(character_id, owner_id) do
with {:ok, character} <- WandererApp.Character.get_character(character_id),
{:ok, owner_character} <- WandererApp.Character.get_character(owner_id) do
character.user_id != nil and character.user_id == owner_character.user_id
else
_ -> false
end
end
# Private implementation of update character tracking
defp do_update_character_tracking(character, map_id, track, caller_pid) do
# First check current tracking state to avoid unnecessary permission checks
@@ -126,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(
@@ -212,6 +285,9 @@ defmodule WandererApp.Character.TrackingUtils do
{:ok, %{tracked: false} = existing_settings} ->
if track do
{:ok, updated_settings} = WandererApp.MapCharacterSettingsRepo.track(existing_settings)
# Ensure character is in map state (fixes race condition where character
# might not be synced yet from presence updates)
:ok = WandererApp.Map.add_character(map_id, character)
:ok = track([character], map_id, true, caller_pid)
{:ok, updated_settings}
else
@@ -228,6 +304,9 @@ defmodule WandererApp.Character.TrackingUtils do
tracked: true
})
# Add character to map state immediately (fixes race condition where
# character wouldn't appear on map until next update_presence cycle)
:ok = WandererApp.Map.add_character(map_id, character)
:ok = track([character], map_id, true, caller_pid)
{:ok, settings}
else
@@ -290,6 +369,31 @@ defmodule WandererApp.Character.TrackingUtils do
if is_track_allowed do
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
# Immediately set tracking_start_time cache key to enable map tracking
# This ensures the character is tracked for updates even before the
# Tracker process is fully started (avoids race condition)
tracking_start_key = "character:#{character_id}:map:#{map_id}:tracking_start_time"
case WandererApp.Cache.lookup(tracking_start_key) do
{:ok, nil} ->
WandererApp.Cache.put(tracking_start_key, DateTime.utc_now())
# Clear stale location caches for fresh tracking
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
_ ->
# Already tracking, no need to update
:ok
end
# Also call update_track_settings to update character state when tracker is ready
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: true
})
end
:ok

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

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

@@ -177,7 +177,7 @@ defmodule WandererApp.Map do
end
def list_hubs(map_id, hubs) do
{:ok, map} = map_id |> get_map()
{:ok, _map} = map_id |> get_map()
{:ok, hubs}
end
@@ -315,7 +315,7 @@ defmodule WandererApp.Map do
end
end
def update_subscription_settings!(%{map_id: map_id} = map, %{
def update_subscription_settings!(%{map_id: map_id} = _map, %{
characters_limit: characters_limit,
hubs_limit: hubs_limit
}) do
@@ -326,7 +326,7 @@ defmodule WandererApp.Map do
|> get_map!()
end
def update_options!(%{map_id: map_id} = map, options) do
def update_options!(%{map_id: map_id} = _map, options) do
map_id
|> update_map(%{options: options})

View File

@@ -115,11 +115,20 @@ defmodule WandererApp.Map.Manager do
Enum.each(pings, fn %{id: ping_id, map_id: map_id, type: type} = ping ->
{:ok, %{system: system}} = ping |> Ash.load([:system])
Server.Impl.broadcast!(map_id, :ping_cancelled, %{
id: ping_id,
solar_system_id: system.solar_system_id,
type: type
})
# Handle case where parent system was already deleted
case system do
nil ->
Logger.warning(
"[cleanup_expired_pings] ping #{ping_id} destroyed (parent system already deleted)"
)
%{solar_system_id: solar_system_id} ->
Server.Impl.broadcast!(map_id, :ping_cancelled, %{
id: ping_id,
solar_system_id: solar_system_id,
type: type
})
end
Ash.destroy!(ping)
end)

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

@@ -68,6 +68,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
WandererApp.Map.update_map_state(map_id, %{
map: Map.merge(old_map, map_update)
})
# Broadcast to map channel so all viewers can refresh their available characters
WandererApp.Map.Server.Impl.broadcast!(map_id, :acl_members_changed, %{})
end
def handle_acl_updated(map_id, acl_id) do
@@ -87,6 +90,10 @@ defmodule WandererApp.Map.Server.AclsImpl do
acl_id
|> update_acl()
|> broadcast_acl_updates(map_id)
# Broadcast to map channel so all viewers can refresh their available characters
# This fixes the issue where users don't see newly added ACL members as available for tracking
WandererApp.Map.Server.Impl.broadcast!(map_id, :acl_members_changed, %{acl_id: acl_id})
end
end
@@ -108,6 +115,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
|> Map.get(:characters, [])
WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids)
# Broadcast to map channel so all viewers can refresh their available characters
WandererApp.Map.Server.Impl.broadcast!(map_id, :acl_members_changed, %{})
end
def track_acls([]), do: :ok

View File

@@ -814,14 +814,16 @@ defmodule WandererApp.Map.Server.CharactersImpl do
do: :ok
defp update_location(
%{map: %{scope: scope}, map_id: map_id, map_opts: map_opts} =
%{map: map, map_id: map_id, map_opts: map_opts} =
_state,
character_id,
location,
old_location
) do
scopes = get_effective_scopes(map)
ConnectionsImpl.is_connection_valid(
scope,
scopes,
old_location.solar_system_id,
location.solar_system_id
)
@@ -879,6 +881,21 @@ 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)
# Get effective scopes from map, with fallback to legacy scope
defp get_effective_scopes(%{scopes: scopes}) when is_list(scopes) and scopes != [], do: scopes
defp get_effective_scopes(%{scope: scope}) when is_atom(scope),
do: legacy_scope_to_scopes(scope)
defp 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()
@@ -403,7 +409,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 +650,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 +712,41 @@ 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
# Connection is valid if:
# 1. Neither system is prohibited
# 2. At least one system matches one of the selected scopes
not is_prohibited_system_class?(from_system_static_info.system_class) and
not is_prohibited_system_class?(to_system_static_info.system_class) and
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
(system_matches_any_scope?(from_system_static_info.system_class, scopes) or
system_matches_any_scope?(to_system_static_info.system_class, scopes))
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 +760,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 +768,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")
@@ -859,14 +915,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
source_system_info.system_class == @c13 or target_system_info.system_class == @c13 ->
@frigate_ship_size
# C4 to null gets frigate (unless C4 is shattered)
(source_system_info.system_class == @c4 and target_system_info.system_class == @ns and
not source_system_info.is_shattered) or
(target_system_info.system_class == @c4 and
source_system_info.system_class == @ns and
not target_system_info.is_shattered) ->
@frigate_ship_size
true ->
# Default to large for other wormhole connections
@large_ship_size
@@ -909,9 +957,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

@@ -129,8 +129,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
def remove_system_comment(
map_id,
comment_id,
user_id,
character_id
_user_id,
_character_id
) do
{:ok, %{system_id: system_id} = comment} =
WandererApp.MapSystemCommentRepo.get_by_id(comment_id)
@@ -309,7 +309,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
map_id
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
|> case do
{:ok, result} ->
{:ok, _result} ->
:ok = WandererApp.Map.remove_system(map_id, solar_system_id)
@ddrt.delete([solar_system_id], "rtree_#{map_id}")
Impl.broadcast!(map_id, :systems_removed, [solar_system_id])
@@ -403,21 +403,41 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|> Enum.each(fn s ->
try do
{:ok, %{eve_id: eve_id, system: system}} = s |> Ash.load([:system])
:ok = Ash.destroy!(s)
# 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)"
# Use Ash.destroy (not destroy!) to handle already-deleted signatures gracefully
case Ash.destroy(s) do
:ok ->
# Handle case where parent system was already deleted
case system do
nil ->
Logger.debug(fn ->
"[cleanup_linked_signatures] signature #{eve_id} destroyed (parent system already deleted)"
end)
%{solar_system_id: solar_system_id} ->
Logger.debug(fn ->
"[cleanup_linked_signatures] for system #{solar_system_id}: #{inspect(eve_id)}"
end)
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
end
{:error, %Ash.Error.Invalid{errors: errors}} ->
# Check if this is a StaleRecord error (signature already deleted)
if Enum.any?(errors, &match?(%Ash.Error.Changes.StaleRecord{}, &1)) do
Logger.debug(fn ->
"[cleanup_linked_signatures] signature #{eve_id} already deleted (StaleRecord)"
end)
else
Logger.error(
"[cleanup_linked_signatures] Failed to destroy signature #{eve_id}: #{inspect(errors)}"
)
end
{:error, error} ->
Logger.error(
"[cleanup_linked_signatures] Failed to destroy signature: #{inspect(error)}"
)
%{solar_system_id: solar_system_id} ->
Logger.warning(
"[cleanup_linked_signatures] for system #{solar_system_id}: #{inspect(eve_id)}"
)
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
end
rescue
e ->
@@ -679,7 +699,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 +766,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 +802,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 +893,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

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

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

@@ -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,6 @@ defmodule WandererAppWeb.MapAuditAPIController do
require Logger
alias WandererApp.Api
alias WandererAppWeb.Helpers.APIUtils
# -----------------------------------------------------------------

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,6 +2,7 @@ defmodule WandererAppWeb.AccessListsLive do
use WandererAppWeb, :live_view
alias WandererApp.ExternalEvents.AclEventBroadcaster
require Ash.Query
require Logger
@impl true
@@ -281,11 +282,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
@@ -444,11 +441,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 +567,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 +602,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 +638,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 +669,7 @@ defmodule WandererAppWeb.AccessListsLive do
"""
end
slot(:option)
attr(:option, :any, required: true)
def search_member_item(assigns) do
~H"""
@@ -737,4 +718,31 @@ defmodule WandererAppWeb.AccessListsLive do
defp map_ui_acl(acl, selected_id) do
acl |> Map.put(:selected, acl.id == selected_id)
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

@@ -115,13 +115,20 @@
</div>
</div>
<.link
disabled={@selected_acl_id == "" or not can_add_members?(@access_list, @current_user)}
:if={@selected_acl_id != "" and can_add_members?(@access_list, @current_user)}
class="btn mt-2 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>
</main>
</div>

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

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

@@ -17,6 +17,13 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket
end
def handle_server_event(%{event: :acl_members_changed, payload: _payload}, socket) do
# ACL members have changed - notify frontend to refresh tracking data
# This ensures users see newly added characters as available for tracking
socket
|> MapEventHandler.push_map_event("refresh_tracking_data", %{})
end
def handle_server_event(
:refresh_permissions,
%{assigns: %{current_user: current_user, map_slug: map_slug}} = socket

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

@@ -5,8 +5,6 @@ defmodule WandererAppWeb.MapAuditLive do
alias WandererAppWeb.UserActivity
@active_subscription_periods ["2M", "3M"]
def mount(
%{"slug" => map_slug, "period" => period, "activity" => activity} = _params,
_session,

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

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

@@ -116,7 +116,9 @@ defmodule WandererAppWeb.PresenceGracePeriodManager do
|> Map.new()
if length(timers_to_cancel) > 0 do
Logger.debug("[PresenceGracePeriod] Cleared state for map #{map_id} - cancelled #{length(timers_to_cancel)} timers")
Logger.debug(
"[PresenceGracePeriod] Cleared state for map #{map_id} - cancelled #{length(timers_to_cancel)} timers"
)
end
new_state = %{

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.88.9"
@version "1.89.5"
def project do
[

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

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