mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-01-31 11:06:01 +00:00
Compare commits
103 Commits
v1.88.5
...
advent-cha
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1226b6abf3 | ||
|
|
7a1f5c0966 | ||
|
|
7039ced11e | ||
|
|
42b5bb337f | ||
|
|
1dbb24f6ec | ||
|
|
c242f510e0 | ||
|
|
c59d51636e | ||
|
|
c5a8aa1b4d | ||
|
|
cba050a9e7 | ||
|
|
59fcbef3b1 | ||
|
|
2f1eb6eeaa | ||
|
|
71ae326cf7 | ||
|
|
07829caf0f | ||
|
|
a5850b5a8d | ||
|
|
9f6849209b | ||
|
|
7bd295cbad | ||
|
|
078e5fc19e | ||
|
|
3877e121c3 | ||
|
|
dcb2a0cdb2 | ||
|
|
f5294eee84 | ||
|
|
a5c87b6fa4 | ||
|
|
eae275f515 | ||
|
|
68ae6706dd | ||
|
|
a34b30af15 | ||
|
|
38b49266ed | ||
|
|
049884bb4c | ||
|
|
3c75b2b59f | ||
|
|
4ad5d191a3 | ||
|
|
2499c24cc1 | ||
|
|
6f0043205c | ||
|
|
597741fa60 | ||
|
|
d313ae8cd2 | ||
|
|
06d5d8072e | ||
|
|
f2d112df5c | ||
|
|
716604fa84 | ||
|
|
cae958a1e6 | ||
|
|
283b36c882 | ||
|
|
051e71f1a6 | ||
|
|
20a50e8db0 | ||
|
|
79d7f7ce7d | ||
|
|
6c4b65c446 | ||
|
|
2b07af5e12 | ||
|
|
d0901eecb4 | ||
|
|
ee85d29c54 | ||
|
|
a237d6513d | ||
|
|
02979588c1 | ||
|
|
3abe40855f | ||
|
|
d0d9418a89 | ||
|
|
3ce742eb01 | ||
|
|
ae566fb907 | ||
|
|
fa32c62f63 | ||
|
|
6880be11c5 | ||
|
|
5289893264 | ||
|
|
f15370a3df | ||
|
|
cfac867c0a | ||
|
|
f50ea40b15 | ||
|
|
04b2d57081 | ||
|
|
b235ea52e0 | ||
|
|
2cb2dc526c | ||
|
|
f3c38ba62a | ||
|
|
29473f2d3b | ||
|
|
48654250e8 | ||
|
|
7aa24245b6 | ||
|
|
6070d74684 | ||
|
|
c3de3c4e35 | ||
|
|
5c513f3e50 | ||
|
|
5a980c6b89 | ||
|
|
85c075c5a6 | ||
|
|
f068afd16e | ||
|
|
ac71b0af64 | ||
|
|
5c515d6acd | ||
|
|
4585c3a94b | ||
|
|
cf2c27c961 | ||
|
|
f8e403025c | ||
|
|
46a1898be9 | ||
|
|
25fa7c07bc | ||
|
|
e7219e0eec | ||
|
|
45130fcffa | ||
|
|
5f75d4440d | ||
|
|
34210f63e3 | ||
|
|
5f60fd4922 | ||
|
|
47ef7dda55 | ||
|
|
0f3550a687 | ||
|
|
8f242f3535 | ||
|
|
1ce39e5394 | ||
|
|
cca7b912aa | ||
|
|
d939e32500 | ||
|
|
97ebe66db5 | ||
|
|
f437fc4541 | ||
|
|
6c65538450 | ||
|
|
d566a74df4 | ||
|
|
03e030a7d3 | ||
|
|
e738e1da9c | ||
|
|
972b3a6cbe | ||
|
|
96b4a3077e | ||
|
|
6b308e8a1e | ||
|
|
d0874cbc6f | ||
|
|
f106a51bf5 | ||
|
|
dc47dc5f81 | ||
|
|
dc81cffeea | ||
|
|
5766fcf4d8 | ||
|
|
c57a3b2cea | ||
|
|
be7bbe6872 |
110
.github/workflows/test.yml
vendored
110
.github/workflows/test.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
test:
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
@@ -35,17 +35,17 @@ jobs:
|
||||
--health-retries 5
|
||||
ports:
|
||||
- 5432:5432
|
||||
|
||||
|
||||
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:
|
||||
@@ -54,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: |
|
||||
@@ -71,42 +71,42 @@ 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
|
||||
run: |
|
||||
# Run tests with coverage
|
||||
output=$(mix test --cover 2>&1 || true)
|
||||
echo "$output" > test_output.txt
|
||||
|
||||
|
||||
# Parse test results
|
||||
if echo "$output" | grep -q "0 failures"; then
|
||||
echo "status=✅ All Passed" >> $GITHUB_OUTPUT
|
||||
@@ -115,16 +115,16 @@ jobs:
|
||||
echo "status=❌ Some Failed" >> $GITHUB_OUTPUT
|
||||
test_status="failed"
|
||||
fi
|
||||
|
||||
|
||||
# Extract test counts
|
||||
test_line=$(echo "$output" | grep -E "[0-9]+ tests?, [0-9]+ failures?" | head -1 || echo "0 tests, 0 failures")
|
||||
total_tests=$(echo "$test_line" | grep -o '[0-9]\+ tests\?' | grep -o '[0-9]\+' | head -1 || echo "0")
|
||||
failures=$(echo "$test_line" | grep -o '[0-9]\+ failures\?' | grep -o '[0-9]\+' | head -1 || echo "0")
|
||||
|
||||
|
||||
echo "total=$total_tests" >> $GITHUB_OUTPUT
|
||||
echo "failures=$failures" >> $GITHUB_OUTPUT
|
||||
echo "passed=$((total_tests - failures))" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Calculate success rate
|
||||
if [ "$total_tests" -gt 0 ]; then
|
||||
success_rate=$(echo "scale=1; ($total_tests - $failures) * 100 / $total_tests" | bc)
|
||||
@@ -132,26 +132,26 @@ jobs:
|
||||
success_rate="0"
|
||||
fi
|
||||
echo "success_rate=$success_rate" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
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
|
||||
@@ -161,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")
|
||||
@@ -183,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
|
||||
@@ -198,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
|
||||
@@ -225,7 +225,7 @@ jobs:
|
||||
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Create test results summary
|
||||
id: summary
|
||||
run: |
|
||||
@@ -236,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
|
||||
@@ -252,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
|
||||
@@ -261,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
|
||||
@@ -271,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` |
|
||||
@@ -284,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.
|
||||
|
||||
214
CHANGELOG.md
214
CHANGELOG.md
@@ -2,6 +2,220 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: skip systems or connections cleanup for not started maps
|
||||
|
||||
## [v1.90.7](https://github.com/wanderer-industries/wanderer/compare/v1.90.6...v1.90.7) (2025-12-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed scopes
|
||||
|
||||
## [v1.90.6](https://github.com/wanderer-industries/wanderer/compare/v1.90.5...v1.90.6) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes
|
||||
|
||||
## [v1.90.5](https://github.com/wanderer-industries/wanderer/compare/v1.90.4...v1.90.5) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes
|
||||
|
||||
## [v1.90.4](https://github.com/wanderer-industries/wanderer/compare/v1.90.3...v1.90.4) (2025-12-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map scopes & signatures clean up behaviour
|
||||
|
||||
## [v1.90.3](https://github.com/wanderer-industries/wanderer/compare/v1.90.2...v1.90.3) (2025-12-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added pagination for long ACL lists
|
||||
|
||||
## [v1.90.2](https://github.com/wanderer-industries/wanderer/compare/v1.90.1...v1.90.2) (2025-12-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added system position updates to SSE
|
||||
|
||||
## [v1.90.1](https://github.com/wanderer-industries/wanderer/compare/v1.90.0...v1.90.1) (2025-12-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed connections and signatures remove issues, added comprehensive audit log for auto removed connections and signatures
|
||||
|
||||
## [v1.90.0](https://github.com/wanderer-industries/wanderer/compare/v1.89.6...v1.90.0) (2025-12-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* core: Added several map scopes support (Wh, Hi, Low, Null, Pochven)
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed clean up for linked signatures
|
||||
|
||||
* core: fixed issue with default select mode
|
||||
|
||||
* apiV1 default fields updates
|
||||
|
||||
## [v1.89.6](https://github.com/wanderer-industries/wanderer/compare/v1.89.5...v1.89.6) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* kills: fixed zkb links (added "allow-popups-to-escape-sandbox" to CSP)
|
||||
|
||||
## [v1.89.5](https://github.com/wanderer-industries/wanderer/compare/v1.89.4...v1.89.5) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.89.4](https://github.com/wanderer-industries/wanderer/compare/v1.89.3...v1.89.4) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed acl character update issues
|
||||
|
||||
## [v1.89.3](https://github.com/wanderer-industries/wanderer/compare/v1.89.2...v1.89.3) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed tracking issues
|
||||
|
||||
## [v1.89.2](https://github.com/wanderer-industries/wanderer/compare/v1.89.1...v1.89.2) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed linked signatures cleanup
|
||||
|
||||
## [v1.88.8](https://github.com/wanderer-industries/wanderer/compare/v1.88.7...v1.88.8) (2025-11-28)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed pings issue
|
||||
|
||||
## [v1.88.7](https://github.com/wanderer-industries/wanderer/compare/v1.88.6...v1.88.7) (2025-11-28)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed tracking issues
|
||||
|
||||
## [v1.88.6](https://github.com/wanderer-industries/wanderer/compare/v1.88.5...v1.88.6) (2025-11-28)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed tracking issues
|
||||
|
||||
## [v1.88.5](https://github.com/wanderer-industries/wanderer/compare/v1.88.4...v1.88.5) (2025-11-28)
|
||||
|
||||
|
||||
|
||||
50
Makefile
50
Makefile
@@ -32,6 +32,56 @@ format f:
|
||||
test t:
|
||||
MIX_ENV=test mix test
|
||||
|
||||
# Run tests in 4 parallel partitions (useful for CI or faster local runs)
|
||||
test-parallel tp:
|
||||
@echo "Running tests in 4 parallel partitions..."
|
||||
@mkdir -p /tmp/wanderer_test_results
|
||||
@rm -f /tmp/wanderer_test_results/partition_*.txt /tmp/wanderer_test_results/exit_*.txt
|
||||
@for i in 1 2 3 4; do \
|
||||
(MIX_TEST_PARTITION=$$i MIX_ENV=test mix test --partitions 4 2>&1; echo $$? > /tmp/wanderer_test_results/exit_$$i.txt) | \
|
||||
tee /tmp/wanderer_test_results/partition_$$i.txt | sed "s/^/[P$$i] /" & \
|
||||
done; \
|
||||
wait
|
||||
@echo ""
|
||||
@echo "========================================"
|
||||
@echo " TEST RESULTS SUMMARY"
|
||||
@echo "========================================"
|
||||
@total_tests=0; total_failures=0; total_excluded=0; all_passed=true; \
|
||||
for i in 1 2 3 4; do \
|
||||
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
|
||||
if [ "$$exit_code" != "0" ]; then all_passed=false; fi; \
|
||||
summary=$$(grep -E "^[0-9]+ (tests?|doctest)" /tmp/wanderer_test_results/partition_$$i.txt | tail -1 || echo "No results"); \
|
||||
tests=$$(echo "$$summary" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
failures=$$(echo "$$summary" | grep -oE "[0-9]+ failures?" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
excluded=$$(echo "$$summary" | grep -oE "[0-9]+ excluded" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
total_tests=$$((total_tests + tests)); \
|
||||
total_failures=$$((total_failures + failures)); \
|
||||
total_excluded=$$((total_excluded + excluded)); \
|
||||
if [ "$$exit_code" = "0" ]; then \
|
||||
echo "Partition $$i: ✓ $$summary"; \
|
||||
else \
|
||||
echo "Partition $$i: ✗ $$summary (exit code: $$exit_code)"; \
|
||||
fi; \
|
||||
done; \
|
||||
echo "========================================"; \
|
||||
echo "TOTAL: $$total_tests tests, $$total_failures failures, $$total_excluded excluded"; \
|
||||
echo "========================================"; \
|
||||
if [ "$$all_passed" = "true" ]; then \
|
||||
echo "✓ All partitions passed!"; \
|
||||
else \
|
||||
echo "✗ Some partitions failed. Details below:"; \
|
||||
echo ""; \
|
||||
for i in 1 2 3 4; do \
|
||||
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
|
||||
if [ "$$exit_code" != "0" ]; then \
|
||||
echo "======== PARTITION $$i FAILURES ========"; \
|
||||
grep -A 50 "Failures:" /tmp/wanderer_test_results/partition_$$i.txt 2>/dev/null || cat /tmp/wanderer_test_results/partition_$$i.txt; \
|
||||
echo ""; \
|
||||
fi; \
|
||||
done; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
coverage cover co:
|
||||
MIX_ENV=test mix test --cover
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WormholeSignaturesDialog';
|
||||
20
assets/js/hooks/Mapper/components/ui-kit/RespawnTag.tsx
Normal file
20
assets/js/hooks/Mapper/components/ui-kit/RespawnTag.tsx
Normal 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>
|
||||
);
|
||||
@@ -23,3 +23,4 @@ export * from './MenuItemWithInfo';
|
||||
export * from './MarkdownTextViewer.tsx';
|
||||
export * from './WdButton.tsx';
|
||||
export * from './constants.ts';
|
||||
export * from './RespawnTag';
|
||||
|
||||
@@ -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 });
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ export default {
|
||||
};
|
||||
|
||||
refreshZone.addEventListener('click', handleUpdate);
|
||||
refreshZone.addEventListener('mouseover', handleUpdate);
|
||||
// refreshZone.addEventListener('mouseover', handleUpdate);
|
||||
|
||||
this.updated();
|
||||
},
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
BIN
assets/static/images/news/2025/12-18-advent-giveaway/cover.jpg
Normal file
BIN
assets/static/images/news/2025/12-18-advent-giveaway/cover.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -264,7 +264,7 @@ config :logger,
|
||||
case config_env() do
|
||||
:prod -> "info"
|
||||
:dev -> "info"
|
||||
:test -> "debug"
|
||||
:test -> "warning"
|
||||
end
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import Config
|
||||
|
||||
# Disable Ash async operations in tests to ensure transactional safety
|
||||
# This prevents Ash from spawning tasks that could bypass the Ecto sandbox
|
||||
config :ash, :disable_async?, true
|
||||
|
||||
# Configure your database
|
||||
#
|
||||
# The MIX_TEST_PARTITION environment variable can be used
|
||||
|
||||
@@ -16,6 +16,11 @@ defmodule WandererApp.Api.AccessList do
|
||||
|
||||
includes([:owner, :members])
|
||||
|
||||
default_fields([
|
||||
:name,
|
||||
:description
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -79,12 +84,15 @@ defmodule WandererApp.Api.AccessList do
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :description, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
# Note: api_key intentionally not public for security
|
||||
attribute :api_key, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
@@ -16,6 +16,14 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
|
||||
includes([:access_list])
|
||||
|
||||
default_fields([
|
||||
:name,
|
||||
:eve_character_id,
|
||||
:eve_corporation_id,
|
||||
:eve_alliance_id,
|
||||
:role
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -89,22 +97,27 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_character_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_corporation_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_alliance_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :role, :atom do
|
||||
default "viewer"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
|
||||
@@ -19,9 +19,10 @@ defmodule WandererApp.Api.Changes.InjectMapFromActor do
|
||||
_other ->
|
||||
# nil or unexpected return shape - check for direct map_id
|
||||
# Check params (input), arguments, and attributes (in that order)
|
||||
map_id = Map.get(changeset.params, :map_id) ||
|
||||
Ash.Changeset.get_argument(changeset, :map_id) ||
|
||||
Ash.Changeset.get_attribute(changeset, :map_id)
|
||||
map_id =
|
||||
Map.get(changeset.params, :map_id) ||
|
||||
Ash.Changeset.get_argument(changeset, :map_id) ||
|
||||
Ash.Changeset.get_attribute(changeset, :map_id)
|
||||
|
||||
case map_id do
|
||||
nil ->
|
||||
|
||||
@@ -13,6 +13,8 @@ defmodule WandererApp.Api.Map do
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("maps_v1")
|
||||
|
||||
migration_defaults scopes: "'{wormholes}'"
|
||||
end
|
||||
|
||||
json_api do
|
||||
@@ -111,6 +113,7 @@ defmodule WandererApp.Api.Map do
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:scopes,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
@@ -135,6 +138,7 @@ defmodule WandererApp.Api.Map do
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:scopes,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
@@ -209,7 +213,7 @@ defmodule WandererApp.Api.Map do
|
||||
end
|
||||
|
||||
create :duplicate do
|
||||
accept [:name, :description, :scope, :only_tracked_characters]
|
||||
accept [:name, :description, :scope, :scopes, :only_tracked_characters]
|
||||
argument :source_map_id, :uuid, allow_nil?: false
|
||||
argument :copy_acls, :boolean, default: true
|
||||
argument :copy_user_settings, :boolean, default: true
|
||||
@@ -225,9 +229,14 @@ defmodule WandererApp.Api.Map do
|
||||
description =
|
||||
Ash.Changeset.get_attribute(changeset, :description) || source_map.description
|
||||
|
||||
# Use provided scopes or fall back to source map scopes
|
||||
scopes =
|
||||
Ash.Changeset.get_attribute(changeset, :scopes) || source_map.scopes
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.change_attribute(:description, description)
|
||||
|> Ash.Changeset.change_attribute(:scope, source_map.scope)
|
||||
|> Ash.Changeset.change_attribute(:scopes, scopes)
|
||||
|> Ash.Changeset.change_attribute(
|
||||
:only_tracked_characters,
|
||||
source_map.only_tracked_characters
|
||||
@@ -359,6 +368,24 @@ defmodule WandererApp.Api.Map do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :scopes, {:array, :atom} do
|
||||
default([:wormholes])
|
||||
allow_nil?(true)
|
||||
public?(true)
|
||||
|
||||
constraints(
|
||||
items: [
|
||||
one_of: [
|
||||
:wormholes,
|
||||
:hi,
|
||||
:low,
|
||||
:null,
|
||||
:pochven
|
||||
]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
@@ -27,6 +27,11 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
includes([:map, :character])
|
||||
|
||||
default_fields([
|
||||
:tracked,
|
||||
:followed
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -128,6 +133,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
require_atomic? false
|
||||
|
||||
accept([
|
||||
:tracked,
|
||||
:followed,
|
||||
:ship,
|
||||
:ship_name,
|
||||
:ship_item_id,
|
||||
@@ -217,14 +224,17 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
attribute :tracked, :boolean do
|
||||
default false
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :followed, :boolean do
|
||||
default false
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
# Note: These attributes are encrypted (AshCloak) and intentionally not public
|
||||
attribute :solar_system_id, :integer
|
||||
attribute :structure_id, :integer
|
||||
attribute :station_id, :integer
|
||||
|
||||
@@ -22,6 +22,19 @@ defmodule WandererApp.Api.MapConnection do
|
||||
|
||||
includes([:map])
|
||||
|
||||
default_fields([
|
||||
:solar_system_source,
|
||||
:solar_system_target,
|
||||
:mass_status,
|
||||
:time_status,
|
||||
:ship_size_type,
|
||||
:type,
|
||||
:wormhole_type,
|
||||
:count_of_passage,
|
||||
:locked,
|
||||
:custom_info
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -197,15 +210,20 @@ defmodule WandererApp.Api.MapConnection do
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :solar_system_source, :integer
|
||||
attribute :solar_system_target, :integer
|
||||
attribute :solar_system_source, :integer do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_target, :integer do
|
||||
public? true
|
||||
end
|
||||
|
||||
# where 0 - greater than half
|
||||
# where 1 - less than half
|
||||
# where 2 - critical less than 10%
|
||||
attribute :mass_status, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -218,7 +236,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# 6 - EOL 48h
|
||||
attribute :time_status, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -229,7 +247,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# where 4 - Capital
|
||||
attribute :ship_size_type, :integer do
|
||||
default(2)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -238,21 +256,26 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# where 2 - Bridge
|
||||
attribute :type, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :wormhole_type, :string
|
||||
attribute :wormhole_type, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :count_of_passage, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :locked, :boolean
|
||||
attribute :locked, :boolean do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :custom_info, :string do
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
||||
:updated_by
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:settings
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_default_settings")
|
||||
|
||||
@@ -93,6 +97,7 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
||||
|
||||
attribute :settings, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
constraints min_length: 2
|
||||
description "JSON string containing the default map settings"
|
||||
end
|
||||
|
||||
@@ -18,6 +18,15 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
:map
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:plan,
|
||||
:status,
|
||||
:characters_limit,
|
||||
:hubs_limit,
|
||||
:active_till,
|
||||
:auto_renew?
|
||||
])
|
||||
|
||||
# Enable automatic filtering and sorting
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
@@ -135,6 +144,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :plan, :atom do
|
||||
default "alpha"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -150,6 +160,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :status, :atom do
|
||||
default "active"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -164,22 +175,24 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :characters_limit, :integer do
|
||||
default(100)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :hubs_limit, :integer do
|
||||
default(10)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :active_till, :utc_datetime do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :auto_renew?, :boolean do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
|
||||
@@ -19,6 +19,10 @@ defmodule WandererApp.Api.MapSystemComment do
|
||||
:character
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:text
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_system_comments")
|
||||
|
||||
@@ -73,6 +77,7 @@ defmodule WandererApp.Api.MapSystemComment do
|
||||
|
||||
attribute :text, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
|
||||
@@ -16,6 +16,20 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
|
||||
includes([:system])
|
||||
|
||||
default_fields([
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:description,
|
||||
:temporary_name,
|
||||
:type,
|
||||
:linked_system_id,
|
||||
:kind,
|
||||
:group,
|
||||
:custom_info,
|
||||
:deleted
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -184,42 +198,56 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
|
||||
attribute :eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :description, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :temporary_name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :type, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :linked_system_id, :integer do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :kind, :string
|
||||
attribute :group, :string
|
||||
attribute :kind, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :group, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :custom_info, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :deleted, :boolean do
|
||||
allow_nil? false
|
||||
default false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :update_forced_at, :utc_datetime do
|
||||
|
||||
@@ -41,6 +41,21 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
:system
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
])
|
||||
|
||||
# Enable automatic filtering and sorting
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
@@ -151,50 +166,62 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
|
||||
attribute :structure_type_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :structure_type, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_id, :integer do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :notes, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_ticker, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :status, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :end_time, :utc_datetime_usec do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp :inserted_at
|
||||
|
||||
@@ -24,6 +24,13 @@ defmodule WandererApp.Api.MapUserSettings do
|
||||
:user
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:settings,
|
||||
:main_character_eve_id,
|
||||
:following_character_eve_id,
|
||||
:hubs
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_user_settings")
|
||||
|
||||
@@ -85,19 +92,22 @@ defmodule WandererApp.Api.MapUserSettings do
|
||||
|
||||
attribute :settings, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :main_character_eve_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :following_character_eve_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :hubs, {:array, :string} do
|
||||
allow_nil?(true)
|
||||
|
||||
public? true
|
||||
default([])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +31,13 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
includes([:character, :user])
|
||||
|
||||
default_fields([
|
||||
:entity_id,
|
||||
:entity_type,
|
||||
:event_type,
|
||||
:event_data
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -86,10 +93,12 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
attribute :entity_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :entity_type, :atom do
|
||||
default "map"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -104,6 +113,7 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
attribute :event_type, :atom do
|
||||
default "custom"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -153,7 +163,9 @@ defmodule WandererApp.Api.UserActivity do
|
||||
allow_nil?(false)
|
||||
end
|
||||
|
||||
attribute :event_data, :string
|
||||
attribute :event_data, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
|
||||
@@ -45,7 +45,7 @@ defmodule WandererApp.Cache do
|
||||
def insert({id, key}, value, opts) when is_binary(id) and (is_binary(key) or is_atom(key)),
|
||||
do: insert("#{id}:#{key}", value, opts)
|
||||
|
||||
def insert(key, nil, opts) when is_binary(key) or is_atom(key), do: delete(key)
|
||||
def insert(key, nil, _opts) when is_binary(key) or is_atom(key), do: delete(key)
|
||||
def insert(key, value, opts) when is_binary(key) or is_atom(key), do: put(key, value, opts)
|
||||
|
||||
def insert_or_update(key, value, update_fn, opts \\ [])
|
||||
|
||||
@@ -598,9 +598,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :skipped}
|
||||
end
|
||||
|
||||
_ ->
|
||||
@@ -799,7 +796,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
corporation_id
|
||||
|> WandererApp.Esi.get_corporation_info()
|
||||
|> case do
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker}} ->
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
@@ -1002,7 +999,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
defp maybe_update_active_maps(
|
||||
%{character_id: character_id, active_maps: active_maps} =
|
||||
state,
|
||||
%{map_id: map_id, track: true} = track_settings
|
||||
%{map_id: map_id, track: true}
|
||||
) do
|
||||
if not Enum.member?(active_maps, map_id) do
|
||||
WandererApp.Cache.put(
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
@moduledoc false
|
||||
@moduledoc """
|
||||
Implementation of the character tracker manager.
|
||||
|
||||
This module manages the lifecycle of character trackers and handles:
|
||||
- Starting/stopping character tracking
|
||||
- Garbage collection of inactive trackers (5-minute timeout)
|
||||
- Processing the untrack queue (5-minute interval)
|
||||
|
||||
## Logging
|
||||
|
||||
This module emits detailed logs for debugging character tracking issues:
|
||||
- WARNING: Unexpected states or potential issues
|
||||
- DEBUG: Start/stop tracking events, garbage collection, queue processing
|
||||
"""
|
||||
require Logger
|
||||
|
||||
defstruct [
|
||||
@@ -27,6 +40,13 @@ 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"
|
||||
)
|
||||
|
||||
%{
|
||||
characters: [],
|
||||
opts: args
|
||||
@@ -38,6 +58,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
WandererApp.Cache.insert("tracked_characters", [])
|
||||
|
||||
if length(tracked_characters) > 0 do
|
||||
Logger.debug(
|
||||
"[TrackerManager] Restoring #{length(tracked_characters)} tracked characters from cache"
|
||||
)
|
||||
end
|
||||
|
||||
tracked_characters
|
||||
|> Enum.each(fn character_id ->
|
||||
start_tracking(state, character_id)
|
||||
@@ -53,7 +79,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
true
|
||||
)
|
||||
|
||||
Logger.debug(fn -> "Add character to track_characters_queue: #{inspect(character_id)}" end)
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Queuing character #{character_id} for tracking start"
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
@@ -71,13 +99,33 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
true <- Enum.member?(characters, character_id),
|
||||
false <- WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Stopping tracker for character #{character_id} - " <>
|
||||
"reason: no active maps (garbage collected after #{div(@inactive_character_timeout, 60_000)} minutes)"
|
||||
end)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:last_active_time")
|
||||
WandererApp.Character.delete_character_state(character_id)
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :character, :tracker, :stopped],
|
||||
%{count: 1, system_time: System.system_time()},
|
||||
%{character_id: character_id, reason: :garbage_collection}
|
||||
)
|
||||
else
|
||||
{:ok, characters} when is_list(characters) ->
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Character #{character_id} not in tracked list, skipping stop"
|
||||
end)
|
||||
|
||||
false ->
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Character #{character_id} has pending track request, skipping stop"
|
||||
end)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
@@ -101,13 +149,35 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
} = track_settings
|
||||
) do
|
||||
if track do
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Enabling tracking for character #{character_id} on map #{map_id}"
|
||||
end)
|
||||
|
||||
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} - " <>
|
||||
"will be processed within #{div(@untrack_characters_interval, 60_000)} minutes"
|
||||
end)
|
||||
|
||||
add_to_untrack_queue(map_id, character_id)
|
||||
end
|
||||
|
||||
@@ -130,8 +200,20 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
"character_untrack_queue",
|
||||
[],
|
||||
fn untrack_queue ->
|
||||
untrack_queue
|
||||
|> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)
|
||||
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)
|
||||
|
||||
if length(filtered) < original_length do
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Removed character #{character_id} from untrack queue for map #{map_id} - " <>
|
||||
"character re-enabled tracking"
|
||||
end)
|
||||
end
|
||||
|
||||
filtered
|
||||
end
|
||||
)
|
||||
end
|
||||
@@ -170,6 +252,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
|
||||
{:ok, track_characters_queue} = WandererApp.Cache.lookup("track_characters_queue", [])
|
||||
|
||||
if length(track_characters_queue) > 0 do
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Processing start queue: #{length(track_characters_queue)} characters"
|
||||
end)
|
||||
end
|
||||
|
||||
track_characters_queue
|
||||
|> Enum.each(fn character_id ->
|
||||
track_character(character_id, %{})
|
||||
@@ -186,35 +274,66 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
{:ok, characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
case WandererApp.Cache.lookup("character:#{character_id}:last_active_time") do
|
||||
{:ok, nil} ->
|
||||
:skip
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Running garbage collection on #{length(characters)} tracked characters"
|
||||
end)
|
||||
|
||||
{:ok, last_active_time} ->
|
||||
duration = DateTime.diff(DateTime.utc_now(), last_active_time, :second)
|
||||
|
||||
if duration * 1000 > @inactive_character_timeout do
|
||||
{:stop, character_id}
|
||||
else
|
||||
inactive_characters =
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
case WandererApp.Cache.lookup("character:#{character_id}:last_active_time") do
|
||||
{:ok, nil} ->
|
||||
# Character is still active (no last_active_time set)
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end,
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(60)
|
||||
)
|
||||
|> Enum.each(fn result ->
|
||||
case result do
|
||||
{:ok, {:stop, character_id}} ->
|
||||
Process.send_after(self(), {:stop_track, character_id}, 100)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
{:ok, last_active_time} ->
|
||||
duration_seconds = DateTime.diff(DateTime.utc_now(), last_active_time, :second)
|
||||
duration_ms = duration_seconds * 1000
|
||||
|
||||
if duration_ms > @inactive_character_timeout do
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Character #{character_id} marked for garbage collection - " <>
|
||||
"inactive for #{div(duration_seconds, 60)} minutes " <>
|
||||
"(threshold: #{div(@inactive_character_timeout, 60_000)} minutes)"
|
||||
end)
|
||||
|
||||
{:stop, character_id, duration_seconds}
|
||||
else
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end,
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(60)
|
||||
)
|
||||
|> Enum.reduce([], fn result, acc ->
|
||||
case result do
|
||||
{:ok, {:stop, character_id, duration}} ->
|
||||
[{character_id, duration} | acc]
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
if length(inactive_characters) > 0 do
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Garbage collection found #{length(inactive_characters)} inactive characters to stop"
|
||||
end)
|
||||
|
||||
# Emit telemetry for garbage collection
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :character, :tracker, :garbage_collection],
|
||||
%{inactive_count: length(inactive_characters), total_tracked: length(characters)},
|
||||
%{character_ids: Enum.map(inactive_characters, fn {id, _} -> id end)}
|
||||
)
|
||||
end
|
||||
|
||||
inactive_characters
|
||||
|> Enum.each(fn {character_id, _duration} ->
|
||||
Process.send_after(self(), {:stop_track, character_id}, 100)
|
||||
end)
|
||||
|
||||
state
|
||||
@@ -226,9 +345,22 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
) do
|
||||
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
|
||||
|
||||
WandererApp.Cache.lookup!("character_untrack_queue", [])
|
||||
untrack_queue = WandererApp.Cache.lookup!("character_untrack_queue", [])
|
||||
|
||||
if length(untrack_queue) > 0 do
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Processing untrack queue: #{length(untrack_queue)} character-map pairs"
|
||||
end)
|
||||
end
|
||||
|
||||
untrack_queue
|
||||
|> Task.async_stream(
|
||||
fn {map_id, character_id} ->
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Untracking character #{character_id} from map #{map_id} - " <>
|
||||
"reason: character no longer present on map"
|
||||
end)
|
||||
|
||||
remove_from_untrack_queue(map_id, character_id)
|
||||
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
||||
@@ -255,12 +387,36 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
|
||||
|
||||
# Emit telemetry for untrack event
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :character, :tracker, :untracked_from_map],
|
||||
%{system_time: System.system_time()},
|
||||
%{character_id: character_id, map_id: map_id, reason: :presence_left}
|
||||
)
|
||||
|
||||
{:ok, character_id, map_id}
|
||||
end,
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(30)
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
|> Enum.each(fn result ->
|
||||
case result do
|
||||
{:ok, {:ok, character_id, map_id}} ->
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Successfully untracked character #{character_id} from map #{map_id}"
|
||||
end)
|
||||
|
||||
{:exit, reason} ->
|
||||
Logger.warning(fn ->
|
||||
"[TrackerManager] Untrack task exited with reason: #{inspect(reason)}"
|
||||
end)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
state
|
||||
end
|
||||
@@ -268,9 +424,17 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
def handle_info({:stop_track, character_id}, state) do
|
||||
if not WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking") do
|
||||
WandererApp.Cache.insert("character:#{character_id}:is_stop_tracking", true)
|
||||
Logger.debug(fn -> "Stopping character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Executing stop_track for character #{character_id}"
|
||||
end)
|
||||
|
||||
stop_tracking(state, character_id)
|
||||
WandererApp.Cache.delete("character:#{character_id}:is_stop_tracking")
|
||||
else
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Character #{character_id} already being stopped, skipping duplicate request"
|
||||
end)
|
||||
end
|
||||
|
||||
state
|
||||
@@ -279,7 +443,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
def track_character(character_id, opts) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
false <- Enum.member?(characters, character_id) do
|
||||
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Starting tracker for character #{character_id}"
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"tracked_characters",
|
||||
@@ -312,7 +478,30 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
character_id,
|
||||
%{opts: opts}
|
||||
])
|
||||
|
||||
# Emit telemetry for tracker start
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :character, :tracker, :started],
|
||||
%{count: 1, system_time: System.system_time()},
|
||||
%{character_id: character_id}
|
||||
)
|
||||
else
|
||||
true ->
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Character #{character_id} already being tracked"
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
[],
|
||||
fn existing ->
|
||||
existing
|
||||
|> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.delete("#{character_id}:track_requested")
|
||||
|
||||
_ ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
|
||||
@@ -88,15 +88,4 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
|
||||
{:ok, pid}
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_child(uuid) do
|
||||
case Registry.lookup(@registry, uuid) do
|
||||
[{pid, _}] ->
|
||||
GenServer.cast(pid, :stop)
|
||||
|
||||
_ ->
|
||||
Logger.warn("Unable to locate pool assigned to #{inspect(uuid)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,7 +38,7 @@ defmodule WandererApp.Character.TrackingConfigUtils do
|
||||
%{id: "default", title: "Default", value: default_count}
|
||||
]
|
||||
|
||||
{:ok, pools_count} =
|
||||
{:ok, _pools_count} =
|
||||
Cachex.get(
|
||||
:esi_auth_cache,
|
||||
"configs_total_count"
|
||||
|
||||
@@ -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,10 +115,160 @@ 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
|
||||
WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|
||||
|> case do
|
||||
# First check current tracking state to avoid unnecessary permission checks
|
||||
current_settings = WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|
||||
|
||||
case {track, current_settings} do
|
||||
# Already tracked and wants to stay tracked - no permission check needed
|
||||
{true, {:ok, %{tracked: true} = settings}} ->
|
||||
do_update_character_tracking_impl(character, map_id, track, caller_pid, {:ok, settings})
|
||||
|
||||
# Wants to enable tracking - check permissions first
|
||||
{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
|
||||
)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning(
|
||||
"[CharacterTracking] Character #{character.id} cannot be tracked on map #{map_id}: #{reason}"
|
||||
)
|
||||
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
# Untracking is always allowed
|
||||
{false, settings_result} ->
|
||||
do_update_character_tracking_impl(character, map_id, track, caller_pid, settings_result)
|
||||
end
|
||||
end
|
||||
|
||||
# Check if a character has permission to be tracked on a map
|
||||
defp check_character_tracking_permission(character, map_id) do
|
||||
with {:ok, %{acls: acls, owner_id: owner_id}} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
) do
|
||||
# Check if character is the map owner
|
||||
if character.id == owner_id do
|
||||
{:ok, :allowed}
|
||||
else
|
||||
# Check if character belongs to same user as owner (Option 3 check)
|
||||
case check_same_user_as_owner(character, owner_id) do
|
||||
true ->
|
||||
{:ok, :allowed}
|
||||
|
||||
false ->
|
||||
# Check ACL-based permissions
|
||||
[character_permissions] =
|
||||
WandererApp.Permissions.check_characters_access([character], acls)
|
||||
|
||||
map_permissions = WandererApp.Permissions.get_permissions(character_permissions)
|
||||
|
||||
if map_permissions.track_character and map_permissions.view_system do
|
||||
{:ok, :allowed}
|
||||
else
|
||||
{:error,
|
||||
"Character does not have tracking permission on this map. Please add the character to a map access list or ensure you are the map owner."}
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
{:error, _} ->
|
||||
{:error, "Failed to verify map permissions"}
|
||||
end
|
||||
end
|
||||
|
||||
# Check if character belongs to the same user as the map owner
|
||||
defp check_same_user_as_owner(_character, nil), do: false
|
||||
|
||||
defp check_same_user_as_owner(character, owner_id) do
|
||||
case WandererApp.Character.get_character(owner_id) do
|
||||
{:ok, owner_character} ->
|
||||
character.user_id != nil and character.user_id == owner_character.user_id
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update_character_tracking_impl(character, map_id, track, caller_pid, settings_result) do
|
||||
case settings_result do
|
||||
# Untracking flow
|
||||
{:ok, %{tracked: true} = existing_settings} ->
|
||||
if not track do
|
||||
@@ -132,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
|
||||
@@ -148,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
|
||||
@@ -210,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
|
||||
|
||||
@@ -8,7 +8,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
@ttl :timer.hours(1)
|
||||
|
||||
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
|
||||
@req_esi_options [base_url: "https://esi.evetech.net", finch: WandererApp.Finch]
|
||||
|
||||
@cache_opts [cache: true]
|
||||
@retry_opts [retry: false, retry_log_level: :warning]
|
||||
@@ -74,7 +73,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
)
|
||||
|
||||
def get_routes_eve(hubs, origin, params, opts),
|
||||
def get_routes_eve(hubs, origin, _params, _opts),
|
||||
do:
|
||||
{:ok,
|
||||
hubs
|
||||
@@ -101,33 +100,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end)}
|
||||
|
||||
defp do_get_routes_eve(origin, destination, params, opts) do
|
||||
esi_params =
|
||||
Map.merge(params, %{
|
||||
connections: params.connections |> Enum.join(","),
|
||||
avoid: params.avoid |> Enum.join(",")
|
||||
})
|
||||
|
||||
do_get(
|
||||
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|> case do
|
||||
{:ok, result} ->
|
||||
%{
|
||||
"origin" => origin,
|
||||
"destination" => destination,
|
||||
"systems" => result,
|
||||
"success" => true
|
||||
}
|
||||
|
||||
error ->
|
||||
Logger.warning("Error getting routes: #{inspect(error)}")
|
||||
%{"origin" => origin, "destination" => destination, "systems" => [], "success" => false}
|
||||
end
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "group-info-#{group_id}",
|
||||
@@ -273,6 +245,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
# Note: search_val and categories_val are used by the @decorate cacheable annotation above
|
||||
_unused = {search_val, categories_val}
|
||||
get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
end
|
||||
|
||||
@@ -348,7 +322,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
defp with_cache_opts(opts),
|
||||
do: opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
|
||||
|
||||
defp do_get(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
defp do_get(path, api_opts, opts, pool \\ @general_pool) do
|
||||
case Cachex.get(:api_cache, path) do
|
||||
{:ok, cached_data} when not is_nil(cached_data) ->
|
||||
{:ok, cached_data}
|
||||
@@ -358,7 +332,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_request(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
defp do_get_request(path, api_opts, opts, pool) do
|
||||
try do
|
||||
req_options_for_pool(pool)
|
||||
|> Req.new()
|
||||
@@ -448,7 +422,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, %{status: status} = _error} when status in [401, 403] ->
|
||||
do_get_retry(path, api_opts, opts)
|
||||
|
||||
{:ok, %{status: status, headers: headers}} ->
|
||||
{:ok, %{status: status}} ->
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
|
||||
{:error, %Mint.TransportError{reason: :timeout}} ->
|
||||
@@ -832,10 +806,10 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp handle_refresh_token_result(
|
||||
{:error, %OAuth2.Error{reason: :econnrefused} = error},
|
||||
character,
|
||||
_character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
_scopes
|
||||
) do
|
||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||
|
||||
@@ -393,9 +393,6 @@ defmodule WandererApp.EveDataService do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_solar_system_name(solar_system_name, wormhole_class) do
|
||||
end
|
||||
|
||||
defp get_triglavian_data(default_data, triglavian_systems, solar_system_id) do
|
||||
case Enum.find(triglavian_systems, fn system -> system.solar_system_id == solar_system_id end) do
|
||||
nil ->
|
||||
@@ -413,8 +410,12 @@ defmodule WandererApp.EveDataService do
|
||||
|
||||
defp get_security(security) do
|
||||
case security do
|
||||
nil -> {:ok, ""}
|
||||
_ -> {:ok, String.to_float(security) |> get_true_security() |> Float.to_string(decimals: 1)}
|
||||
nil ->
|
||||
{:ok, ""}
|
||||
|
||||
_ ->
|
||||
{:ok,
|
||||
String.to_float(security) |> get_true_security() |> :erlang.float_to_binary(decimals: 1)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -496,23 +497,23 @@ defmodule WandererApp.EveDataService do
|
||||
do: {:ok, 10_100}
|
||||
|
||||
defp get_wormhole_class_id(systems, region_id, constellation_id, solar_system_id) do
|
||||
with region <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||
end),
|
||||
constellation <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
||||
end),
|
||||
solar_system <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||
end),
|
||||
wormhole_class_id <- get_wormhole_class_id(region, constellation, solar_system) do
|
||||
{:ok, wormhole_class_id}
|
||||
else
|
||||
_ -> {:ok, -1}
|
||||
end
|
||||
region =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||
end)
|
||||
|
||||
constellation =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
||||
end)
|
||||
|
||||
solar_system =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||
end)
|
||||
|
||||
wormhole_class_id = get_wormhole_class_id(region, constellation, solar_system)
|
||||
{:ok, wormhole_class_id}
|
||||
end
|
||||
|
||||
defp get_wormhole_class_id(_region, _constellation, solar_system)
|
||||
|
||||
@@ -178,6 +178,10 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_payload(payload, visited) when is_map(payload) do
|
||||
Map.new(payload, fn {k, v} -> {to_string(k), serialize_value(v, visited)} end)
|
||||
end
|
||||
|
||||
# Get allowed fields based on struct type
|
||||
defp get_allowed_fields(module) do
|
||||
module_name = module |> Module.split() |> List.last()
|
||||
@@ -192,10 +196,6 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_payload(payload, visited) when is_map(payload) do
|
||||
Map.new(payload, fn {k, v} -> {to_string(k), serialize_value(v, visited)} end)
|
||||
end
|
||||
|
||||
defp serialize_fields(fields, visited) do
|
||||
Enum.reduce(fields, %{}, fn {k, v}, acc ->
|
||||
if is_nil(v) do
|
||||
|
||||
@@ -98,8 +98,8 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
|
||||
"id" => payload["system_id"] || payload[:system_id],
|
||||
"attributes" => %{
|
||||
"locked" => payload["locked"] || payload[:locked],
|
||||
"x" => payload["x"] || payload[:x],
|
||||
"y" => payload["y"] || payload[:y],
|
||||
"position_x" => payload["position_x"] || payload[:position_x],
|
||||
"position_y" => payload["position_y"] || payload[:position_y],
|
||||
"updated_at" => event.timestamp
|
||||
},
|
||||
"relationships" => %{
|
||||
|
||||
@@ -182,7 +182,7 @@ defmodule WandererApp.Kills.Client do
|
||||
end
|
||||
|
||||
# Guard against duplicate disconnection events
|
||||
def handle_info({:disconnected, reason}, %{connected: false, connecting: false} = state) do
|
||||
def handle_info({:disconnected, _reason}, %{connected: false, connecting: false} = state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -566,7 +566,7 @@ defmodule WandererApp.Kills.Client do
|
||||
end
|
||||
end
|
||||
|
||||
defp check_health(%{socket_pid: pid} = state) do
|
||||
defp check_health(%{socket_pid: pid}) do
|
||||
if socket_alive?(pid) do
|
||||
:healthy
|
||||
else
|
||||
@@ -590,22 +590,6 @@ defmodule WandererApp.Kills.Client do
|
||||
Process.send_after(self(), :health_check, @health_check_interval)
|
||||
end
|
||||
|
||||
defp handle_connection_lost(%{connected: false} = _state) do
|
||||
Logger.debug("[Client] Connection already lost, skipping cleanup")
|
||||
end
|
||||
|
||||
defp handle_connection_lost(state) do
|
||||
Logger.warning("[Client] Connection lost, cleaning up and reconnecting")
|
||||
|
||||
# Clean up existing socket
|
||||
if state.socket_pid do
|
||||
disconnect_socket(state.socket_pid)
|
||||
end
|
||||
|
||||
# Reset state and trigger reconnection
|
||||
send(self(), {:disconnected, :connection_lost})
|
||||
end
|
||||
|
||||
# Handler module for WebSocket events
|
||||
defmodule Handler do
|
||||
@moduledoc """
|
||||
@@ -640,7 +624,7 @@ defmodule WandererApp.Kills.Client do
|
||||
}
|
||||
|
||||
case GenSocketClient.join(transport, "killmails:lobby", join_params) do
|
||||
{:ok, response} ->
|
||||
{:ok, _response} ->
|
||||
send(state.parent, {:connected, self()})
|
||||
# Reset disconnected flag on successful connection
|
||||
{:ok, %{state | disconnected: false}}
|
||||
|
||||
@@ -46,7 +46,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(%{event: :map_server_started, payload: map_info}, state) do
|
||||
def handle_info(%{event: :map_server_started, payload: _map_info}, state) do
|
||||
{:noreply, schedule_subscription_update(state)}
|
||||
end
|
||||
|
||||
@@ -191,7 +191,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
||||
# Client is not connected, retry with backoff
|
||||
schedule_retry_update(state)
|
||||
|
||||
error ->
|
||||
_error ->
|
||||
schedule_retry_update(state)
|
||||
end
|
||||
rescue
|
||||
|
||||
@@ -12,6 +12,7 @@ defmodule WandererApp.Map do
|
||||
defstruct map_id: nil,
|
||||
name: nil,
|
||||
scope: :none,
|
||||
scopes: nil,
|
||||
owner_id: nil,
|
||||
characters: [],
|
||||
systems: Map.new(),
|
||||
@@ -22,11 +23,15 @@ defmodule WandererApp.Map do
|
||||
characters_limit: nil,
|
||||
hubs_limit: nil
|
||||
|
||||
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs}) do
|
||||
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs} = input) do
|
||||
# Extract the new scopes array field if present (nil if not set)
|
||||
scopes = Map.get(input, :scopes)
|
||||
|
||||
map =
|
||||
struct!(__MODULE__,
|
||||
map_id: map_id,
|
||||
scope: scope,
|
||||
scopes: scopes,
|
||||
owner_id: owner_id,
|
||||
name: name,
|
||||
acls: acls,
|
||||
@@ -177,7 +182,7 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
|
||||
def list_hubs(map_id, hubs) do
|
||||
{:ok, map} = map_id |> get_map()
|
||||
{:ok, _map} = map_id |> get_map()
|
||||
|
||||
{:ok, hubs}
|
||||
end
|
||||
@@ -315,7 +320,7 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
end
|
||||
|
||||
def update_subscription_settings!(%{map_id: map_id} = map, %{
|
||||
def update_subscription_settings!(%{map_id: map_id} = _map, %{
|
||||
characters_limit: characters_limit,
|
||||
hubs_limit: hubs_limit
|
||||
}) do
|
||||
@@ -326,7 +331,7 @@ defmodule WandererApp.Map do
|
||||
|> get_map!()
|
||||
end
|
||||
|
||||
def update_options!(%{map_id: map_id} = map, options) do
|
||||
def update_options!(%{map_id: map_id} = _map, options) do
|
||||
map_id
|
||||
|> update_map(%{options: options})
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -76,11 +76,6 @@ defmodule WandererApp.Map.Operations do
|
||||
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
||||
defdelegate create_connection(map_id, attrs, char_id), to: Connections
|
||||
|
||||
@doc "Create a connection from a Plug.Conn"
|
||||
@spec create_connection(Plug.Conn.t(), map()) ::
|
||||
{:ok, :created} | {:skip, :exists} | {:error, atom()}
|
||||
defdelegate create_connection(conn, attrs), to: Connections
|
||||
|
||||
@doc "Update a connection"
|
||||
@spec update_connection(String.t(), String.t(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
|
||||
@@ -329,6 +329,9 @@ defmodule WandererApp.Map.MapPool do
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||
|
||||
defp do_start_map(map_id, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||
if map_id in map_ids do
|
||||
# Map already started
|
||||
@@ -344,8 +347,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
[map_id | r_map_ids]
|
||||
end)
|
||||
|
||||
completed_operations = [:registry | completed_operations]
|
||||
|
||||
case registry_result do
|
||||
{new_value, _old_value} when is_list(new_value) ->
|
||||
:ok
|
||||
@@ -363,13 +364,9 @@ defmodule WandererApp.Map.MapPool do
|
||||
raise "Failed to add to cache: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
completed_operations = [:cache | completed_operations]
|
||||
|
||||
# Step 3: Start the map server using extracted helper
|
||||
do_initialize_map_server(map_id)
|
||||
|
||||
completed_operations = [:map_server | completed_operations]
|
||||
|
||||
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
||||
new_state = %{state | map_ids: [map_id | map_ids]}
|
||||
|
||||
@@ -445,8 +442,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
r_map_ids |> Enum.reject(fn id -> id == map_id end)
|
||||
end)
|
||||
|
||||
completed_operations = [:registry | completed_operations]
|
||||
|
||||
case registry_result do
|
||||
{new_value, _old_value} when is_list(new_value) ->
|
||||
:ok
|
||||
@@ -464,14 +459,10 @@ defmodule WandererApp.Map.MapPool do
|
||||
raise "Failed to delete from cache: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
completed_operations = [:cache | completed_operations]
|
||||
|
||||
# Step 3: Stop the map server (clean up all map resources)
|
||||
map_id
|
||||
|> Server.Impl.stop_map()
|
||||
|
||||
completed_operations = [:map_server | completed_operations]
|
||||
|
||||
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
||||
new_state = %{state | map_ids: map_ids |> Enum.reject(fn id -> id == map_id end)}
|
||||
|
||||
@@ -560,9 +551,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
# and the cleanup operations are safe to leave in a "stopped" state
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||
|
||||
@impl true
|
||||
def handle_info(:backup_state, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||
|
||||
@@ -179,15 +179,4 @@ defmodule WandererApp.Map.MapPoolDynamicSupervisor do
|
||||
{:ok, pid}
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_child(uuid) do
|
||||
case Registry.lookup(@registry, uuid) do
|
||||
[{pid, _}] ->
|
||||
GenServer.cast(pid, :stop)
|
||||
|
||||
_ ->
|
||||
Logger.warn("Unable to locate pool assigned to #{inspect(uuid)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,7 +77,7 @@ defmodule WandererApp.Map.Routes do
|
||||
end
|
||||
end
|
||||
|
||||
def find(_map_id, hubs, origin, routes_settings, true) do
|
||||
def find(_map_id, hubs, origin, _routes_settings, true) do
|
||||
origin = origin |> String.to_integer()
|
||||
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
|
||||
|
||||
|
||||
@@ -93,10 +93,8 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines the ship size for a connection, applying wormhole‑specific rules
|
||||
for C1, C13, and C4⇄NS links, falling back to the caller’s provided size or Large.
|
||||
"""
|
||||
# Determines the ship size for a connection, applying wormhole-specific rules
|
||||
# for C1, C13, and C4⇄NS links, falling back to the caller's provided size or Large.
|
||||
defp resolve_ship_size(type_val, ship_size_val, src_info, tgt_info) do
|
||||
case parse_type(type_val) do
|
||||
@connection_type_wormhole ->
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererApp.Map.Operations.Duplication do
|
||||
"""
|
||||
|
||||
require Logger
|
||||
import Ash.Query, only: [filter: 2]
|
||||
|
||||
alias WandererApp.Api
|
||||
alias WandererApp.Api.{MapSystem, MapConnection, MapSystemSignature, MapCharacterSettings}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
@moduledoc false
|
||||
@moduledoc """
|
||||
Handles character-related operations for map servers.
|
||||
|
||||
This module manages:
|
||||
- Character tracking on maps
|
||||
- Permission-based character cleanup
|
||||
- Character presence updates
|
||||
|
||||
## Logging
|
||||
|
||||
This module emits detailed logs for debugging character tracking issues:
|
||||
- INFO: Character track/untrack events, permission cleanup results
|
||||
- WARNING: Permission failures, unexpected states
|
||||
- DEBUG: Detailed permission check results
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
@@ -15,6 +29,11 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
if Enum.empty?(invalidate_character_ids) do
|
||||
:ok
|
||||
else
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Running permission cleanup for map #{map_id} - " <>
|
||||
"checking #{length(invalidate_character_ids)} characters"
|
||||
end)
|
||||
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
@@ -30,6 +49,11 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
def track_characters(_map_id, []), do: :ok
|
||||
|
||||
def track_characters(map_id, [character_id | rest]) do
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Starting tracking for character #{character_id} on map #{map_id} - " <>
|
||||
"reason: character joined presence"
|
||||
end)
|
||||
|
||||
track_character(map_id, character_id)
|
||||
track_characters(map_id, rest)
|
||||
end
|
||||
@@ -41,6 +65,12 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:characters, [])
|
||||
|
||||
if length(character_ids) > 0 do
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Scheduling permission check for #{length(character_ids)} characters on map #{map_id}"
|
||||
end)
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids)
|
||||
|
||||
:ok
|
||||
@@ -48,6 +78,13 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
|
||||
def untrack_characters(map_id, character_ids) do
|
||||
if length(character_ids) > 0 do
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Untracking #{length(character_ids)} characters from map #{map_id} - " <>
|
||||
"reason: characters no longer in presence_character_ids (grace period expired or user disconnected)"
|
||||
end)
|
||||
end
|
||||
|
||||
character_ids
|
||||
|> Enum.each(fn character_id ->
|
||||
character_map_active = is_character_map_active?(map_id, character_id)
|
||||
@@ -58,13 +95,32 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
|
||||
defp untrack_character(true, map_id, character_id) do
|
||||
Logger.info(fn ->
|
||||
"[CharactersImpl] Untracking character #{character_id} from map #{map_id} - " <>
|
||||
"character was actively tracking this map"
|
||||
end)
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :character, :tracking, :stopped],
|
||||
%{system_time: System.system_time()},
|
||||
%{character_id: character_id, map_id: map_id, reason: :presence_expired}
|
||||
)
|
||||
|
||||
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: false
|
||||
})
|
||||
end
|
||||
|
||||
defp untrack_character(_is_character_map_active, _map_id, _character_id), do: :ok
|
||||
defp untrack_character(false, map_id, character_id) do
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Skipping untrack for character #{character_id} on map #{map_id} - " <>
|
||||
"character was not actively tracking this map"
|
||||
end)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp is_character_map_active?(map_id, character_id) do
|
||||
case WandererApp.Character.get_character_state(character_id) do
|
||||
@@ -79,59 +135,134 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
defp process_invalidate_characters(invalidate_character_ids, map_id, acls) do
|
||||
{:ok, %{map: %{owner_id: owner_id}}} = WandererApp.Map.get_map_state(map_id)
|
||||
|
||||
invalidate_character_ids
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
character_id
|
||||
|> WandererApp.Character.get_character()
|
||||
|> case do
|
||||
{:ok, %{user_id: nil}} ->
|
||||
{:remove_character, character_id}
|
||||
# Option 3: Get owner's user_id to allow all characters from the same user
|
||||
owner_user_id = get_owner_user_id(owner_id)
|
||||
|
||||
{:ok, character} ->
|
||||
[character_permissions] =
|
||||
WandererApp.Permissions.check_characters_access([character], acls)
|
||||
|
||||
map_permissions =
|
||||
WandererApp.Permissions.get_map_permissions(
|
||||
character_permissions,
|
||||
owner_id,
|
||||
[character_id]
|
||||
)
|
||||
|
||||
case map_permissions do
|
||||
%{view_system: false} ->
|
||||
{:remove_character, character_id}
|
||||
|
||||
%{track_character: false} ->
|
||||
{:remove_character, character_id}
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end,
|
||||
timeout: :timer.seconds(60),
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.reduce([], fn
|
||||
{:ok, {:remove_character, character_id}}, acc ->
|
||||
[character_id | acc]
|
||||
|
||||
{:ok, _result}, acc ->
|
||||
acc
|
||||
|
||||
{:error, reason}, acc ->
|
||||
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
|
||||
acc
|
||||
Logger.debug(fn ->
|
||||
"[CharacterCleanup] Map #{map_id} - validating permissions for #{length(invalidate_character_ids)} characters"
|
||||
end)
|
||||
|> case do
|
||||
[] -> :ok
|
||||
character_ids_to_remove -> remove_and_untrack_characters(map_id, character_ids_to_remove)
|
||||
|
||||
results =
|
||||
invalidate_character_ids
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
character_id
|
||||
|> WandererApp.Character.get_character()
|
||||
|> case do
|
||||
{:ok, %{user_id: nil}} ->
|
||||
{:remove_character, character_id, :no_user_id}
|
||||
|
||||
{:ok, character} ->
|
||||
# Option 3: Check if character belongs to the same user as owner
|
||||
is_same_user_as_owner =
|
||||
owner_user_id != nil and character.user_id == owner_user_id
|
||||
|
||||
if is_same_user_as_owner do
|
||||
# All characters from the map owner's account have full access
|
||||
:ok
|
||||
else
|
||||
[character_permissions] =
|
||||
WandererApp.Permissions.check_characters_access([character], acls)
|
||||
|
||||
map_permissions =
|
||||
WandererApp.Permissions.get_map_permissions(
|
||||
character_permissions,
|
||||
owner_id,
|
||||
[character_id]
|
||||
)
|
||||
|
||||
case map_permissions do
|
||||
%{view_system: false} ->
|
||||
{:remove_character, character_id, :no_view_permission}
|
||||
|
||||
%{track_character: false} ->
|
||||
{:remove_character, character_id, :no_track_permission}
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end,
|
||||
timeout: :timer.seconds(60),
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.reduce([], fn
|
||||
{:ok, {:remove_character, character_id, reason}}, acc ->
|
||||
[{character_id, reason} | acc]
|
||||
|
||||
{:ok, _result}, acc ->
|
||||
acc
|
||||
|
||||
{:error, reason}, acc ->
|
||||
Logger.error(
|
||||
"[CharacterCleanup] Error checking character permissions: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
acc
|
||||
end)
|
||||
|
||||
case results do
|
||||
[] ->
|
||||
Logger.debug(fn ->
|
||||
"[CharacterCleanup] Map #{map_id} - all #{length(invalidate_character_ids)} characters passed permission check"
|
||||
end)
|
||||
|
||||
:ok
|
||||
|
||||
characters_to_remove ->
|
||||
# Group by reason for better logging
|
||||
by_reason = Enum.group_by(characters_to_remove, fn {_id, reason} -> reason end)
|
||||
|
||||
Enum.each(by_reason, fn {reason, chars} ->
|
||||
char_ids = Enum.map(chars, fn {id, _} -> id end)
|
||||
reason_str = permission_removal_reason_to_string(reason)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"[CharacterCleanup] Map #{map_id} - removing #{length(char_ids)} characters: #{reason_str} - " <>
|
||||
"character_ids: #{inspect(char_ids)}"
|
||||
end)
|
||||
|
||||
# Emit telemetry for each removal reason
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :character, :tracking, :permission_revoked],
|
||||
%{count: length(char_ids), system_time: System.system_time()},
|
||||
%{map_id: map_id, character_ids: char_ids, reason: reason}
|
||||
)
|
||||
end)
|
||||
|
||||
character_ids_to_remove = Enum.map(characters_to_remove, fn {id, _} -> id end)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"[CharacterCleanup] Map #{map_id} - total #{length(character_ids_to_remove)} characters " <>
|
||||
"will be removed due to permission issues (NO GRACE PERIOD)"
|
||||
end)
|
||||
|
||||
remove_and_untrack_characters(map_id, character_ids_to_remove)
|
||||
end
|
||||
end
|
||||
|
||||
defp permission_removal_reason_to_string(:no_user_id),
|
||||
do: "no user_id associated with character"
|
||||
|
||||
defp permission_removal_reason_to_string(:no_view_permission), do: "lost view_system permission"
|
||||
|
||||
defp permission_removal_reason_to_string(:no_track_permission),
|
||||
do: "lost track_character permission"
|
||||
|
||||
defp permission_removal_reason_to_string(reason), do: "#{inspect(reason)}"
|
||||
|
||||
# Helper to get the owner's user_id for Option 3
|
||||
defp get_owner_user_id(nil), do: nil
|
||||
|
||||
defp get_owner_user_id(owner_id) do
|
||||
case WandererApp.Character.get_character(owner_id) do
|
||||
{:ok, %{user_id: user_id}} -> user_id
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@@ -161,10 +292,18 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
|
||||
defp remove_and_untrack_characters(map_id, character_ids) do
|
||||
Logger.debug(fn ->
|
||||
"Map #{map_id} - remove and untrack characters #{inspect(character_ids)}"
|
||||
# Option 4: Enhanced logging for character removal
|
||||
Logger.info(fn ->
|
||||
"[CharacterCleanup] Map #{map_id} - starting removal of #{length(character_ids)} characters: #{inspect(character_ids)}"
|
||||
end)
|
||||
|
||||
# Emit telemetry for monitoring
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :characters_cleanup, :removal_started],
|
||||
%{character_count: length(character_ids), system_time: System.system_time()},
|
||||
%{map_id: map_id, character_ids: character_ids}
|
||||
)
|
||||
|
||||
map_id
|
||||
|> untrack_characters(character_ids)
|
||||
|
||||
@@ -174,10 +313,21 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:ok, settings} ->
|
||||
settings
|
||||
|> Enum.each(fn s ->
|
||||
Logger.info(fn ->
|
||||
"[CharacterCleanup] Map #{map_id} - destroying settings and removing character #{s.character_id}"
|
||||
end)
|
||||
|
||||
WandererApp.MapCharacterSettingsRepo.destroy!(s)
|
||||
remove_character(map_id, s.character_id)
|
||||
end)
|
||||
|
||||
# Emit telemetry for successful removal
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :characters_cleanup, :removal_complete],
|
||||
%{removed_count: length(settings), system_time: System.system_time()},
|
||||
%{map_id: map_id}
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
@@ -664,21 +814,33 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
do: :ok
|
||||
|
||||
defp update_location(
|
||||
%{map: %{scope: scope}, map_id: map_id, map_opts: map_opts} =
|
||||
%{map: map, map_id: map_id, map_opts: map_opts} =
|
||||
_state,
|
||||
character_id,
|
||||
location,
|
||||
old_location
|
||||
) do
|
||||
ConnectionsImpl.is_connection_valid(
|
||||
scope,
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id
|
||||
scopes = get_effective_scopes(map)
|
||||
|
||||
is_valid =
|
||||
ConnectionsImpl.is_connection_valid(
|
||||
scopes,
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id
|
||||
)
|
||||
|
||||
Logger.debug(
|
||||
"[CharacterTracking] update_location: map=#{map_id}, " <>
|
||||
"from=#{old_location.solar_system_id}, to=#{location.solar_system_id}, " <>
|
||||
"scopes=#{inspect(scopes)}, map.scopes=#{inspect(map[:scopes])}, " <>
|
||||
"map.scope=#{inspect(map[:scope])}, is_valid=#{is_valid}"
|
||||
)
|
||||
|> case do
|
||||
|
||||
case is_valid do
|
||||
true ->
|
||||
# Add new location system
|
||||
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts) do
|
||||
# Connection is valid (at least one system matches scopes)
|
||||
# Add systems that match the map's scopes - individual system filtering by maybe_add_system
|
||||
case SystemsImpl.maybe_add_system(map_id, location, old_location, map_opts, scopes) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
@@ -688,8 +850,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
)
|
||||
end
|
||||
|
||||
# Add old location system (in case it wasn't on map)
|
||||
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts) do
|
||||
# Add old location system (in case it wasn't on map) - only if it matches scopes
|
||||
case SystemsImpl.maybe_add_system(map_id, old_location, location, map_opts, scopes) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
@@ -729,6 +891,24 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
defp is_character_in_space?(%{station_id: station_id, structure_id: structure_id} = _location),
|
||||
do: is_nil(structure_id) && is_nil(station_id)
|
||||
|
||||
@doc """
|
||||
Get effective scopes from map, with fallback to legacy scope.
|
||||
Returns the scopes array that should be used for filtering.
|
||||
"""
|
||||
def get_effective_scopes(%{scopes: scopes}) when is_list(scopes) and scopes != [], do: scopes
|
||||
|
||||
def get_effective_scopes(%{scope: scope}) when is_atom(scope),
|
||||
do: legacy_scope_to_scopes(scope)
|
||||
|
||||
def get_effective_scopes(_), do: [:wormholes]
|
||||
|
||||
# Legacy scope to new scopes array conversion
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
defp add_character(
|
||||
map_id,
|
||||
%{id: character_id} = map_character,
|
||||
|
||||
@@ -57,6 +57,12 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
@known_space [@hs, @ls, @ns, @pochven]
|
||||
|
||||
# Individual space type lists for granular scope matching
|
||||
@hi_space [@hs]
|
||||
@low_space [@ls]
|
||||
@null_space [@ns]
|
||||
@pochven_space [@pochven]
|
||||
|
||||
@prohibited_systems [@jita]
|
||||
@prohibited_system_classes [
|
||||
@a1,
|
||||
@@ -100,7 +106,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
@connection_type_wormhole 0
|
||||
@connection_type_stargate 1
|
||||
@connection_type_bridge 2
|
||||
# @connection_type_bridge 2 # reserved for future use
|
||||
@medium_ship_size 1
|
||||
|
||||
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
|
||||
@@ -290,6 +296,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
do: update_connection(map_id, :update_custom_info, [:custom_info], connection_update)
|
||||
|
||||
def cleanup_connections(map_id) do
|
||||
# Defensive check: Skip cleanup if cache appears invalid
|
||||
# This prevents incorrectly deleting connections when cache is empty due to
|
||||
# race conditions during map restart or cache corruption
|
||||
case WandererApp.Map.get_map(map_id) do
|
||||
{:error, :not_found} ->
|
||||
Logger.warning(
|
||||
"[cleanup_connections] Skipping map #{map_id} - cache miss detected, " <>
|
||||
"map data not found in cache"
|
||||
)
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :cleanup_connections, :cache_miss],
|
||||
%{system_time: System.system_time()},
|
||||
%{map_id: map_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:ok, _map} ->
|
||||
do_cleanup_connections(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cleanup_connections(map_id) do
|
||||
connection_auto_expire_hours = get_connection_auto_expire_hours()
|
||||
connection_auto_eol_hours = get_connection_auto_eol_hours()
|
||||
connection_eol_expire_timeout_hours = get_eol_expire_timeout_mins() / 60
|
||||
@@ -343,6 +373,27 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
solar_system_source: solar_system_source_id,
|
||||
solar_system_target: solar_system_target_id
|
||||
} ->
|
||||
# Emit telemetry for connection auto-deletion
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :connection_cleanup, :delete],
|
||||
%{system_time: System.system_time()},
|
||||
%{
|
||||
map_id: map_id,
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id,
|
||||
reason: :auto_cleanup
|
||||
}
|
||||
)
|
||||
|
||||
# Log auto-deletion for audit trail (no user/character context for auto-cleanup)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id
|
||||
})
|
||||
|
||||
delete_connection(map_id, %{
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id
|
||||
@@ -403,7 +454,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
time_status: time_status,
|
||||
solar_system_source: solar_system_source,
|
||||
solar_system_target: solar_system_target
|
||||
} = updated_connection
|
||||
} = _updated_connection
|
||||
) do
|
||||
with source_system when not is_nil(source_system) <-
|
||||
WandererApp.Map.find_system_by_location(
|
||||
@@ -644,31 +695,49 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
start_time
|
||||
)
|
||||
|
||||
def can_add_location(_scope, nil), do: false
|
||||
def can_add_location(_scopes, nil), do: false
|
||||
|
||||
def can_add_location(:none, _solar_system_id), do: false
|
||||
def can_add_location([], _solar_system_id), do: false
|
||||
|
||||
def can_add_location(scope, solar_system_id) do
|
||||
def can_add_location(scopes, solar_system_id) when is_list(scopes) do
|
||||
{:ok, system_static_info} = get_system_static_info(solar_system_id)
|
||||
|
||||
case scope do
|
||||
:wormholes ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
|
||||
@wh_space |> Enum.member?(system_static_info.system_class)
|
||||
|
||||
:stargates ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
@known_space |> Enum.member?(system_static_info.system_class)
|
||||
|
||||
:all ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
|
||||
system_matches_any_scope?(system_static_info.system_class, scopes)
|
||||
end
|
||||
|
||||
# Legacy support for single scope atom
|
||||
def can_add_location(:none, _solar_system_id), do: false
|
||||
|
||||
def can_add_location(scope, solar_system_id) when is_atom(scope) do
|
||||
can_add_location(legacy_scope_to_scopes(scope), solar_system_id)
|
||||
end
|
||||
|
||||
# Helper function to check if a system class matches any of the selected scopes
|
||||
defp system_matches_any_scope?(_system_class, []), do: false
|
||||
|
||||
defp system_matches_any_scope?(system_class, scopes) do
|
||||
Enum.any?(scopes, fn scope ->
|
||||
system_matches_scope?(system_class, scope)
|
||||
end)
|
||||
end
|
||||
|
||||
# Individual scope matching functions
|
||||
defp system_matches_scope?(system_class, :wormholes), do: system_class in @wh_space
|
||||
defp system_matches_scope?(system_class, :hi), do: system_class in @hi_space
|
||||
defp system_matches_scope?(system_class, :low), do: system_class in @low_space
|
||||
defp system_matches_scope?(system_class, :null), do: system_class in @null_space
|
||||
defp system_matches_scope?(system_class, :pochven), do: system_class in @pochven_space
|
||||
defp system_matches_scope?(_system_class, _), do: false
|
||||
|
||||
# Legacy scope to new scopes array conversion
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
def is_prohibited_system_class?(system_class) do
|
||||
@prohibited_system_classes |> Enum.member?(system_class)
|
||||
end
|
||||
@@ -688,17 +757,59 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
)
|
||||
)
|
||||
|
||||
def is_connection_valid(_scope, from_solar_system_id, to_solar_system_id)
|
||||
def is_connection_valid(_scopes, from_solar_system_id, to_solar_system_id)
|
||||
when is_nil(from_solar_system_id) or is_nil(to_solar_system_id),
|
||||
do: false
|
||||
|
||||
def is_connection_valid([], _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
# New array-based scopes support
|
||||
def is_connection_valid(scopes, from_solar_system_id, to_solar_system_id)
|
||||
when is_list(scopes) and from_solar_system_id != to_solar_system_id do
|
||||
with {:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
|
||||
{:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do
|
||||
# First check: neither system is prohibited
|
||||
not_prohibited =
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
|
||||
not (@prohibited_systems |> Enum.member?(to_solar_system_id))
|
||||
|
||||
if not_prohibited do
|
||||
from_is_wormhole = from_system_static_info.system_class in @wh_space
|
||||
to_is_wormhole = to_system_static_info.system_class in @wh_space
|
||||
wormholes_enabled = :wormholes in scopes
|
||||
|
||||
# Wormhole border behavior: if wormholes scope is enabled AND at least one
|
||||
# system is a wormhole, allow the connection (adds border k-space systems)
|
||||
# Otherwise: BOTH systems must match the configured scopes
|
||||
if wormholes_enabled and (from_is_wormhole or to_is_wormhole) do
|
||||
# At least one system matches (wormhole matches :wormholes, or other matches its scope)
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) or
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
else
|
||||
# Non-wormhole movement: both systems must match scopes
|
||||
system_matches_any_scope?(from_system_static_info.system_class, scopes) and
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes)
|
||||
end
|
||||
else
|
||||
false
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy support: :all scope
|
||||
def is_connection_valid(:all, from_solar_system_id, to_solar_system_id),
|
||||
do: from_solar_system_id != to_solar_system_id
|
||||
|
||||
# Legacy support: :none scope
|
||||
def is_connection_valid(:none, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
# Legacy support: single atom scope (including :stargates which is used for connection type detection)
|
||||
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
|
||||
when from_solar_system_id != to_solar_system_id do
|
||||
when is_atom(scope) and from_solar_system_id != to_solar_system_id do
|
||||
with {:ok, known_jumps} <- find_solar_system_jump(from_solar_system_id, to_solar_system_id),
|
||||
{:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
|
||||
{:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do
|
||||
@@ -712,7 +823,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
:stargates ->
|
||||
# For stargates, we need to check:
|
||||
# 1. Both systems are in known space (HS, LS, NS)
|
||||
# 1. Both systems are in known space (HS, LS, NS, Pochven)
|
||||
# 2. There is a known jump between them
|
||||
# 3. Neither system is prohibited
|
||||
from_system_static_info.system_class in @known_space and
|
||||
@@ -720,13 +831,21 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (known_jumps |> Enum.empty?())
|
||||
|
||||
_ ->
|
||||
# For other legacy scopes, convert to array and use new logic
|
||||
is_connection_valid(
|
||||
legacy_scope_to_scopes(scope),
|
||||
from_solar_system_id,
|
||||
to_solar_system_id
|
||||
)
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def is_connection_valid(_scope, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
def is_connection_valid(_scopes, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
def get_connection_mark_eol_time(map_id, connection_id, default \\ DateTime.utc_now()) do
|
||||
WandererApp.Cache.get("map_#{map_id}:conn_#{connection_id}:mark_eol_time")
|
||||
@@ -859,14 +978,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 +1020,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_time_status(_source_solar_system_id, _target_solar_system_id, _ship_size_type),
|
||||
do: @connection_time_status_default
|
||||
|
||||
defp get_new_time_status(_start_time, @connection_time_status_default),
|
||||
do: @connection_time_status_eol_24
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -662,12 +662,45 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
not Enum.member?(presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
# Log presence changes for debugging
|
||||
if length(new_present_character_ids) > 0 or length(not_present_character_ids) > 0 do
|
||||
Logger.debug(fn ->
|
||||
"[MapServer] Map #{map_id} presence update - " <>
|
||||
"newly_present: #{inspect(new_present_character_ids)}, " <>
|
||||
"no_longer_present: #{inspect(not_present_character_ids)}, " <>
|
||||
"total_present: #{length(presence_character_ids)}"
|
||||
end)
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:old_presence_character_ids",
|
||||
presence_character_ids
|
||||
)
|
||||
|
||||
# Track new characters
|
||||
if length(new_present_character_ids) > 0 do
|
||||
Logger.debug(fn ->
|
||||
"[MapServer] Map #{map_id} - starting tracking for #{length(new_present_character_ids)} newly present characters"
|
||||
end)
|
||||
end
|
||||
|
||||
CharactersImpl.track_characters(map_id, new_present_character_ids)
|
||||
|
||||
# Untrack characters no longer present (grace period has expired)
|
||||
if length(not_present_character_ids) > 0 do
|
||||
Logger.debug(fn ->
|
||||
"[MapServer] Map #{map_id} - #{length(not_present_character_ids)} characters no longer in presence " <>
|
||||
"(grace period expired or never had one) - will be untracked"
|
||||
end)
|
||||
|
||||
# Emit telemetry for presence-based untracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :presence, :characters_left],
|
||||
%{count: length(not_present_character_ids), system_time: System.system_time()},
|
||||
%{map_id: map_id, character_ids: not_present_character_ids}
|
||||
)
|
||||
end
|
||||
|
||||
CharactersImpl.untrack_characters(map_id, not_present_character_ids)
|
||||
|
||||
broadcast!(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -256,6 +256,37 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
|
||||
defp maybe_update_connection_mass_status(_map_id, _old_sig, _updated_sig), do: :ok
|
||||
|
||||
@doc """
|
||||
Wrapper for updating a signature's linked_system_id with logging.
|
||||
Logs all unlink operations (when linked_system_id is set to nil) with context
|
||||
to help diagnose unexpected unlinking issues.
|
||||
"""
|
||||
def update_signature_linked_system(signature, %{linked_system_id: nil} = params) do
|
||||
# Log all unlink operations with context for debugging
|
||||
Logger.warning(
|
||||
"[Signature Unlink] eve_id=#{signature.eve_id} " <>
|
||||
"system_id=#{signature.system_id} " <>
|
||||
"old_linked_system_id=#{signature.linked_system_id} " <>
|
||||
"stacktrace=#{format_stacktrace()}"
|
||||
)
|
||||
|
||||
MapSystemSignature.update_linked_system(signature, params)
|
||||
end
|
||||
|
||||
def update_signature_linked_system(signature, params) do
|
||||
MapSystemSignature.update_linked_system(signature, params)
|
||||
end
|
||||
|
||||
defp format_stacktrace do
|
||||
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
|
||||
|
||||
stacktrace
|
||||
|> Enum.take(10)
|
||||
|> Enum.map_join(" <- ", fn {mod, fun, arity, _} ->
|
||||
"#{inspect(mod)}.#{fun}/#{arity}"
|
||||
end)
|
||||
end
|
||||
|
||||
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
|
||||
ActivityTracker.track_map_event(event, %{
|
||||
map_id: map_id,
|
||||
|
||||
@@ -4,6 +4,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Map.Server.Impl
|
||||
alias WandererApp.Map.Server.SignaturesImpl
|
||||
|
||||
@ddrt Application.compile_env(:wanderer_app, :ddrt)
|
||||
@system_auto_expire_minutes 15
|
||||
@@ -129,8 +130,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
def remove_system_comment(
|
||||
map_id,
|
||||
comment_id,
|
||||
user_id,
|
||||
character_id
|
||||
_user_id,
|
||||
_character_id
|
||||
) do
|
||||
{:ok, %{system_id: system_id} = comment} =
|
||||
WandererApp.MapSystemCommentRepo.get_by_id(comment_id)
|
||||
@@ -146,6 +147,30 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end
|
||||
|
||||
def cleanup_systems(map_id) do
|
||||
# Defensive check: Skip cleanup if cache appears invalid
|
||||
# This prevents incorrectly deleting systems when cache is empty due to
|
||||
# race conditions during map restart or cache corruption
|
||||
case WandererApp.Map.get_map(map_id) do
|
||||
{:error, :not_found} ->
|
||||
Logger.warning(
|
||||
"[cleanup_systems] Skipping map #{map_id} - cache miss detected, " <>
|
||||
"map data not found in cache"
|
||||
)
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :cleanup_systems, :cache_miss],
|
||||
%{system_time: System.system_time()},
|
||||
%{map_id: map_id}
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:ok, _map} ->
|
||||
do_cleanup_systems(map_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_cleanup_systems(map_id) do
|
||||
expired_systems =
|
||||
map_id
|
||||
|> WandererApp.Map.list_systems!()
|
||||
@@ -309,7 +334,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
map_id
|
||||
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
|
||||
|> case do
|
||||
{:ok, result} ->
|
||||
{:ok, _result} ->
|
||||
:ok = WandererApp.Map.remove_system(map_id, solar_system_id)
|
||||
@ddrt.delete([solar_system_id], "rtree_#{map_id}")
|
||||
Impl.broadcast!(map_id, :systems_removed, [solar_system_id])
|
||||
@@ -383,6 +408,16 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|> Enum.each(fn connection ->
|
||||
try do
|
||||
Logger.debug(fn -> "Removing connection from map: #{inspect(connection)}" end)
|
||||
|
||||
# Audit logging for cascade deletion (no user/character context)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_source_id: connection.solar_system_source,
|
||||
solar_system_target_id: connection.solar_system_target
|
||||
})
|
||||
|
||||
:ok = WandererApp.MapConnectionRepo.destroy(map_id, connection)
|
||||
:ok = WandererApp.Map.remove_connection(map_id, connection)
|
||||
Impl.broadcast!(map_id, :remove_connections, [connection])
|
||||
@@ -393,26 +428,77 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end)
|
||||
end
|
||||
|
||||
# When destination systems are deleted, unlink signatures instead of destroying them.
|
||||
# This preserves the user's scan data while removing the stale link.
|
||||
defp cleanup_linked_signatures(map_id, removed_solar_system_ids) do
|
||||
removed_solar_system_ids
|
||||
|> Enum.map(fn solar_system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
|
||||
end)
|
||||
|> List.flatten()
|
||||
|> Enum.uniq_by(& &1.system_id)
|
||||
|> Enum.each(fn s ->
|
||||
try do
|
||||
{:ok, %{eve_id: eve_id, system: system}} = s |> Ash.load([:system])
|
||||
:ok = Ash.destroy!(s)
|
||||
# Group signatures by their source system for efficient broadcasting
|
||||
signatures_by_system =
|
||||
removed_solar_system_ids
|
||||
|> Enum.flat_map(fn solar_system_id ->
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id!(solar_system_id)
|
||||
end)
|
||||
|> Enum.uniq_by(& &1.id)
|
||||
|> Enum.group_by(fn sig -> sig.system_id end)
|
||||
|
||||
Logger.warning(
|
||||
"[cleanup_linked_signatures] for system #{system.solar_system_id}: #{inspect(eve_id)}"
|
||||
)
|
||||
signatures_by_system
|
||||
|> Enum.each(fn {_system_id, signatures} ->
|
||||
signatures
|
||||
|> Enum.each(fn sig ->
|
||||
try do
|
||||
{:ok, %{eve_id: eve_id, system: system}} = sig |> Ash.load([:system])
|
||||
|
||||
Impl.broadcast!(map_id, :signatures_updated, system.solar_system_id)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to cleanup linked signature: #{inspect(e)}")
|
||||
# Clear the linked_system_id instead of destroying the signature
|
||||
# Use the wrapper to log unlink operations
|
||||
case SignaturesImpl.update_signature_linked_system(sig, %{
|
||||
linked_system_id: nil
|
||||
}) do
|
||||
{:ok, _updated_sig} ->
|
||||
case system do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] signature #{eve_id} unlinked (parent system already deleted)"
|
||||
end)
|
||||
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] unlinked signature #{eve_id} in system #{solar_system_id}"
|
||||
end)
|
||||
|
||||
# Audit logging for cascade unlink (no user/character context)
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_unlinked, %{
|
||||
character_id: nil,
|
||||
user_id: nil,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id,
|
||||
signatures: [eve_id]
|
||||
})
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"[cleanup_linked_signatures] Failed to unlink signature #{sig.eve_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Failed to cleanup linked signature: #{inspect(e)}")
|
||||
end
|
||||
end)
|
||||
|
||||
# Broadcast once per source system after all its signatures are processed
|
||||
case List.first(signatures) do
|
||||
%{system: %{solar_system_id: solar_system_id}} ->
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
|
||||
_ ->
|
||||
# Try to get the system info if not preloaded
|
||||
case List.first(signatures) |> Ash.load([:system]) do
|
||||
{:ok, %{system: %{solar_system_id: solar_system_id}}} ->
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -437,8 +523,47 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end)
|
||||
end
|
||||
|
||||
def maybe_add_system(map_id, location, old_location, map_opts)
|
||||
def maybe_add_system(map_id, location, old_location, map_opts, scopes \\ nil)
|
||||
|
||||
def maybe_add_system(map_id, location, old_location, map_opts, scopes)
|
||||
when not is_nil(location) do
|
||||
alias WandererApp.Map.Server.ConnectionsImpl
|
||||
|
||||
# Check if the system matches the map's configured scopes before adding
|
||||
should_add =
|
||||
case scopes do
|
||||
nil ->
|
||||
true
|
||||
|
||||
[] ->
|
||||
true
|
||||
|
||||
scopes when is_list(scopes) ->
|
||||
# First check: does the location directly match scopes?
|
||||
if ConnectionsImpl.can_add_location(scopes, location.solar_system_id) do
|
||||
true
|
||||
else
|
||||
# Second check: wormhole border behavior
|
||||
# If :wormholes scope is enabled AND old_location is a wormhole,
|
||||
# allow this system to be added as a border system (so you can see
|
||||
# where your wormhole exits to)
|
||||
:wormholes in scopes and
|
||||
not is_nil(old_location) and
|
||||
ConnectionsImpl.can_add_location([:wormholes], old_location.solar_system_id)
|
||||
end
|
||||
end
|
||||
|
||||
if should_add do
|
||||
do_add_system_from_location(map_id, location, old_location, map_opts)
|
||||
else
|
||||
# System filtered out by scope settings - this is expected behavior
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_add_system(_map_id, _location, _old_location, _map_opts, _scopes), do: :ok
|
||||
|
||||
defp do_add_system_from_location(map_id, location, old_location, map_opts) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :system_addition, :start],
|
||||
%{system_time: System.system_time()},
|
||||
@@ -517,12 +642,14 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|> case do
|
||||
{:ok, solar_system_info} ->
|
||||
# Use upsert instead of create - handles race conditions gracefully
|
||||
# visible: true ensures previously-deleted systems become visible again
|
||||
WandererApp.MapSystemRepo.upsert(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: location.solar_system_id,
|
||||
name: solar_system_info.solar_system_name,
|
||||
position_x: position.x,
|
||||
position_y: position.y
|
||||
position_y: position.y,
|
||||
visible: true
|
||||
})
|
||||
|> case do
|
||||
{:ok, system} ->
|
||||
@@ -644,8 +771,6 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_add_system(_map_id, _location, _old_location, _map_opts), do: :ok
|
||||
|
||||
defp do_add_system(
|
||||
map_id,
|
||||
%{
|
||||
@@ -670,7 +795,11 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|
||||
_ ->
|
||||
%{x: x, y: y} =
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts)
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(
|
||||
nil,
|
||||
rtree_name,
|
||||
map_opts
|
||||
)
|
||||
|
||||
%{"x" => x, "y" => y}
|
||||
end
|
||||
@@ -733,7 +862,10 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
})
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to get system static info for #{solar_system_id}: #{inspect(reason)}")
|
||||
Logger.error(
|
||||
"Failed to get system static info for #{solar_system_id}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, :system_info_not_found}
|
||||
end
|
||||
end
|
||||
@@ -766,7 +898,10 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
:ok
|
||||
|
||||
{:error, reason} = error ->
|
||||
Logger.error("Failed to add system #{solar_system_id} to map #{map_id}: #{inspect(reason)}")
|
||||
Logger.error(
|
||||
"Failed to add system #{solar_system_id} to map #{map_id}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
error
|
||||
end
|
||||
else
|
||||
@@ -854,10 +989,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
updated_system
|
||||
end
|
||||
|
||||
defp maybe_update_labels(system, _labels), do: system
|
||||
|
||||
defp maybe_update_labels(
|
||||
%{name: old_labels} = system,
|
||||
%{labels: old_labels} = system,
|
||||
labels
|
||||
)
|
||||
when not is_nil(labels) and old_labels != labels do
|
||||
@@ -971,12 +1104,16 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
# ADDITIVE: Also broadcast to external event system (webhooks/WebSocket)
|
||||
# This may fail if the relay is not available (e.g., in tests), which is fine
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :system_metadata_changed, %{
|
||||
system_id: updated_system.id,
|
||||
solar_system_id: updated_system.solar_system_id,
|
||||
name: updated_system.name,
|
||||
temporary_name: updated_system.temporary_name,
|
||||
labels: updated_system.labels,
|
||||
description: updated_system.description,
|
||||
status: updated_system.status
|
||||
status: updated_system.status,
|
||||
locked: updated_system.locked,
|
||||
position_x: updated_system.position_x,
|
||||
position_y: updated_system.position_y
|
||||
})
|
||||
|
||||
:ok
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
use WandererApp, :repository
|
||||
|
||||
require Logger
|
||||
|
||||
def get(map_id, character_id) do
|
||||
case WandererApp.Api.MapCharacterSettings.read_by_map_and_character(%{
|
||||
map_id: map_id,
|
||||
@@ -54,13 +56,12 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
|
||||
|
||||
def track(%{map_id: map_id, character_id: character_id}) do
|
||||
# Only update the tracked field, preserving other fields
|
||||
case WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: map_id,
|
||||
character_id: character_id
|
||||
}) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
# First ensure the record exists (get creates if not exists)
|
||||
case get(map_id, character_id) do
|
||||
{:ok, settings} when not is_nil(settings) ->
|
||||
# Now update the tracked field
|
||||
settings
|
||||
|> WandererApp.Api.MapCharacterSettings.update(%{tracked: true})
|
||||
|
||||
error ->
|
||||
Logger.error(
|
||||
@@ -72,13 +73,12 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def untrack(%{map_id: map_id, character_id: character_id}) do
|
||||
# Only update the tracked field, preserving other fields
|
||||
case WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: map_id,
|
||||
character_id: character_id
|
||||
}) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
# First ensure the record exists (get creates if not exists)
|
||||
case get(map_id, character_id) do
|
||||
{:ok, settings} when not is_nil(settings) ->
|
||||
# Now update the tracked field
|
||||
settings
|
||||
|> WandererApp.Api.MapCharacterSettings.update(%{tracked: false})
|
||||
|
||||
error ->
|
||||
Logger.error(
|
||||
@@ -103,18 +103,36 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
end
|
||||
|
||||
def follow(settings) do
|
||||
WandererApp.Api.MapCharacterSettings.follow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
})
|
||||
def follow(%{map_id: map_id, character_id: character_id} = _settings) do
|
||||
# First ensure the record exists (get creates if not exists)
|
||||
case get(map_id, character_id) do
|
||||
{:ok, settings} when not is_nil(settings) ->
|
||||
settings
|
||||
|> WandererApp.Api.MapCharacterSettings.update(%{followed: true})
|
||||
|
||||
error ->
|
||||
Logger.error(
|
||||
"Failed to follow character: #{character_id} on map: #{map_id}, #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow(settings) do
|
||||
WandererApp.Api.MapCharacterSettings.unfollow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
})
|
||||
def unfollow(%{map_id: map_id, character_id: character_id} = _settings) do
|
||||
# First ensure the record exists (get creates if not exists)
|
||||
case get(map_id, character_id) do
|
||||
{:ok, settings} when not is_nil(settings) ->
|
||||
settings
|
||||
|> WandererApp.Api.MapCharacterSettings.update(%{followed: false})
|
||||
|
||||
error ->
|
||||
Logger.error(
|
||||
"Failed to unfollow character: #{character_id} on map: #{map_id}, #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
def follow!(settings) do
|
||||
|
||||
@@ -99,7 +99,7 @@ defmodule WandererApp.MapConnectionRepo do
|
||||
def get_by_id(map_id, id) do
|
||||
# Use read_by_map action which doesn't have the FilterConnectionsByActorMap preparation
|
||||
# that was causing "filter being false" errors in tests
|
||||
import Ash.Query
|
||||
require Ash.Query
|
||||
|
||||
WandererApp.Api.MapConnection
|
||||
|> Ash.Query.for_read(:read_by_map, %{map_id: map_id})
|
||||
|
||||
@@ -38,6 +38,4 @@ defmodule WandererApp.MapPingsRepo do
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def destroy(_ping_id), do: :ok
|
||||
end
|
||||
|
||||
@@ -84,7 +84,7 @@ defmodule WandererApp.MapRepo do
|
||||
end
|
||||
end
|
||||
|
||||
error in Ash.Error.Query.NotFound ->
|
||||
_error in Ash.Error.Query.NotFound ->
|
||||
Logger.debug("Map not found with slug: #{slug}")
|
||||
{:error, :not_found}
|
||||
|
||||
|
||||
@@ -487,15 +487,6 @@ defmodule WandererApp.SecurityAudit do
|
||||
|
||||
# Private functions
|
||||
|
||||
defp store_audit_entry(_audit_entry) do
|
||||
# Handle async processing if enabled
|
||||
# if async_enabled?() do
|
||||
# WandererApp.SecurityAudit.AsyncProcessor.log_event(audit_entry)
|
||||
# else
|
||||
# do_store_audit_entry(audit_entry)
|
||||
# end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def do_store_audit_entry(audit_entry) do
|
||||
# Ensure event_type is properly formatted
|
||||
@@ -631,11 +622,6 @@ defmodule WandererApp.SecurityAudit do
|
||||
end
|
||||
end
|
||||
|
||||
defp async_enabled? do
|
||||
Application.get_env(:wanderer_app, __MODULE__, [])
|
||||
|> Keyword.get(:async, false)
|
||||
end
|
||||
|
||||
defp emit_telemetry_event(audit_entry) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :security_audit],
|
||||
|
||||
@@ -5,7 +5,11 @@ defmodule WandererApp.Test.Logger do
|
||||
"""
|
||||
|
||||
@callback info(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback info(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback error(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback error(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback warning(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback warning(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback debug(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback debug(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
end
|
||||
|
||||
@@ -9,12 +9,24 @@ defmodule WandererApp.Test.LoggerStub do
|
||||
@impl true
|
||||
def info(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def info(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def error(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def error(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def warning(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def warning(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def debug(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def debug(_message, _metadata), do: :ok
|
||||
end
|
||||
|
||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Vault do
|
||||
end)
|
||||
end
|
||||
|
||||
defp find_fallback_module_to_decrypt(config, ciphertext) do
|
||||
defp find_fallback_module_to_decrypt(config, _ciphertext) do
|
||||
Enum.find(config[:ciphers], fn {label, _} ->
|
||||
label == :fallback
|
||||
end)
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererAppWeb.ApiRouter do
|
||||
"""
|
||||
|
||||
use Phoenix.Router
|
||||
import WandererAppWeb.ApiRouterHelpers
|
||||
alias WandererAppWeb.{ApiRoutes, ApiRouter.RouteSpec}
|
||||
require Logger
|
||||
|
||||
@@ -171,7 +170,7 @@ defmodule WandererAppWeb.ApiRouter do
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp find_similar_routes(path_info, version) do
|
||||
defp find_similar_routes(path_info, _version) do
|
||||
# Find routes with similar paths in current or other versions
|
||||
all_routes = ApiRoutes.table()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule WandererAppWeb.ApiSpec do
|
||||
@behaviour OpenApiSpex.OpenApi
|
||||
|
||||
alias OpenApiSpex.{OpenApi, Info, Paths, Components, SecurityScheme, Server, Schema}
|
||||
alias OpenApiSpex.{OpenApi, Info, Paths, Components, SecurityScheme, Server}
|
||||
alias WandererAppWeb.{Endpoint, Router}
|
||||
alias WandererAppWeb.Schemas.ApiSchemas
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
"""
|
||||
attr(:type, :string, default: nil)
|
||||
attr(:class, :string, default: nil)
|
||||
attr(:data, :any, default: nil)
|
||||
attr(:rest, :global, include: ~w(disabled form name value))
|
||||
|
||||
slot(:inner_block, required: true)
|
||||
@@ -296,6 +297,7 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
"phx-submit-loading:opacity-75 p-button p-component p-button-outlined p-button-sm",
|
||||
@class
|
||||
]}
|
||||
data={@data}
|
||||
{@rest}
|
||||
>
|
||||
{render_slot(@inner_block)}
|
||||
@@ -614,7 +616,7 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
attr(:empty_label, :string, default: nil)
|
||||
attr(:rows, :list, required: true)
|
||||
attr(:row_id, :any, default: nil, doc: "the function for generating the row id")
|
||||
attr(:row_selected, :boolean, default: false, doc: "the function for generating the row id")
|
||||
attr(:row_selected, :any, default: false, doc: "the function for generating the row id")
|
||||
attr(:row_click, :any, default: nil, doc: "the function for handling phx-click on each row")
|
||||
|
||||
attr(:row_item, :any,
|
||||
@@ -703,13 +705,21 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:field, :any, required: true)
|
||||
attr(:placeholder, :string, default: nil)
|
||||
attr(:label, :string, default: nil)
|
||||
attr(:label_class, :string, default: nil)
|
||||
attr(:input_class, :string, default: nil)
|
||||
attr(:dropdown_extra_class, :string, default: nil)
|
||||
attr(:option_extra_class, :string, default: nil)
|
||||
attr(:mode, :atom, default: :single)
|
||||
attr(:options, :list, default: [])
|
||||
attr(:debounce, :integer, default: nil)
|
||||
attr(:update_min_len, :integer, default: nil)
|
||||
attr(:available_option_class, :string, default: nil)
|
||||
attr(:value_mapper, :any, default: nil)
|
||||
slot(:inner_block)
|
||||
slot(:option)
|
||||
|
||||
def live_select(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||
assigns =
|
||||
|
||||
@@ -23,6 +23,7 @@ defmodule WandererAppWeb.Layouts do
|
||||
|
||||
attr :app_version, :string
|
||||
attr :enabled, :boolean
|
||||
attr :latest_post, :any, default: nil
|
||||
|
||||
def new_version_banner(assigns) do
|
||||
~H"""
|
||||
@@ -36,27 +37,89 @@ defmodule WandererAppWeb.Layouts do
|
||||
>
|
||||
<div class="hs-overlay-backdrop transition duration absolute left-0 top-0 w-full h-full bg-gray-900 bg-opacity-50 dark:bg-opacity-80 dark:bg-neutral-900">
|
||||
</div>
|
||||
<div class="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex items-center">
|
||||
<div class="rounded w-9 h-9 w-[80px] h-[66px] flex items-center justify-center relative z-20">
|
||||
<.icon name="hero-chevron-double-right" class="w-9 h-9 mr-[-40px]" />
|
||||
</div>
|
||||
<div id="refresh-area">
|
||||
<.live_component module={WandererAppWeb.MapRefresh} id="map-refresh" />
|
||||
<div class="absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] flex flex-col items-center gap-6">
|
||||
<div class="flex items-center">
|
||||
<div class="rounded w-9 h-9 w-[80px] h-[66px] flex items-center justify-center relative z-20">
|
||||
<.icon name="hero-chevron-double-right" class="w-9 h-9 mr-[-40px]" />
|
||||
</div>
|
||||
<div id="refresh-area">
|
||||
<.live_component module={WandererAppWeb.MapRefresh} id="map-refresh" />
|
||||
</div>
|
||||
|
||||
<div class="rounded h-[66px] flex items-center justify-center relative z-20">
|
||||
<div class=" flex items-center w-[200px] h-full">
|
||||
<.icon name="hero-chevron-double-left" class="w-9 h-9 mr-[20px]" />
|
||||
<div class=" flex flex-col items-center justify-center h-full">
|
||||
<div class="text-white text-nowrap text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
Update Required
|
||||
</div>
|
||||
<a
|
||||
href="/changelog"
|
||||
target="_blank"
|
||||
class="text-sm link-secondary [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
|
||||
>
|
||||
What's new?
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded h-[66px] flex items-center justify-center relative z-20">
|
||||
<div class=" flex items-center w-[200px] h-full">
|
||||
<.icon name="hero-chevron-double-left" class="w-9 h-9 mr-[20px]" />
|
||||
<div class=" flex flex-col items-center justify-center h-full">
|
||||
<div class="text-white text-nowrap text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
Update Required
|
||||
<div class="flex flex-row gap-6 z-20">
|
||||
<div
|
||||
:if={@latest_post}
|
||||
class="bg-gray-800/80 rounded-lg overflow-hidden min-w-[300px] backdrop-blur-sm border border-gray-700"
|
||||
>
|
||||
<a href={"/news/#{@latest_post.id}"} target="_blank" class="block group/post">
|
||||
<div class="relative">
|
||||
<img
|
||||
src={@latest_post.cover_image_uri}
|
||||
class="w-[300px] h-[140px] object-cover opacity-80 group-hover/post:opacity-100 transition-opacity"
|
||||
/>
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black/70">
|
||||
</div>
|
||||
<div class="absolute top-2 left-2 flex items-center gap-1 bg-orange-500/90 px-2 py-0.5 rounded text-xs font-semibold">
|
||||
<.icon name="hero-newspaper-solid" class="w-3 h-3" />
|
||||
<span>Latest News</span>
|
||||
</div>
|
||||
<div class="absolute bottom-0 left-0 w-full p-3">
|
||||
<% [first_part | rest] = String.split(@latest_post.title, ":", parts: 2) %>
|
||||
<h3 class="text-white text-sm font-bold ccp-font [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
{first_part}
|
||||
</h3>
|
||||
<p
|
||||
:if={rest != []}
|
||||
class="text-gray-200 text-xs ccp-font text-ellipsis overflow-hidden whitespace-nowrap [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
|
||||
>
|
||||
{List.first(rest)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-800/80 rounded-lg p-4 min-w-[280px] backdrop-blur-sm border border-gray-700">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<.icon name="hero-gift-solid" class="w-5 h-5 text-green-400" />
|
||||
<span class="text-white font-semibold text-sm [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
Support Wanderer
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-gray-300 text-xs mb-3 [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]">
|
||||
Buy PLEX from the official EVE Online store using our promocode to support the development.
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<code class="bg-gray-900/60 px-2 py-1 rounded text-green-400 text-sm font-mono border border-gray-600">
|
||||
WANDERER
|
||||
</code>
|
||||
<a
|
||||
href="/changelog"
|
||||
href="https://www.eveonline.com/plex"
|
||||
target="_blank"
|
||||
class="text-sm link-secondary [text-shadow:_0_1px_0_rgb(0_0_0_/_40%)]"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-1 text-sm text-green-400 hover:text-green-300 transition-colors"
|
||||
>
|
||||
What's new?
|
||||
<span>Get PLEX</span>
|
||||
<.icon name="hero-arrow-top-right-on-square-mini" class="w-4 h-4" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,11 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<.new_version_banner app_version={@app_version} enabled={@map_subscriptions_enabled?} />
|
||||
<.new_version_banner
|
||||
app_version={@app_version}
|
||||
enabled={true}
|
||||
latest_post={@latest_post}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<.live_component module={WandererAppWeb.Alerts} id="notifications" view_flash={@flash} />
|
||||
|
||||
@@ -433,6 +433,10 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
# Invalidate map_characters cache for all maps using this ACL
|
||||
# This ensures the tracking page shows updated members even when map server isn't running
|
||||
invalidate_map_characters_cache(acl_id)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
@@ -440,6 +444,23 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
)
|
||||
end
|
||||
|
||||
defp invalidate_map_characters_cache(acl_id) do
|
||||
case Ash.read(
|
||||
WandererApp.Api.MapAccessList
|
||||
|> Ash.Query.for_read(:read_by_acl, %{acl_id: acl_id})
|
||||
) do
|
||||
{:ok, map_acls} ->
|
||||
Enum.each(map_acls, fn %{map_id: map_id} ->
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
"Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp member_to_json(member) do
|
||||
base = %{
|
||||
|
||||
@@ -42,12 +42,18 @@ defmodule WandererAppWeb.AuthController do
|
||||
|
||||
WandererApp.Character.update_character(character.id, character_update)
|
||||
|
||||
# Update corporation/alliance data from ESI to ensure access control is current
|
||||
update_character_affiliation(character)
|
||||
|
||||
{:ok, character}
|
||||
|
||||
{:error, _error} ->
|
||||
{:ok, character} = WandererApp.Api.Character.create(character_data)
|
||||
:telemetry.execute([:wanderer_app, :user, :character, :registered], %{count: 1})
|
||||
|
||||
# Fetch initial corporation/alliance data for new characters
|
||||
update_character_affiliation(character)
|
||||
|
||||
{:ok, character}
|
||||
end
|
||||
|
||||
@@ -113,4 +119,102 @@ defmodule WandererAppWeb.AuthController do
|
||||
end
|
||||
|
||||
def maybe_update_character_user_id(_character, _user_id), do: :ok
|
||||
|
||||
# Updates character's corporation and alliance data from ESI.
|
||||
# This ensures ACL-based access control uses current corporation membership,
|
||||
# even for characters not actively being tracked on any map.
|
||||
defp update_character_affiliation(%{id: character_id, eve_id: eve_id} = character) do
|
||||
# Run async to not block the SSO callback
|
||||
Task.start(fn ->
|
||||
character_eve_id = eve_id |> String.to_integer()
|
||||
|
||||
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
|
||||
{:ok, [affiliation_info]} when is_map(affiliation_info) ->
|
||||
new_corporation_id = Map.get(affiliation_info, "corporation_id")
|
||||
new_alliance_id = Map.get(affiliation_info, "alliance_id")
|
||||
|
||||
# Check if corporation changed
|
||||
corporation_changed = character.corporation_id != new_corporation_id
|
||||
alliance_changed = character.alliance_id != new_alliance_id
|
||||
|
||||
if corporation_changed or alliance_changed do
|
||||
update_affiliation_data(character_id, character, new_corporation_id, new_alliance_id)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
"[AuthController] Failed to fetch affiliation for character #{character_id}: #{inspect(error)}"
|
||||
)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp update_character_affiliation(_character), do: :ok
|
||||
|
||||
defp update_affiliation_data(character_id, character, corporation_id, alliance_id) do
|
||||
# Fetch corporation info
|
||||
corporation_update =
|
||||
case WandererApp.Esi.get_corporation_info(corporation_id) do
|
||||
{:ok, %{"name" => corp_name, "ticker" => corp_ticker}} ->
|
||||
%{
|
||||
corporation_id: corporation_id,
|
||||
corporation_name: corp_name,
|
||||
corporation_ticker: corp_ticker
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{corporation_id: corporation_id}
|
||||
end
|
||||
|
||||
# Fetch alliance info if present
|
||||
alliance_update =
|
||||
case alliance_id do
|
||||
nil ->
|
||||
%{alliance_id: nil, alliance_name: nil, alliance_ticker: nil}
|
||||
|
||||
_ ->
|
||||
case WandererApp.Esi.get_alliance_info(alliance_id) do
|
||||
{:ok, %{"name" => alliance_name, "ticker" => alliance_ticker}} ->
|
||||
%{
|
||||
alliance_id: alliance_id,
|
||||
alliance_name: alliance_name,
|
||||
alliance_ticker: alliance_ticker
|
||||
}
|
||||
|
||||
_ ->
|
||||
%{alliance_id: alliance_id}
|
||||
end
|
||||
end
|
||||
|
||||
full_update = Map.merge(corporation_update, alliance_update)
|
||||
|
||||
# Update database
|
||||
case character.corporation_id != corporation_id do
|
||||
true ->
|
||||
{:ok, _} = WandererApp.Api.Character.update_corporation(character, corporation_update)
|
||||
|
||||
false ->
|
||||
:ok
|
||||
end
|
||||
|
||||
case character.alliance_id != alliance_id do
|
||||
true ->
|
||||
{:ok, _} = WandererApp.Api.Character.update_alliance(character, alliance_update)
|
||||
|
||||
false ->
|
||||
:ok
|
||||
end
|
||||
|
||||
# Update cache
|
||||
WandererApp.Character.update_character(character_id, full_update)
|
||||
|
||||
Logger.info(
|
||||
"[AuthController] Updated affiliation for character #{character_id}: " <>
|
||||
"corp #{character.corporation_id} -> #{corporation_id}, " <>
|
||||
"alliance #{character.alliance_id} -> #{alliance_id}"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -123,12 +123,6 @@ defmodule WandererAppWeb.LicenseApiController do
|
||||
end
|
||||
end
|
||||
|
||||
def update_validity(conn, %{"id" => _license_id}) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Missing required parameter: is_valid"})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a license's expiration date.
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
import Ash.Query, only: [filter: 2]
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Api.Character
|
||||
alias WandererApp.MapSystemRepo
|
||||
alias WandererApp.MapCharacterSettingsRepo
|
||||
alias WandererApp.MapConnectionRepo
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
alias WandererAppWeb.Schemas.{ApiSchemas, ResponseSchemas}
|
||||
@@ -16,7 +14,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
# V1 API Actions (for compatibility with versioned API router)
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def index_v1(conn, params) do
|
||||
def index_v1(conn, _params) do
|
||||
# Delegate to the existing list implementation or create a basic one
|
||||
json(conn, %{
|
||||
data: [],
|
||||
@@ -43,7 +41,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def create_v1(conn, params) do
|
||||
def create_v1(conn, _params) do
|
||||
# Basic create implementation for testing
|
||||
json(conn, %{
|
||||
data: %{
|
||||
@@ -59,7 +57,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def update_v1(conn, %{"id" => id} = params) do
|
||||
def update_v1(conn, %{"id" => id} = _params) do
|
||||
# Basic update implementation for testing
|
||||
json(conn, %{
|
||||
data: %{
|
||||
@@ -82,7 +80,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
|> text("")
|
||||
end
|
||||
|
||||
def duplicate_v1(conn, %{"id" => id} = params) do
|
||||
def duplicate_v1(conn, %{"id" => id} = _params) do
|
||||
# Basic duplicate implementation for testing
|
||||
json(conn, %{
|
||||
data: %{
|
||||
@@ -99,7 +97,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def bulk_create_v1(conn, params) do
|
||||
def bulk_create_v1(conn, _params) do
|
||||
# Basic bulk create implementation for testing
|
||||
json(conn, %{
|
||||
data: [
|
||||
@@ -121,7 +119,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def bulk_update_v1(conn, params) do
|
||||
def bulk_update_v1(conn, _params) do
|
||||
# Basic bulk update implementation for testing
|
||||
json(conn, %{
|
||||
data: [
|
||||
@@ -325,13 +323,6 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
# Helper functions for the API controller
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
defp get_map_id_by_slug(slug) do
|
||||
case WandererApp.Api.Map.get_map_by_slug(slug) do
|
||||
{:ok, map} -> {:ok, map.id}
|
||||
{:error, error} -> {:error, "Map not found for slug: #{slug}, error: #{inspect(error)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_map_identifier(params) do
|
||||
case Map.get(params, "map_identifier") do
|
||||
nil ->
|
||||
|
||||
@@ -4,8 +4,7 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Api
|
||||
|
||||
alias WandererAppWeb.UserActivityItem
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
@@ -155,10 +154,10 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
|
||||
result
|
||||
|> Map.put(:character, WandererAppWeb.MapEventHandler.map_ui_character_stat(character))
|
||||
|> Map.put(:event_name, WandererAppWeb.UserActivity.get_event_name(event_type))
|
||||
|> Map.put(:event_name, WandererAppWeb.UserActivityItem.get_event_name(event_type))
|
||||
|> Map.put(
|
||||
:event_data,
|
||||
WandererAppWeb.UserActivity.get_event_data(
|
||||
WandererAppWeb.UserActivityItem.get_event_data(
|
||||
event_type,
|
||||
Jason.decode!(event_data) |> Map.drop(["character_id"])
|
||||
)
|
||||
|
||||
@@ -65,24 +65,6 @@ defmodule WandererAppWeb.MapEventsAPIController do
|
||||
items: @event_schema
|
||||
})
|
||||
|
||||
@events_list_params %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
since: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
format: :date_time,
|
||||
description: "Return events after this timestamp (ISO8601)"
|
||||
},
|
||||
limit: %OpenApiSpex.Schema{
|
||||
type: :integer,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 100,
|
||||
description: "Maximum number of events to return"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# OpenApiSpex Operations
|
||||
# -----------------------------------------------------------------
|
||||
@@ -173,7 +155,7 @@ defmodule WandererAppWeb.MapEventsAPIController do
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Invalid 'limit' parameter. Must be between 1 and 100."})
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Internal server error"})
|
||||
@@ -184,7 +166,7 @@ defmodule WandererAppWeb.MapEventsAPIController do
|
||||
# Private Functions
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
defp get_map(conn, map_identifier) do
|
||||
defp get_map(conn, _map_identifier) do
|
||||
# The map should already be loaded by the CheckMapApiKey plug
|
||||
case conn.assigns[:map] do
|
||||
nil -> {:error, :map_not_found}
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule WandererAppWeb.Plugs.JsonApiPerformanceMonitor do
|
||||
conn
|
||||
|> register_before_send(fn conn ->
|
||||
end_time = System.monotonic_time(:millisecond)
|
||||
duration = end_time - start_time
|
||||
_duration = end_time - start_time
|
||||
|
||||
# Extract response metadata
|
||||
response_metadata = extract_response_metadata(conn, request_metadata)
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererAppWeb.Plugs.LicenseAuth do
|
||||
require Logger
|
||||
|
||||
alias WandererApp.License.LicenseManager
|
||||
alias WandererApp.Helpers.Config
|
||||
|
||||
@doc """
|
||||
Authenticates requests using the LM_AUTH_KEY.
|
||||
@@ -21,7 +20,7 @@ defmodule WandererAppWeb.Plugs.LicenseAuth do
|
||||
"""
|
||||
def authenticate_lm(conn, _opts) do
|
||||
auth_header = get_req_header(conn, "authorization")
|
||||
lm_auth_key = Config.get_env(:wanderer_app, :lm_auth_key)
|
||||
lm_auth_key = Application.get_env(:wanderer_app, :lm_auth_key)
|
||||
|
||||
case auth_header do
|
||||
["Bearer " <> token] ->
|
||||
|
||||
@@ -37,7 +37,7 @@ defmodule WandererAppWeb.UserAuth do
|
||||
nil ->
|
||||
{:halt, redirect_require_login(socket)}
|
||||
|
||||
%User{characters: characters} ->
|
||||
%User{characters: _characters} ->
|
||||
{:cont, new_socket}
|
||||
end
|
||||
|
||||
@@ -112,13 +112,6 @@ defmodule WandererAppWeb.UserAuth do
|
||||
|> LiveView.redirect(to: ~p"/")
|
||||
end
|
||||
|
||||
defp track_characters([]), do: :ok
|
||||
|
||||
defp track_characters([%{id: character_id} | characters]) do
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
track_characters(characters)
|
||||
end
|
||||
|
||||
defp maybe_store_return_to(%{method: "GET"} = conn) do
|
||||
%{request_path: request_path, query_string: query_string} = conn
|
||||
return_to = if query_string == "", do: request_path, else: request_path <> "?" <> query_string
|
||||
|
||||
@@ -2,8 +2,11 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
use WandererAppWeb, :live_view
|
||||
|
||||
alias WandererApp.ExternalEvents.AclEventBroadcaster
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
@members_per_page 50
|
||||
|
||||
@impl true
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do
|
||||
{:ok, characters} = WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
@@ -24,7 +27,9 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
user_id: user_id,
|
||||
access_lists: access_lists |> Enum.map(fn acl -> map_ui_acl(acl, nil) end),
|
||||
characters: characters,
|
||||
members: []
|
||||
members: [],
|
||||
members_page: 1,
|
||||
members_per_page: @members_per_page
|
||||
)}
|
||||
end
|
||||
|
||||
@@ -38,7 +43,9 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
allow_acl_creation: false,
|
||||
access_lists: [],
|
||||
characters: [],
|
||||
members: []
|
||||
members: [],
|
||||
members_page: 1,
|
||||
members_per_page: @members_per_page
|
||||
)}
|
||||
end
|
||||
|
||||
@@ -92,10 +99,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> assign(:page_title, "Access Lists - Members")
|
||||
|> assign(:selected_acl_id, acl_id)
|
||||
|> assign(:access_list, access_list)
|
||||
|> assign(
|
||||
:members,
|
||||
members
|
||||
)
|
||||
|> assign(:members, members)
|
||||
|> assign(:members_page, 1)
|
||||
else
|
||||
_ ->
|
||||
socket
|
||||
@@ -281,11 +286,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> Enum.find(&(&1.id == member_id))
|
||||
|> WandererApp.Api.AccessListMember.destroy!()
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{socket.assigns.selected_acl_id}",
|
||||
{:acl_updated, %{acl_id: socket.assigns.selected_acl_id}}
|
||||
)
|
||||
broadcast_acl_updated(socket.assigns.selected_acl_id)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -327,6 +328,20 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("members_prev_page", _, socket) do
|
||||
new_page = max(1, socket.assigns.members_page - 1)
|
||||
{:noreply, assign(socket, :members_page, new_page)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("members_next_page", _, socket) do
|
||||
total_members = length(socket.assigns.members)
|
||||
max_page = max(1, ceil(total_members / socket.assigns.members_per_page))
|
||||
new_page = min(max_page, socket.assigns.members_page + 1)
|
||||
{:noreply, assign(socket, :members_page, new_page)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("noop", _, socket) do
|
||||
{:noreply, socket}
|
||||
@@ -444,11 +459,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :update], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{socket.assigns.selected_acl_id}",
|
||||
{:acl_updated, %{acl_id: socket.assigns.selected_acl_id}}
|
||||
)
|
||||
broadcast_acl_updated(socket.assigns.selected_acl_id)
|
||||
|
||||
socket
|
||||
|> assign(
|
||||
@@ -574,11 +585,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{access_list_id}",
|
||||
{:acl_updated, %{acl_id: access_list_id}}
|
||||
)
|
||||
broadcast_acl_updated(access_list_id)
|
||||
|
||||
{:ok, member}
|
||||
|
||||
@@ -613,11 +620,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{access_list_id}",
|
||||
{:acl_updated, %{acl_id: access_list_id}}
|
||||
)
|
||||
broadcast_acl_updated(access_list_id)
|
||||
|
||||
{:ok, member}
|
||||
|
||||
@@ -653,11 +656,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{access_list_id}",
|
||||
{:acl_updated, %{acl_id: access_list_id}}
|
||||
)
|
||||
broadcast_acl_updated(access_list_id)
|
||||
|
||||
{:ok, member}
|
||||
|
||||
@@ -688,7 +687,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
"""
|
||||
end
|
||||
|
||||
slot(:option)
|
||||
attr(:option, :any, required: true)
|
||||
|
||||
def search_member_item(assigns) do
|
||||
~H"""
|
||||
@@ -737,4 +736,44 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
defp map_ui_acl(acl, selected_id) do
|
||||
acl |> Map.put(:selected, acl.id == selected_id)
|
||||
end
|
||||
|
||||
defp paginated_members(members, page, per_page) do
|
||||
members
|
||||
|> Enum.sort_by(&{&1.role, &1.name}, &<=/2)
|
||||
|> Enum.drop((page - 1) * per_page)
|
||||
|> Enum.take(per_page)
|
||||
end
|
||||
|
||||
defp total_pages(members, per_page) do
|
||||
max(1, ceil(length(members) / per_page))
|
||||
end
|
||||
|
||||
# Broadcast ACL update and invalidate map_characters cache for all maps using this ACL
|
||||
# This ensures the tracking page shows updated members even when map server isn't running
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
invalidate_map_characters_cache(acl_id)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
{:acl_updated, %{acl_id: acl_id}}
|
||||
)
|
||||
end
|
||||
|
||||
defp invalidate_map_characters_cache(acl_id) do
|
||||
case Ash.read(
|
||||
WandererApp.Api.MapAccessList
|
||||
|> Ash.Query.for_read(:read_by_acl, %{acl_id: acl_id})
|
||||
) do
|
||||
{:ok, map_acls} ->
|
||||
Enum.each(map_acls, fn %{map_id: map_id} ->
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
"Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -82,11 +82,14 @@
|
||||
</h3>
|
||||
|
||||
<div
|
||||
class="dropzone droppable draggable-dropzone--occupied flex flex-col gap-1 w-full rounded-none h-[calc(100vh-211px)] !overflow-y-auto"
|
||||
class={[
|
||||
"dropzone droppable draggable-dropzone--occupied flex flex-col gap-1 w-full rounded-none h-[calc(100vh-191px)] !overflow-y-auto",
|
||||
classes("!h-[calc(100vh-240px)]": length(@members) > @members_per_page)
|
||||
]}
|
||||
id="acl_members"
|
||||
>
|
||||
<div
|
||||
:for={member <- @members |> Enum.sort_by(&{&1.role, &1.name}, &<=/2)}
|
||||
:for={member <- paginated_members(@members, @members_page, @members_per_page)}
|
||||
draggable="true"
|
||||
id={member.id}
|
||||
class="draggable !p-1 h-10 cursor-move bg-black bg-opacity-25 hover:text-white"
|
||||
@@ -113,15 +116,45 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div :if={length(@members) > @members_per_page} class="flex items-center justify-between px-3 py-2 border-t border-gray-500 bg-black bg-opacity-25">
|
||||
<span class="text-sm text-gray-400">
|
||||
Page {@members_page} of {total_pages(@members, @members_per_page)} ({length(@members)} members)
|
||||
</span>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
phx-click="members_prev_page"
|
||||
disabled={@members_page <= 1}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page <= 1, do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-left" class="w-4 h-4" />
|
||||
</button>
|
||||
<button
|
||||
phx-click="members_next_page"
|
||||
disabled={@members_page >= total_pages(@members, @members_per_page)}
|
||||
class={"btn btn-sm btn-ghost " <> if(@members_page >= total_pages(@members, @members_per_page), do: "btn-disabled", else: "")}
|
||||
>
|
||||
<.icon name="hero-chevron-right" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<.link
|
||||
disabled={@selected_acl_id == "" or not can_add_members?(@access_list, @current_user)}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none"
|
||||
:if={@selected_acl_id != "" and can_add_members?(@access_list, @current_user)}
|
||||
class="btn w-full btn-neutral rounded-none"
|
||||
patch={~p"/access-lists/#{@selected_acl_id}/add-members"}
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</.link>
|
||||
<div
|
||||
:if={@selected_acl_id == "" or not can_add_members?(@access_list, @current_user)}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none btn-disabled"
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -146,10 +179,10 @@
|
||||
placeholder="Select an owner"
|
||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||
/>
|
||||
|
||||
|
||||
<!-- Divider between above inputs and the API key section -->
|
||||
<hr class="my-4 border-gray-600" />
|
||||
|
||||
|
||||
<!-- API Key Section with grid layout -->
|
||||
<div class="mt-2">
|
||||
<label class="block text-sm font-medium text-gray-200 mb-1">ACL API key</label>
|
||||
|
||||
@@ -4,8 +4,6 @@ defmodule WandererAppWeb.AdminLive do
|
||||
require Logger
|
||||
alias BetterNumber, as: Number
|
||||
|
||||
@invite_link_ttl :timer.hours(24)
|
||||
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket)
|
||||
when not is_nil(user_id) do
|
||||
WandererApp.StartCorpWalletTrackerTask.maybe_start_corp_wallet_tracker(
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
rows={@transactions}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={transaction}>
|
||||
<:col :let={_transaction}>
|
||||
<div class=" text-22">
|
||||
<.icon name="hero-credit-card-solid" class="h-5 w-5" />
|
||||
</div>
|
||||
@@ -267,7 +267,7 @@
|
||||
rows={@active_map_subscriptions}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={subscription}>
|
||||
<:col :let={_subscription}>
|
||||
<div class=" text-22">
|
||||
<.icon name="hero-check-badge-solid" class="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Character.TrackingUtils
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, maps} = WandererApp.Maps.get_available_maps(socket.assigns.current_user)
|
||||
@@ -18,22 +20,19 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(characters: [], selected_map: nil, maps: [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
# Unsubscribe from previous map if any
|
||||
socket = maybe_unsubscribe_from_map(socket)
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :characters_tracking)
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
|> assign(selected_map: nil, selected_map_slug: nil)
|
||||
end
|
||||
|
||||
defp apply_action(
|
||||
@@ -43,6 +42,10 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
) do
|
||||
selected_map = maps |> Enum.find(&(&1.slug == map_slug))
|
||||
|
||||
# Unsubscribe from previous map and subscribe to new one
|
||||
socket = maybe_unsubscribe_from_map(socket)
|
||||
socket = maybe_subscribe_to_map(socket, selected_map)
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :characters_tracking)
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
@@ -55,6 +58,27 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
end)
|
||||
end
|
||||
|
||||
# Subscribe to map PubSub channel to receive ACL update notifications
|
||||
defp maybe_subscribe_to_map(socket, nil), do: socket
|
||||
|
||||
defp maybe_subscribe_to_map(socket, %{id: map_id}) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id)
|
||||
end
|
||||
|
||||
socket
|
||||
end
|
||||
|
||||
# Unsubscribe from previous map's PubSub channel
|
||||
defp maybe_unsubscribe_from_map(%{assigns: %{selected_map: nil}} = socket), do: socket
|
||||
|
||||
defp maybe_unsubscribe_from_map(%{assigns: %{selected_map: %{id: map_id}}} = socket) do
|
||||
Phoenix.PubSub.unsubscribe(WandererApp.PubSub, map_id)
|
||||
socket
|
||||
end
|
||||
|
||||
defp maybe_unsubscribe_from_map(socket), do: socket
|
||||
|
||||
@impl true
|
||||
def handle_event("select_map_" <> map_slug, _, socket) do
|
||||
{:noreply,
|
||||
@@ -77,21 +101,30 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
case characters |> Enum.find(&(&1.id == character_id)) do
|
||||
%{tracked: false} ->
|
||||
WandererApp.MapCharacterSettingsRepo.track(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
%{tracked: current_tracked, eve_id: eve_id} ->
|
||||
# Use TrackingUtils.update_tracking to properly set/unset the tracking_start_time
|
||||
# cache key, which is required for the character to appear in get_tracked_character_ids
|
||||
case TrackingUtils.update_tracking(
|
||||
selected_map.id,
|
||||
eve_id,
|
||||
current_user.id,
|
||||
not current_tracked,
|
||||
self(),
|
||||
false
|
||||
) do
|
||||
{:ok, _tracking_data, _event} ->
|
||||
:ok
|
||||
|
||||
%{tracked: true} ->
|
||||
WandererApp.MapCharacterSettingsRepo.untrack(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
{:error, reason} ->
|
||||
Logger.error(
|
||||
"Failed to toggle tracking for character #{character_id} on map #{selected_map.id}: #{inspect(reason)}"
|
||||
)
|
||||
end
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(selected_map.id, [
|
||||
character_id
|
||||
])
|
||||
nil ->
|
||||
Logger.warning(
|
||||
"Character #{character_id} not found in available characters for map #{selected_map.id}"
|
||||
)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
@@ -111,6 +144,20 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Handle ACL members changed event - reload characters list
|
||||
@impl true
|
||||
def handle_info(
|
||||
%{event: :acl_members_changed},
|
||||
%{assigns: %{selected_map: selected_map, current_user: current_user}} = socket
|
||||
)
|
||||
when not is_nil(selected_map) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_async(:characters, fn ->
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
end)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_event, socket), do: {:noreply, socket}
|
||||
end
|
||||
|
||||
@@ -79,7 +79,7 @@ defmodule WandererAppWeb.MapSubscription do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp get_title(%{plan: plan, auto_renew?: auto_renew?, active_till: active_till} = subscription) do
|
||||
defp get_title(%{plan: plan, auto_renew?: auto_renew?, active_till: active_till}) do
|
||||
if plan != :alpha do
|
||||
"Active subscription: omega \nActive till: #{Calendar.strftime(active_till, "%m/%d/%Y")} \nAuto renew: #{auto_renew?}"
|
||||
else
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
defmodule WandererAppWeb.UserActivity do
|
||||
defmodule WandererAppWeb.UserActivityItem do
|
||||
use WandererAppWeb, :live_component
|
||||
use LiveViewEvents
|
||||
|
||||
@@ -300,13 +300,13 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
%{"character_eve_id" => character_eve_id},
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: %{id: current_user_id}
|
||||
map_id: _map_id,
|
||||
current_user: %{id: _current_user_id}
|
||||
}
|
||||
} = socket
|
||||
)
|
||||
when not is_nil(character_eve_id) do
|
||||
{:ok, character} = WandererApp.Character.get_by_eve_id("#{character_eve_id}")
|
||||
{:ok, _character} = WandererApp.Character.get_by_eve_id("#{character_eve_id}")
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
@@ -338,12 +338,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
station_id: character.station_id
|
||||
}
|
||||
|
||||
defp get_map_with_acls(map_id) do
|
||||
with {:ok, map} <- WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, Ash.load!(map, :acls)}
|
||||
end
|
||||
end
|
||||
|
||||
def needs_tracking_setup?(
|
||||
only_tracked_characters,
|
||||
characters,
|
||||
|
||||
@@ -120,10 +120,16 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
{:ok, signatures} =
|
||||
WandererApp.Api.MapSystemSignature.by_linked_system_id(solar_system_target_id)
|
||||
|
||||
signatures
|
||||
|> Enum.filter(fn s ->
|
||||
s.system_id == source_system.id
|
||||
end)
|
||||
filtered_signatures =
|
||||
signatures
|
||||
|> Enum.filter(fn s ->
|
||||
s.system_id == source_system.id
|
||||
end)
|
||||
|
||||
# Collect eve_ids for audit logging
|
||||
deleted_eve_ids = Enum.map(filtered_signatures, & &1.eve_id)
|
||||
|
||||
filtered_signatures
|
||||
|> Enum.each(fn s ->
|
||||
if not is_nil(s.temporary_name) && s.temporary_name == target_system.temporary_name do
|
||||
map_id
|
||||
@@ -143,6 +149,17 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
|> WandererApp.Api.MapSystemSignature.destroy!()
|
||||
end)
|
||||
|
||||
# Audit log signatures deleted with connection
|
||||
if deleted_eve_ids != [] do
|
||||
WandererApp.User.ActivityTracker.track_map_event(:signatures_removed, %{
|
||||
character_id: main_character_id,
|
||||
user_id: current_user_id,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_source_id,
|
||||
signatures: deleted_eve_ids
|
||||
})
|
||||
end
|
||||
|
||||
WandererApp.Map.Server.Impl.broadcast!(
|
||||
map_id,
|
||||
:signatures_updated,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -17,6 +17,10 @@ defmodule WandererAppWeb.MapPingsEventHandler do
|
||||
{:ok, pings} = WandererApp.MapPingsRepo.get_by_map(map_id)
|
||||
|
||||
pings
|
||||
|> Enum.filter(fn ping ->
|
||||
# Skip pings where system or character associations are nil (deleted)
|
||||
not is_nil(ping.system) and not is_nil(ping.character)
|
||||
end)
|
||||
|> Enum.reduce(socket, fn %{
|
||||
id: id,
|
||||
type: type,
|
||||
|
||||
@@ -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(
|
||||
%{
|
||||
@@ -60,7 +60,10 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
|
||||
ping_system_ids =
|
||||
pings
|
||||
|> Enum.map(fn %{system: %{solar_system_id: solar_system_id}} -> "#{solar_system_id}" end)
|
||||
|> Enum.flat_map(fn
|
||||
%{system: %{solar_system_id: solar_system_id}} -> ["#{solar_system_id}"]
|
||||
_ -> []
|
||||
end)
|
||||
|
||||
route_hubs = (ping_system_ids ++ hubs) |> Enum.uniq()
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
current_user: %{id: current_user_id},
|
||||
map_id: map_id,
|
||||
main_character_id: main_character_id,
|
||||
map_user_settings: map_user_settings,
|
||||
map_user_settings: _map_user_settings,
|
||||
user_permissions: %{update_system: true}
|
||||
} = assigns
|
||||
} = socket
|
||||
@@ -363,8 +363,8 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
linked_sig_eve_id: nil
|
||||
})
|
||||
|
||||
s
|
||||
|> WandererApp.Api.MapSystemSignature.update_linked_system(%{
|
||||
# Use the wrapper to log unlink operations
|
||||
WandererApp.Map.Server.SignaturesImpl.update_signature_linked_system(s, %{
|
||||
linked_system_id: nil
|
||||
})
|
||||
end)
|
||||
@@ -380,7 +380,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
|
||||
def handle_ui_event(
|
||||
"undo_delete_signatures",
|
||||
%{"system_id" => solar_system_id, "eve_ids" => eve_ids} = payload,
|
||||
%{"system_id" => solar_system_id, "eve_ids" => eve_ids} = _payload,
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
|
||||
@@ -97,7 +97,7 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
%{"solarSystemId" => solar_system_id} = _event,
|
||||
%{
|
||||
assigns: %{
|
||||
current_user: current_user,
|
||||
current_user: _current_user,
|
||||
has_tracked_characters?: true,
|
||||
map_id: map_id,
|
||||
user_permissions: %{add_system: true}
|
||||
@@ -109,7 +109,7 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
|> case do
|
||||
%{id: system_id} = system when not is_nil(system_id) ->
|
||||
%{id: system_id} = _system when not is_nil(system_id) ->
|
||||
{:ok, comments} = WandererApp.MapSystemCommentRepo.get_by_system(system_id)
|
||||
|
||||
{:reply,
|
||||
|
||||
@@ -3,10 +3,6 @@ defmodule WandererAppWeb.MapAuditLive do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererAppWeb.UserActivity
|
||||
|
||||
@active_subscription_periods ["2M", "3M"]
|
||||
|
||||
def mount(
|
||||
%{"slug" => map_slug, "period" => period, "activity" => activity} = _params,
|
||||
_session,
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
/>
|
||||
</div>
|
||||
<.live_component
|
||||
module={UserActivity}
|
||||
module={WandererAppWeb.UserActivityItem}
|
||||
id="user-activity"
|
||||
notify_to={self()}
|
||||
can_undo_types={@can_undo_types}
|
||||
|
||||
@@ -157,7 +157,7 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
|> assign(:groups, groups)
|
||||
end
|
||||
|
||||
defp map_ui_character(map_id, character) do
|
||||
defp map_ui_character(_map_id, character) do
|
||||
character
|
||||
|> Map.take([
|
||||
:id,
|
||||
|
||||
@@ -230,6 +230,7 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
def handle_event(socket, {:DOWN, ref, :process, _pid, reason}) when is_reference(ref) do
|
||||
# Task failed, log the error and update the client
|
||||
Logger.error("Task failed: #{inspect(reason)}")
|
||||
socket
|
||||
end
|
||||
|
||||
def handle_event(socket, event),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user