mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-02-08 06:56:02 +00:00
Compare commits
1 Commits
develop
...
revert-561
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
646262447d |
@@ -16,8 +16,3 @@ export WANDERER_SSE_ENABLED="true"
|
|||||||
export WANDERER_WEBHOOKS_ENABLED="true"
|
export WANDERER_WEBHOOKS_ENABLED="true"
|
||||||
export WANDERER_SSE_MAX_CONNECTIONS="1000"
|
export WANDERER_SSE_MAX_CONNECTIONS="1000"
|
||||||
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
|
export WANDERER_WEBHOOK_TIMEOUT_MS="15000"
|
||||||
|
|
||||||
# Promo codes for map subscriptions (optional)
|
|
||||||
# Format: CODE:DISCOUNT_PERCENT,CODE2:DISCOUNT_PERCENT2
|
|
||||||
# Codes are case-insensitive, discounts stack with period discounts
|
|
||||||
# export WANDERER_PROMO_CODES="PROMO2025:10,NEWUSER:20"
|
|
||||||
|
|||||||
110
.github/workflows/test.yml
vendored
110
.github/workflows/test.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
name: Test Suite
|
name: Test Suite
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15
|
image: postgres:15
|
||||||
@@ -35,17 +35,17 @@ jobs:
|
|||||||
--health-retries 5
|
--health-retries 5
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Elixir/OTP
|
- name: Setup Elixir/OTP
|
||||||
uses: erlef/setup-beam@v1
|
uses: erlef/setup-beam@v1
|
||||||
with:
|
with:
|
||||||
elixir-version: ${{ env.ELIXIR_VERSION }}
|
elixir-version: ${{ env.ELIXIR_VERSION }}
|
||||||
otp-version: ${{ env.OTP_VERSION }}
|
otp-version: ${{ env.OTP_VERSION }}
|
||||||
|
|
||||||
- name: Cache Elixir dependencies
|
- name: Cache Elixir dependencies
|
||||||
uses: actions/cache@v3
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
@@ -54,12 +54,12 @@ jobs:
|
|||||||
_build
|
_build
|
||||||
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
||||||
restore-keys: ${{ runner.os }}-mix-
|
restore-keys: ${{ runner.os }}-mix-
|
||||||
|
|
||||||
- name: Install Elixir dependencies
|
- name: Install Elixir dependencies
|
||||||
run: |
|
run: |
|
||||||
mix deps.get
|
mix deps.get
|
||||||
mix deps.compile
|
mix deps.compile
|
||||||
|
|
||||||
- name: Check code formatting
|
- name: Check code formatting
|
||||||
id: format
|
id: format
|
||||||
run: |
|
run: |
|
||||||
@@ -71,42 +71,42 @@ jobs:
|
|||||||
echo "count=1" >> $GITHUB_OUTPUT
|
echo "count=1" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Compile code and capture warnings
|
- name: Compile code and capture warnings
|
||||||
id: compile
|
id: compile
|
||||||
run: |
|
run: |
|
||||||
# Capture compilation output
|
# Capture compilation output
|
||||||
output=$(mix compile 2>&1 || true)
|
output=$(mix compile 2>&1 || true)
|
||||||
echo "$output" > compile_output.txt
|
echo "$output" > compile_output.txt
|
||||||
|
|
||||||
# Count warnings
|
# Count warnings
|
||||||
warning_count=$(echo "$output" | grep -c "warning:" || echo "0")
|
warning_count=$(echo "$output" | grep -c "warning:" || echo "0")
|
||||||
|
|
||||||
# Check if compilation succeeded
|
# Check if compilation succeeded
|
||||||
if mix compile > /dev/null 2>&1; then
|
if mix compile > /dev/null 2>&1; then
|
||||||
echo "status=✅ Success" >> $GITHUB_OUTPUT
|
echo "status=✅ Success" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "status=❌ Failed" >> $GITHUB_OUTPUT
|
echo "status=❌ Failed" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "warnings=$warning_count" >> $GITHUB_OUTPUT
|
echo "warnings=$warning_count" >> $GITHUB_OUTPUT
|
||||||
echo "output<<EOF" >> $GITHUB_OUTPUT
|
echo "output<<EOF" >> $GITHUB_OUTPUT
|
||||||
echo "$output" >> $GITHUB_OUTPUT
|
echo "$output" >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Setup database
|
- name: Setup database
|
||||||
run: |
|
run: |
|
||||||
mix ecto.create
|
mix ecto.create
|
||||||
mix ecto.migrate
|
mix ecto.migrate
|
||||||
|
|
||||||
- name: Run tests with coverage
|
- name: Run tests with coverage
|
||||||
id: tests
|
id: tests
|
||||||
run: |
|
run: |
|
||||||
# Run tests with coverage
|
# Run tests with coverage
|
||||||
output=$(mix test --cover 2>&1 || true)
|
output=$(mix test --cover 2>&1 || true)
|
||||||
echo "$output" > test_output.txt
|
echo "$output" > test_output.txt
|
||||||
|
|
||||||
# Parse test results
|
# Parse test results
|
||||||
if echo "$output" | grep -q "0 failures"; then
|
if echo "$output" | grep -q "0 failures"; then
|
||||||
echo "status=✅ All Passed" >> $GITHUB_OUTPUT
|
echo "status=✅ All Passed" >> $GITHUB_OUTPUT
|
||||||
@@ -115,16 +115,16 @@ jobs:
|
|||||||
echo "status=❌ Some Failed" >> $GITHUB_OUTPUT
|
echo "status=❌ Some Failed" >> $GITHUB_OUTPUT
|
||||||
test_status="failed"
|
test_status="failed"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract test counts
|
# Extract test counts
|
||||||
test_line=$(echo "$output" | grep -E "[0-9]+ tests?, [0-9]+ failures?" | head -1 || echo "0 tests, 0 failures")
|
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")
|
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")
|
failures=$(echo "$test_line" | grep -o '[0-9]\+ failures\?' | grep -o '[0-9]\+' | head -1 || echo "0")
|
||||||
|
|
||||||
echo "total=$total_tests" >> $GITHUB_OUTPUT
|
echo "total=$total_tests" >> $GITHUB_OUTPUT
|
||||||
echo "failures=$failures" >> $GITHUB_OUTPUT
|
echo "failures=$failures" >> $GITHUB_OUTPUT
|
||||||
echo "passed=$((total_tests - failures))" >> $GITHUB_OUTPUT
|
echo "passed=$((total_tests - failures))" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Calculate success rate
|
# Calculate success rate
|
||||||
if [ "$total_tests" -gt 0 ]; then
|
if [ "$total_tests" -gt 0 ]; then
|
||||||
success_rate=$(echo "scale=1; ($total_tests - $failures) * 100 / $total_tests" | bc)
|
success_rate=$(echo "scale=1; ($total_tests - $failures) * 100 / $total_tests" | bc)
|
||||||
@@ -132,26 +132,26 @@ jobs:
|
|||||||
success_rate="0"
|
success_rate="0"
|
||||||
fi
|
fi
|
||||||
echo "success_rate=$success_rate" >> $GITHUB_OUTPUT
|
echo "success_rate=$success_rate" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
exit_code=$?
|
exit_code=$?
|
||||||
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
|
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Generate coverage report
|
- name: Generate coverage report
|
||||||
id: coverage
|
id: coverage
|
||||||
run: |
|
run: |
|
||||||
# Generate coverage report with GitHub format
|
# Generate coverage report with GitHub format
|
||||||
output=$(mix coveralls.github 2>&1 || true)
|
output=$(mix coveralls.github 2>&1 || true)
|
||||||
echo "$output" > coverage_output.txt
|
echo "$output" > coverage_output.txt
|
||||||
|
|
||||||
# Extract coverage percentage
|
# Extract coverage percentage
|
||||||
coverage=$(echo "$output" | grep -o '[0-9]\+\.[0-9]\+%' | head -1 | sed 's/%//' || echo "0")
|
coverage=$(echo "$output" | grep -o '[0-9]\+\.[0-9]\+%' | head -1 | sed 's/%//' || echo "0")
|
||||||
if [ -z "$coverage" ]; then
|
if [ -z "$coverage" ]; then
|
||||||
coverage="0"
|
coverage="0"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "percentage=$coverage" >> $GITHUB_OUTPUT
|
echo "percentage=$coverage" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Determine status
|
# Determine status
|
||||||
if (( $(echo "$coverage >= 80" | bc -l) )); then
|
if (( $(echo "$coverage >= 80" | bc -l) )); then
|
||||||
echo "status=✅ Excellent" >> $GITHUB_OUTPUT
|
echo "status=✅ Excellent" >> $GITHUB_OUTPUT
|
||||||
@@ -161,14 +161,14 @@ jobs:
|
|||||||
echo "status=❌ Needs Improvement" >> $GITHUB_OUTPUT
|
echo "status=❌ Needs Improvement" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Run Credo analysis
|
- name: Run Credo analysis
|
||||||
id: credo
|
id: credo
|
||||||
run: |
|
run: |
|
||||||
# Run Credo and capture output
|
# Run Credo and capture output
|
||||||
output=$(mix credo --strict --format=json 2>&1 || true)
|
output=$(mix credo --strict --format=json 2>&1 || true)
|
||||||
echo "$output" > credo_output.txt
|
echo "$output" > credo_output.txt
|
||||||
|
|
||||||
# Try to parse JSON output
|
# Try to parse JSON output
|
||||||
if echo "$output" | jq . > /dev/null 2>&1; then
|
if echo "$output" | jq . > /dev/null 2>&1; then
|
||||||
issues=$(echo "$output" | jq '.issues | length' 2>/dev/null || echo "0")
|
issues=$(echo "$output" | jq '.issues | length' 2>/dev/null || echo "0")
|
||||||
@@ -183,12 +183,12 @@ jobs:
|
|||||||
normal_issues="0"
|
normal_issues="0"
|
||||||
low_issues="0"
|
low_issues="0"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "total_issues=$issues" >> $GITHUB_OUTPUT
|
echo "total_issues=$issues" >> $GITHUB_OUTPUT
|
||||||
echo "high_issues=$high_issues" >> $GITHUB_OUTPUT
|
echo "high_issues=$high_issues" >> $GITHUB_OUTPUT
|
||||||
echo "normal_issues=$normal_issues" >> $GITHUB_OUTPUT
|
echo "normal_issues=$normal_issues" >> $GITHUB_OUTPUT
|
||||||
echo "low_issues=$low_issues" >> $GITHUB_OUTPUT
|
echo "low_issues=$low_issues" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Determine status
|
# Determine status
|
||||||
if [ "$issues" -eq 0 ]; then
|
if [ "$issues" -eq 0 ]; then
|
||||||
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
||||||
@@ -198,24 +198,24 @@ jobs:
|
|||||||
echo "status=❌ Needs Attention" >> $GITHUB_OUTPUT
|
echo "status=❌ Needs Attention" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Run Dialyzer analysis
|
- name: Run Dialyzer analysis
|
||||||
id: dialyzer
|
id: dialyzer
|
||||||
run: |
|
run: |
|
||||||
# Ensure PLT is built
|
# Ensure PLT is built
|
||||||
mix dialyzer --plt
|
mix dialyzer --plt
|
||||||
|
|
||||||
# Run Dialyzer and capture output
|
# Run Dialyzer and capture output
|
||||||
output=$(mix dialyzer --format=github 2>&1 || true)
|
output=$(mix dialyzer --format=github 2>&1 || true)
|
||||||
echo "$output" > dialyzer_output.txt
|
echo "$output" > dialyzer_output.txt
|
||||||
|
|
||||||
# Count warnings and errors
|
# Count warnings and errors
|
||||||
warnings=$(echo "$output" | grep -c "warning:" || echo "0")
|
warnings=$(echo "$output" | grep -c "warning:" || echo "0")
|
||||||
errors=$(echo "$output" | grep -c "error:" || echo "0")
|
errors=$(echo "$output" | grep -c "error:" || echo "0")
|
||||||
|
|
||||||
echo "warnings=$warnings" >> $GITHUB_OUTPUT
|
echo "warnings=$warnings" >> $GITHUB_OUTPUT
|
||||||
echo "errors=$errors" >> $GITHUB_OUTPUT
|
echo "errors=$errors" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Determine status
|
# Determine status
|
||||||
if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then
|
if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then
|
||||||
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
||||||
@@ -225,7 +225,7 @@ jobs:
|
|||||||
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
|
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Create test results summary
|
- name: Create test results summary
|
||||||
id: summary
|
id: summary
|
||||||
run: |
|
run: |
|
||||||
@@ -236,11 +236,11 @@ jobs:
|
|||||||
coverage_score=${{ steps.coverage.outputs.percentage }}
|
coverage_score=${{ steps.coverage.outputs.percentage }}
|
||||||
credo_score=$(echo "scale=0; (100 - ${{ steps.credo.outputs.total_issues }} * 2)" | bc | sed 's/^-.*$/0/')
|
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/')
|
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)
|
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
|
echo "overall_score=$overall_score" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
# Determine overall status
|
# Determine overall status
|
||||||
if (( $(echo "$overall_score >= 90" | bc -l) )); then
|
if (( $(echo "$overall_score >= 90" | bc -l) )); then
|
||||||
echo "overall_status=🌟 Excellent" >> $GITHUB_OUTPUT
|
echo "overall_status=🌟 Excellent" >> $GITHUB_OUTPUT
|
||||||
@@ -252,7 +252,7 @@ jobs:
|
|||||||
echo "overall_status=❌ Poor" >> $GITHUB_OUTPUT
|
echo "overall_status=❌ Poor" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Find existing PR comment
|
- name: Find existing PR comment
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
id: find_comment
|
id: find_comment
|
||||||
@@ -261,7 +261,7 @@ jobs:
|
|||||||
issue-number: ${{ github.event.pull_request.number }}
|
issue-number: ${{ github.event.pull_request.number }}
|
||||||
comment-author: 'github-actions[bot]'
|
comment-author: 'github-actions[bot]'
|
||||||
body-includes: '## 🧪 Test Results Summary'
|
body-includes: '## 🧪 Test Results Summary'
|
||||||
|
|
||||||
- name: Create or update PR comment
|
- name: Create or update PR comment
|
||||||
if: github.event_name == 'pull_request'
|
if: github.event_name == 'pull_request'
|
||||||
uses: peter-evans/create-or-update-comment@v4
|
uses: peter-evans/create-or-update-comment@v4
|
||||||
@@ -271,11 +271,11 @@ jobs:
|
|||||||
edit-mode: replace
|
edit-mode: replace
|
||||||
body: |
|
body: |
|
||||||
## 🧪 Test Results Summary
|
## 🧪 Test Results Summary
|
||||||
|
|
||||||
**Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }}
|
**Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }}
|
||||||
|
|
||||||
### 📊 Metrics Dashboard
|
### 📊 Metrics Dashboard
|
||||||
|
|
||||||
| Category | Status | Count | Details |
|
| Category | Status | Count | Details |
|
||||||
|----------|---------|-------|---------|
|
|----------|---------|-------|---------|
|
||||||
| 📝 **Code Formatting** | ${{ steps.format.outputs.status }} | ${{ steps.format.outputs.count }} issues | `mix format --check-formatted` |
|
| 📝 **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` |
|
| 📊 **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 }} |
|
| 🎯 **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` |
|
| 🔍 **Dialyzer** | ${{ steps.dialyzer.outputs.status }} | ${{ steps.dialyzer.outputs.errors }} errors, ${{ steps.dialyzer.outputs.warnings }} warnings | `mix dialyzer` |
|
||||||
|
|
||||||
### 🎯 Quality Gates
|
### 🎯 Quality Gates
|
||||||
|
|
||||||
Based on the project's quality thresholds:
|
Based on the project's quality thresholds:
|
||||||
- **Compilation Warnings**: ${{ steps.compile.outputs.warnings }}/148 (limit: 148)
|
- **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)
|
- **Dialyzer Warnings**: ${{ steps.dialyzer.outputs.warnings }}/161 (limit: 161)
|
||||||
- **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%)
|
- **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%)
|
||||||
- **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0)
|
- **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0)
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>📈 Progress Toward Goals</summary>
|
<summary>📈 Progress Toward Goals</summary>
|
||||||
|
|
||||||
Target goals for the project:
|
Target goals for the project:
|
||||||
- ✨ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }})
|
- ✨ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }})
|
||||||
- ✨ **≤10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }})
|
- ✨ **≤10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }})
|
||||||
- ✨ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }})
|
- ✨ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }})
|
||||||
- ✨ **≥85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%)
|
- ✨ **≥85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%)
|
||||||
- ✅ **Zero test failures** (currently: ${{ steps.tests.outputs.failures }})
|
- ✅ **Zero test failures** (currently: ${{ steps.tests.outputs.failures }})
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>🔧 Quick Actions</summary>
|
<summary>🔧 Quick Actions</summary>
|
||||||
|
|
||||||
To improve code quality:
|
To improve code quality:
|
||||||
```bash
|
```bash
|
||||||
# Fix formatting issues
|
# Fix formatting issues
|
||||||
mix format
|
mix format
|
||||||
|
|
||||||
# View detailed Credo analysis
|
# View detailed Credo analysis
|
||||||
mix credo --strict
|
mix credo --strict
|
||||||
|
|
||||||
# Check Dialyzer warnings
|
# Check Dialyzer warnings
|
||||||
mix dialyzer
|
mix dialyzer
|
||||||
|
|
||||||
# Generate detailed coverage report
|
# Generate detailed coverage report
|
||||||
mix coveralls.html
|
mix coveralls.html
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
🤖 *Auto-generated by GitHub Actions* • Updated: ${{ github.event.head_commit.timestamp }}
|
🤖 *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.
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -17,9 +17,6 @@ repomix*
|
|||||||
/priv/static/images/
|
/priv/static/images/
|
||||||
/priv/static/*.js
|
/priv/static/*.js
|
||||||
/priv/static/*.css
|
/priv/static/*.css
|
||||||
/priv/static/*-*.png
|
|
||||||
/priv/static/*-*.webp
|
|
||||||
/priv/static/*-*.webmanifest
|
|
||||||
|
|
||||||
# Dialyzer PLT files
|
# Dialyzer PLT files
|
||||||
/priv/plts/
|
/priv/plts/
|
||||||
|
|||||||
383
CHANGELOG.md
383
CHANGELOG.md
@@ -2,389 +2,6 @@
|
|||||||
|
|
||||||
<!-- changelog -->
|
<!-- changelog -->
|
||||||
|
|
||||||
## [v1.92.0](https://github.com/wanderer-industries/wanderer/compare/v1.91.11...v1.92.0) (2026-01-14)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
|
|
||||||
* Added ability to select a range of wh classes for k162.
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: Show c1/c2/c3 or c4/c5 or link signature modal
|
|
||||||
|
|
||||||
## [v1.91.11](https://github.com/wanderer-industries/wanderer/compare/v1.91.10...v1.91.11) (2026-01-13)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* allow sig api when map relay is off
|
|
||||||
|
|
||||||
## [v1.91.10](https://github.com/wanderer-industries/wanderer/compare/v1.91.9...v1.91.10) (2026-01-07)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* remove actor context requirement from sig api
|
|
||||||
|
|
||||||
## [v1.91.9](https://github.com/wanderer-industries/wanderer/compare/v1.91.8...v1.91.9) (2026-01-06)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed rally point cancel logic
|
|
||||||
|
|
||||||
## [v1.91.8](https://github.com/wanderer-industries/wanderer/compare/v1.91.7...v1.91.8) (2026-01-06)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed rally point cancel logic
|
|
||||||
|
|
||||||
## [v1.91.7](https://github.com/wanderer-industries/wanderer/compare/v1.91.6...v1.91.7) (2026-01-05)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.91.6](https://github.com/wanderer-industries/wanderer/compare/v1.91.5...v1.91.6) (2026-01-04)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed new connections got deleted after linked signature cleanup
|
|
||||||
|
|
||||||
## [v1.91.5](https://github.com/wanderer-industries/wanderer/compare/v1.91.4...v1.91.5) (2025-12-30)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.91.4](https://github.com/wanderer-industries/wanderer/compare/v1.91.3...v1.91.4) (2025-12-30)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed connections create between k-space systems (considered as wh connection)
|
|
||||||
|
|
||||||
## [v1.91.3](https://github.com/wanderer-industries/wanderer/compare/v1.91.2...v1.91.3) (2025-12-28)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.91.2](https://github.com/wanderer-industries/wanderer/compare/v1.91.1...v1.91.2) (2025-12-27)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed map scopes updates & logic
|
|
||||||
|
|
||||||
## [v1.91.1](https://github.com/wanderer-industries/wanderer/compare/v1.91.0...v1.91.1) (2025-12-25)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.91.0](https://github.com/wanderer-industries/wanderer/compare/v1.90.13...v1.91.0) (2025-12-24)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
|
|
||||||
* admin: added maps administration view with basic info, search, restore/delete, acls view and edit options
|
|
||||||
|
|
||||||
## [v1.90.13](https://github.com/wanderer-industries/wanderer/compare/v1.90.12...v1.90.13) (2025-12-19)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed welcome page
|
|
||||||
|
|
||||||
## [v1.90.12](https://github.com/wanderer-industries/wanderer/compare/v1.90.11...v1.90.12) (2025-12-19)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed permissions update after character corp updates
|
|
||||||
|
|
||||||
## [v1.90.11](https://github.com/wanderer-industries/wanderer/compare/v1.90.10...v1.90.11) (2025-12-18)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.90.10](https://github.com/wanderer-industries/wanderer/compare/v1.90.9...v1.90.10) (2025-12-18)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.90.9](https://github.com/wanderer-industries/wanderer/compare/v1.90.8...v1.90.9) (2025-12-15)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: reduce chracters untrack grace period to 15 mins (after change/close/disconnect from map)
|
|
||||||
|
|
||||||
## [v1.90.8](https://github.com/wanderer-industries/wanderer/compare/v1.90.7...v1.90.8) (2025-12-15)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: skip systems or connections cleanup for not started maps
|
|
||||||
|
|
||||||
## [v1.90.7](https://github.com/wanderer-industries/wanderer/compare/v1.90.6...v1.90.7) (2025-12-15)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed scopes
|
|
||||||
|
|
||||||
## [v1.90.6](https://github.com/wanderer-industries/wanderer/compare/v1.90.5...v1.90.6) (2025-12-12)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed map scopes
|
|
||||||
|
|
||||||
## [v1.90.5](https://github.com/wanderer-industries/wanderer/compare/v1.90.4...v1.90.5) (2025-12-12)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed map scopes
|
|
||||||
|
|
||||||
## [v1.90.4](https://github.com/wanderer-industries/wanderer/compare/v1.90.3...v1.90.4) (2025-12-12)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed map scopes & signatures clean up behaviour
|
|
||||||
|
|
||||||
## [v1.90.3](https://github.com/wanderer-industries/wanderer/compare/v1.90.2...v1.90.3) (2025-12-11)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: added pagination for long ACL lists
|
|
||||||
|
|
||||||
## [v1.90.2](https://github.com/wanderer-industries/wanderer/compare/v1.90.1...v1.90.2) (2025-12-10)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: added system position updates to SSE
|
|
||||||
|
|
||||||
## [v1.90.1](https://github.com/wanderer-industries/wanderer/compare/v1.90.0...v1.90.1) (2025-12-08)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed connections and signatures remove issues, added comprehensive audit log for auto removed connections and signatures
|
|
||||||
|
|
||||||
## [v1.90.0](https://github.com/wanderer-industries/wanderer/compare/v1.89.6...v1.90.0) (2025-12-06)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Features:
|
|
||||||
|
|
||||||
* core: Added several map scopes support (Wh, Hi, Low, Null, Pochven)
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed clean up for linked signatures
|
|
||||||
|
|
||||||
* core: fixed issue with default select mode
|
|
||||||
|
|
||||||
* apiV1 default fields updates
|
|
||||||
|
|
||||||
## [v1.89.6](https://github.com/wanderer-industries/wanderer/compare/v1.89.5...v1.89.6) (2025-12-02)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* kills: fixed zkb links (added "allow-popups-to-escape-sandbox" to CSP)
|
|
||||||
|
|
||||||
## [v1.89.5](https://github.com/wanderer-industries/wanderer/compare/v1.89.4...v1.89.5) (2025-12-02)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [v1.89.4](https://github.com/wanderer-industries/wanderer/compare/v1.89.3...v1.89.4) (2025-12-02)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed acl character update issues
|
|
||||||
|
|
||||||
## [v1.89.3](https://github.com/wanderer-industries/wanderer/compare/v1.89.2...v1.89.3) (2025-11-30)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed tracking issues
|
|
||||||
|
|
||||||
## [v1.89.2](https://github.com/wanderer-industries/wanderer/compare/v1.89.1...v1.89.2) (2025-11-30)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## [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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed env errors
|
|
||||||
|
|
||||||
## [v1.88.4](https://github.com/wanderer-industries/wanderer/compare/v1.88.3...v1.88.4) (2025-11-27)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* defensive check for undefined excluded systems
|
|
||||||
|
|
||||||
## [v1.88.3](https://github.com/wanderer-industries/wanderer/compare/v1.88.2...v1.88.3) (2025-11-26)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Bug Fixes:
|
|
||||||
|
|
||||||
* core: fixed env issues
|
|
||||||
|
|
||||||
## [v1.88.1](https://github.com/wanderer-industries/wanderer/compare/v1.88.0...v1.88.1) (2025-11-26)
|
## [v1.88.1](https://github.com/wanderer-industries/wanderer/compare/v1.88.0...v1.88.1) (2025-11-26)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
52
Makefile
52
Makefile
@@ -32,58 +32,8 @@ format f:
|
|||||||
test t:
|
test t:
|
||||||
MIX_ENV=test mix test
|
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:
|
coverage cover co:
|
||||||
MIX_ENV=test mix test --cover
|
mix test --cover
|
||||||
|
|
||||||
unit-tests ut:
|
unit-tests ut:
|
||||||
@echo "Running unit tests..."
|
@echo "Running unit tests..."
|
||||||
|
|||||||
@@ -1001,27 +1001,3 @@ body > div:first-of-type {
|
|||||||
.verticalTabsContainer .p-tabview-panel {
|
.verticalTabsContainer .p-tabview-panel {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Blog post CTA links - only in main post content */
|
|
||||||
.post-content a {
|
|
||||||
display: inline-block;
|
|
||||||
background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
|
|
||||||
color: white !important;
|
|
||||||
padding: 0.5rem 1.25rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
text-decoration: none !important;
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
box-shadow: 0 4px 15px rgba(236, 72, 153, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-content a:hover {
|
|
||||||
background: linear-gradient(135deg, #db2777 0%, #7c3aed 100%);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 20px rgba(236, 72, 153, 0.4);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post-content a:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: 0 2px 10px rgba(236, 72, 153, 0.3);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
storedSettings: { interfaceSettings },
|
storedSettings: { interfaceSettings },
|
||||||
data: { systemSignatures: mapSystemSignatures, pings },
|
data: { systemSignatures: mapSystemSignatures },
|
||||||
} = useMapRootState();
|
} = useMapRootState();
|
||||||
|
|
||||||
const systemStaticInfo = useMemo(() => {
|
const systemStaticInfo = useMemo(() => {
|
||||||
@@ -108,6 +108,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
|||||||
visibleNodes,
|
visibleNodes,
|
||||||
showKSpaceBG,
|
showKSpaceBG,
|
||||||
isThickConnections,
|
isThickConnections,
|
||||||
|
pings,
|
||||||
systemHighlighted,
|
systemHighlighted,
|
||||||
},
|
},
|
||||||
outCommand,
|
outCommand,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { MarkdownComment } from '@/hooks/Mapper/components/mapInterface/components/Comments/components';
|
import { MarkdownComment } from '@/hooks/Mapper/components/mapInterface/components/Comments/components';
|
||||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { CommentType } from '@/hooks/Mapper/types';
|
import { CommentType } from '@/hooks/Mapper/types';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||||
|
|
||||||
export interface CommentsProps {}
|
export interface CommentsProps {}
|
||||||
|
|
||||||
@@ -14,9 +14,7 @@ export const Comments = ({}: CommentsProps) => {
|
|||||||
comments: { loadComments, comments, lastUpdateKey },
|
comments: { loadComments, comments, lastUpdateKey },
|
||||||
} = useMapRootState();
|
} = useMapRootState();
|
||||||
|
|
||||||
const systemId = useMemo(() => {
|
const [systemId] = selectedSystems;
|
||||||
return +selectedSystems[0];
|
|
||||||
}, [selectedSystems]);
|
|
||||||
|
|
||||||
const ref = useRef({ loadComments, systemId });
|
const ref = useRef({ loadComments, systemId });
|
||||||
ref.current = { loadComments, systemId };
|
ref.current = { loadComments, systemId };
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import clsx from 'clsx';
|
|||||||
import { PrimeIcons } from 'primereact/api';
|
import { PrimeIcons } from 'primereact/api';
|
||||||
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
|
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
|
||||||
import { useHotkey } from '@/hooks/Mapper/hooks';
|
import { useHotkey } from '@/hooks/Mapper/hooks';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
import { OutCommand } from '@/hooks/Mapper/types';
|
import { OutCommand } from '@/hooks/Mapper/types';
|
||||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||||
import classes from './CommentsEditor.module.scss';
|
import classes from './CommentsEditor.module.scss';
|
||||||
@@ -19,9 +19,7 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
|
|||||||
outCommand,
|
outCommand,
|
||||||
} = useMapRootState();
|
} = useMapRootState();
|
||||||
|
|
||||||
const systemId = useMemo(() => {
|
const [systemId] = selectedSystems;
|
||||||
return +selectedSystems[0];
|
|
||||||
}, [selectedSystems]);
|
|
||||||
|
|
||||||
const ref = useRef({ outCommand, systemId, textVal });
|
const ref = useRef({ outCommand, systemId, textVal });
|
||||||
ref.current = { outCommand, systemId, textVal };
|
ref.current = { outCommand, systemId, textVal };
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ping) {
|
if (!ping) {
|
||||||
setIsShow(false);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,26 +161,27 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
|||||||
};
|
};
|
||||||
}, [interfaceSettings]);
|
}, [interfaceSettings]);
|
||||||
|
|
||||||
const isShowSelectedSystem = ping && selectedSystem != null && selectedSystem !== ping.solar_system_id;
|
if (!ping) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
|
||||||
|
|
||||||
// Only render Toast when there's a ping
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{ping && (
|
<Toast
|
||||||
<Toast
|
position={placement as never}
|
||||||
key={ping.id}
|
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
|
||||||
position={placement as never}
|
ref={toast}
|
||||||
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
|
content={({ message }) => (
|
||||||
ref={toast}
|
<section
|
||||||
content={({ message }) => (
|
className={clsx(
|
||||||
<section
|
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
|
||||||
className={clsx(
|
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
|
||||||
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
|
)}
|
||||||
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
|
>
|
||||||
)}
|
<div className="flex gap-3">
|
||||||
>
|
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
|
||||||
<div className="flex gap-3">
|
|
||||||
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
|
|
||||||
<div className="flex flex-col gap-1 w-full">
|
<div className="flex flex-col gap-1 w-full">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -253,33 +253,28 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
|||||||
{/*/>*/}
|
{/*/>*/}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
></Toast>
|
></Toast>
|
||||||
)}
|
|
||||||
|
|
||||||
{ping && (
|
<WdButton
|
||||||
<>
|
icon="pi pi-bell"
|
||||||
<WdButton
|
severity="warning"
|
||||||
icon="pi pi-bell"
|
aria-label="Notification"
|
||||||
severity="warning"
|
size="small"
|
||||||
aria-label="Notification"
|
className="w-[33px] h-[33px]"
|
||||||
size="small"
|
outlined
|
||||||
className="w-[33px] h-[33px]"
|
onClick={handleClickShow}
|
||||||
outlined
|
disabled={isShow}
|
||||||
onClick={handleClickShow}
|
/>
|
||||||
disabled={isShow}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConfirmPopup
|
<ConfirmPopup
|
||||||
target={cfRef.current}
|
target={cfRef.current}
|
||||||
visible={cfVisible}
|
visible={cfVisible}
|
||||||
onHide={cfHide}
|
onHide={cfHide}
|
||||||
message="Are you sure you want to delete ping?"
|
message="Are you sure you want to delete ping?"
|
||||||
icon="pi pi-exclamation-triangle text-orange-400"
|
icon="pi pi-exclamation-triangle text-orange-400"
|
||||||
accept={removePing}
|
accept={removePing}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|||||||
|
|
||||||
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
|
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
|
||||||
import {
|
import {
|
||||||
SOLAR_SYSTEM_CLASS_IDS,
|
SOLAR_SYSTEM_CLASS_IDS,
|
||||||
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
|
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
|
||||||
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
|
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
|
||||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||||
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
|
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
|
||||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||||
@@ -91,7 +91,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
|||||||
|
|
||||||
if (k162TypeInfo) {
|
if (k162TypeInfo) {
|
||||||
// Check if the k162Type matches our target system class
|
// Check if the k162Type matches our target system class
|
||||||
return k162TypeInfo.value.includes(targetSystemClassGroup);
|
return customInfo.k162Type === targetSystemClassGroup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export function useSystemKills({ systemId, outCommand, showAllVisible = false, s
|
|||||||
storedSettings: { settingsKills },
|
storedSettings: { settingsKills },
|
||||||
} = useMapRootState();
|
} = useMapRootState();
|
||||||
|
|
||||||
const excludedSystems = useStableValue(settingsKills.excludedSystems ?? []);
|
const excludedSystems = useStableValue(settingsKills.excludedSystems);
|
||||||
|
|
||||||
const effectiveSystemIds = useMemo(() => {
|
const effectiveSystemIds = useMemo(() => {
|
||||||
if (showAllVisible) {
|
if (showAllVisible) {
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/compone
|
|||||||
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
|
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
|
||||||
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
|
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
|
||||||
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity';
|
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity';
|
||||||
import { WormholeSignaturesDialog } from '@/hooks/Mapper/components/mapRootContent/components/WormholeSignaturesDialog';
|
|
||||||
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
|
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
|
||||||
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
|
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
|
||||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||||
@@ -35,7 +34,6 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
|||||||
const [showOnTheMap, setShowOnTheMap] = useState(false);
|
const [showOnTheMap, setShowOnTheMap] = useState(false);
|
||||||
const [showMapSettings, setShowMapSettings] = useState(false);
|
const [showMapSettings, setShowMapSettings] = useState(false);
|
||||||
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
|
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
|
||||||
const [showWormholeList, setShowWormholeList] = useState(false);
|
|
||||||
|
|
||||||
/* Important Notice - this solution needs for use one instance of MapInterface */
|
/* Important Notice - this solution needs for use one instance of MapInterface */
|
||||||
const mapInterface = isReady ? <MapInterface /> : null;
|
const mapInterface = isReady ? <MapInterface /> : null;
|
||||||
@@ -43,7 +41,6 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
|||||||
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
|
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
|
||||||
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
|
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
|
||||||
const handleShowTrackingDialog = useCallback(() => setShowTrackingDialog(true), []);
|
const handleShowTrackingDialog = useCallback(() => setShowTrackingDialog(true), []);
|
||||||
const handleShowWormholesReference = useCallback(() => setShowWormholeList(true), []);
|
|
||||||
|
|
||||||
useMapEventListener(event => {
|
useMapEventListener(event => {
|
||||||
if (event.name === Commands.showTracking) {
|
if (event.name === Commands.showTracking) {
|
||||||
@@ -68,7 +65,6 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
|||||||
onShowOnTheMap={handleShowOnTheMap}
|
onShowOnTheMap={handleShowOnTheMap}
|
||||||
onShowMapSettings={handleShowMapSettings}
|
onShowMapSettings={handleShowMapSettings}
|
||||||
onShowTrackingDialog={handleShowTrackingDialog}
|
onShowTrackingDialog={handleShowTrackingDialog}
|
||||||
onShowWormholesReference={handleShowWormholesReference}
|
|
||||||
additionalContent={<PingsInterface hasLeftOffset />}
|
additionalContent={<PingsInterface hasLeftOffset />}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +79,6 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
|||||||
onShowOnTheMap={handleShowOnTheMap}
|
onShowOnTheMap={handleShowOnTheMap}
|
||||||
onShowMapSettings={handleShowMapSettings}
|
onShowMapSettings={handleShowMapSettings}
|
||||||
onShowTrackingDialog={handleShowTrackingDialog}
|
onShowTrackingDialog={handleShowTrackingDialog}
|
||||||
onShowWormholesReference={handleShowWormholesReference}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Topbar>
|
</Topbar>
|
||||||
@@ -98,7 +93,6 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
|||||||
{showTrackingDialog && (
|
{showTrackingDialog && (
|
||||||
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
|
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
|
||||||
)}
|
)}
|
||||||
<WormholeSignaturesDialog visible={showWormholeList} onHide={() => setShowWormholeList(false)} />
|
|
||||||
|
|
||||||
{hasOldSettings && <OldSettingsDialog />}
|
{hasOldSettings && <OldSettingsDialog />}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -12,15 +12,9 @@ export interface MapContextMenuProps {
|
|||||||
onShowOnTheMap?: () => void;
|
onShowOnTheMap?: () => void;
|
||||||
onShowMapSettings?: () => void;
|
onShowMapSettings?: () => void;
|
||||||
onShowTrackingDialog?: () => void;
|
onShowTrackingDialog?: () => void;
|
||||||
onShowWormholesReference?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MapContextMenu = ({
|
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: MapContextMenuProps) => {
|
||||||
onShowOnTheMap,
|
|
||||||
onShowMapSettings,
|
|
||||||
onShowTrackingDialog,
|
|
||||||
onShowWormholesReference,
|
|
||||||
}: MapContextMenuProps) => {
|
|
||||||
const {
|
const {
|
||||||
outCommand,
|
outCommand,
|
||||||
storedSettings: { setInterfaceSettings },
|
storedSettings: { setInterfaceSettings },
|
||||||
@@ -58,12 +52,6 @@ export const MapContextMenu = ({
|
|||||||
command: onShowOnTheMap,
|
command: onShowOnTheMap,
|
||||||
visible: canTrackCharacters,
|
visible: canTrackCharacters,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Wormholes Ref.',
|
|
||||||
icon: 'pi pi-bullseye',
|
|
||||||
command: onShowWormholesReference,
|
|
||||||
visible: canTrackCharacters,
|
|
||||||
},
|
|
||||||
{ separator: true, visible: true },
|
{ separator: true, visible: true },
|
||||||
{
|
{
|
||||||
label: 'Settings',
|
label: 'Settings',
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ interface RightBarProps {
|
|||||||
onShowOnTheMap?: () => void;
|
onShowOnTheMap?: () => void;
|
||||||
onShowMapSettings?: () => void;
|
onShowMapSettings?: () => void;
|
||||||
onShowTrackingDialog?: () => void;
|
onShowTrackingDialog?: () => void;
|
||||||
onShowWormholesReference?: () => void;
|
|
||||||
additionalContent?: ReactNode;
|
additionalContent?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ export const RightBar = ({
|
|||||||
onShowOnTheMap,
|
onShowOnTheMap,
|
||||||
onShowMapSettings,
|
onShowMapSettings,
|
||||||
onShowTrackingDialog,
|
onShowTrackingDialog,
|
||||||
onShowWormholesReference,
|
|
||||||
additionalContent,
|
additionalContent,
|
||||||
}: RightBarProps) => {
|
}: RightBarProps) => {
|
||||||
const {
|
const {
|
||||||
@@ -92,16 +90,6 @@ export const RightBar = ({
|
|||||||
<i className="pi pi-hashtag"></i>
|
<i className="pi pi-hashtag"></i>
|
||||||
</button>
|
</button>
|
||||||
</WdTooltipWrapper>
|
</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>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -13,26 +13,6 @@ export const renderK162Type = (option: K162Type) => {
|
|||||||
return renderNoValue();
|
return renderNoValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (['c1_c2_c3', 'c4_c5'].includes(value)) {
|
|
||||||
const arr = whClassName.split('_');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
{arr.map(x => (
|
|
||||||
<WHClassView
|
|
||||||
key={x}
|
|
||||||
classNameWh="!text-[11px] !font-bold"
|
|
||||||
hideWhClassName
|
|
||||||
hideTooltip
|
|
||||||
whClassName={x}
|
|
||||||
noOffset
|
|
||||||
useShortTitle
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WHClassView
|
<WHClassView
|
||||||
classNameWh="!text-[11px] !font-bold"
|
classNameWh="!text-[11px] !font-bold"
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { createContext, useCallback, useContext, useRef, useState, useEffect } from 'react';
|
import { createContext, useCallback, useContext, useRef, useState } from 'react';
|
||||||
import { Commands, OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
|
import { OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
|
||||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||||
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
|
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||||
import { CommandInCharactersTrackingInfo } from '@/hooks/Mapper/types/commandsIn.ts';
|
import { CommandInCharactersTrackingInfo } from '@/hooks/Mapper/types/commandsIn.ts';
|
||||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
|
||||||
|
|
||||||
type DiffTrackingInfo = { characterId: string; tracked: boolean };
|
type DiffTrackingInfo = { characterId: string; tracked: boolean };
|
||||||
|
|
||||||
@@ -123,14 +122,6 @@ export const TrackingProvider = ({ children }: WithChildren) => {
|
|||||||
[outCommand],
|
[outCommand],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Listen for refresh_tracking_data event (triggered when ACL members change)
|
|
||||||
useMapEventListener(event => {
|
|
||||||
if (event.name === Commands.refreshTrackingData) {
|
|
||||||
loadTracking();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TrackingContext.Provider
|
<TrackingContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './WormholeSignaturesDialog';
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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,4 +23,3 @@ export * from './MenuItemWithInfo';
|
|||||||
export * from './MarkdownTextViewer.tsx';
|
export * from './MarkdownTextViewer.tsx';
|
||||||
export * from './WdButton.tsx';
|
export * from './WdButton.tsx';
|
||||||
export * from './constants.ts';
|
export * from './constants.ts';
|
||||||
export * from './RespawnTag';
|
|
||||||
|
|||||||
@@ -133,16 +133,6 @@ export const K162_TYPES: K162Type[] = [
|
|||||||
value: 'pochven',
|
value: 'pochven',
|
||||||
whClassName: 'F216',
|
whClassName: 'F216',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'C1/C2/C3',
|
|
||||||
value: 'c1_c2_c3',
|
|
||||||
whClassName: 'E004_D382_L477',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'C4/C5',
|
|
||||||
value: 'c4_c5',
|
|
||||||
whClassName: 'M001_L614',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const K162_TYPES_MAP: { [key: string]: K162Type } = K162_TYPES.reduce(
|
export const K162_TYPES_MAP: { [key: string]: K162Type } = K162_TYPES.reduce(
|
||||||
|
|||||||
@@ -10,4 +10,3 @@ export * from './useCommandComments';
|
|||||||
export * from './useGetCacheCharacter';
|
export * from './useGetCacheCharacter';
|
||||||
export * from './useCommandsActivity';
|
export * from './useCommandsActivity';
|
||||||
export * from './useCommandPings';
|
export * from './useCommandPings';
|
||||||
export * from './useCommandPingBlocked';
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const useCommandComments = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const removeComment = useCallback((data: CommandCommentRemoved) => {
|
const removeComment = useCallback((data: CommandCommentRemoved) => {
|
||||||
ref.current.removeComment(data.solarSystemId, data.commentId);
|
ref.current.removeComment(data.solarSystemId.toString(), data.commentId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return { addComment, removeComment };
|
return { addComment, removeComment };
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { useToast } from '@/hooks/Mapper/ToastProvider';
|
|
||||||
import { CommandPingBlocked } from '@/hooks/Mapper/types';
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|
||||||
export const useCommandPingBlocked = () => {
|
|
||||||
const { show } = useToast();
|
|
||||||
|
|
||||||
const pingBlocked = useCallback(
|
|
||||||
({ message }: CommandPingBlocked) => {
|
|
||||||
show({
|
|
||||||
severity: 'warn',
|
|
||||||
summary: 'Cannot create ping',
|
|
||||||
detail: message,
|
|
||||||
life: 5000,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[show],
|
|
||||||
);
|
|
||||||
|
|
||||||
return { pingBlocked };
|
|
||||||
};
|
|
||||||
@@ -14,8 +14,8 @@ export const useCommandPings = () => {
|
|||||||
ref.current.update({ pings });
|
ref.current.update({ pings });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const pingCancelled = useCallback(({ id }: CommandPingCancelled) => {
|
const pingCancelled = useCallback(({ type, id }: CommandPingCancelled) => {
|
||||||
const newPings = ref.current.pings.filter(x => x.id !== id);
|
const newPings = ref.current.pings.filter(x => x.id !== id && x.type !== type);
|
||||||
ref.current.update({ pings: newPings });
|
ref.current.update({ pings: newPings });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { CommentSystem, CommentType, OutCommand, OutCommandHandler, UseCommentsData } from '@/hooks/Mapper/types';
|
|
||||||
import { useCallback, useRef, useState } from 'react';
|
import { useCallback, useRef, useState } from 'react';
|
||||||
|
import { CommentSystem, CommentType, OutCommand, OutCommandHandler, UseCommentsData } from '@/hooks/Mapper/types';
|
||||||
|
|
||||||
interface UseCommentsProps {
|
interface UseCommentsProps {
|
||||||
outCommand: OutCommandHandler;
|
outCommand: OutCommandHandler;
|
||||||
@@ -8,12 +8,12 @@ interface UseCommentsProps {
|
|||||||
export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData => {
|
export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData => {
|
||||||
const [lastUpdateKey, setLastUpdateKey] = useState(0);
|
const [lastUpdateKey, setLastUpdateKey] = useState(0);
|
||||||
|
|
||||||
const commentBySystemsRef = useRef<Map<number, CommentSystem>>(new Map());
|
const commentBySystemsRef = useRef<Map<string, CommentSystem>>(new Map());
|
||||||
|
|
||||||
const ref = useRef({ outCommand });
|
const ref = useRef({ outCommand });
|
||||||
ref.current = { outCommand };
|
ref.current = { outCommand };
|
||||||
|
|
||||||
const loadComments = useCallback(async (systemId: number) => {
|
const loadComments = useCallback(async (systemId: string) => {
|
||||||
let cSystem = commentBySystemsRef.current.get(systemId);
|
let cSystem = commentBySystemsRef.current.get(systemId);
|
||||||
if (cSystem?.loading || cSystem?.loaded) {
|
if (cSystem?.loading || cSystem?.loaded) {
|
||||||
return;
|
return;
|
||||||
@@ -45,7 +45,7 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
|
|||||||
setLastUpdateKey(x => x + 1);
|
setLastUpdateKey(x => x + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const addComment = useCallback((systemId: number, comment: CommentType) => {
|
const addComment = useCallback((systemId: string, comment: CommentType) => {
|
||||||
const cSystem = commentBySystemsRef.current.get(systemId);
|
const cSystem = commentBySystemsRef.current.get(systemId);
|
||||||
if (cSystem) {
|
if (cSystem) {
|
||||||
cSystem.comments.push(comment);
|
cSystem.comments.push(comment);
|
||||||
@@ -61,7 +61,7 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
|
|||||||
setLastUpdateKey(x => x + 1);
|
setLastUpdateKey(x => x + 1);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const removeComment = useCallback((systemId: number, commentId: string) => {
|
const removeComment = useCallback((systemId: string, commentId: string) => {
|
||||||
const cSystem = commentBySystemsRef.current.get(systemId);
|
const cSystem = commentBySystemsRef.current.get(systemId);
|
||||||
if (!cSystem) {
|
if (!cSystem) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
CommandLinkSignatureToSystem,
|
CommandLinkSignatureToSystem,
|
||||||
CommandMapUpdated,
|
CommandMapUpdated,
|
||||||
CommandPingAdded,
|
CommandPingAdded,
|
||||||
CommandPingBlocked,
|
|
||||||
CommandPingCancelled,
|
CommandPingCancelled,
|
||||||
CommandPresentCharacters,
|
CommandPresentCharacters,
|
||||||
CommandRemoveConnections,
|
CommandRemoveConnections,
|
||||||
@@ -30,7 +29,6 @@ import { ForwardedRef, useImperativeHandle } from 'react';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
useCommandComments,
|
useCommandComments,
|
||||||
useCommandPingBlocked,
|
|
||||||
useCommandPings,
|
useCommandPings,
|
||||||
useCommandsCharacters,
|
useCommandsCharacters,
|
||||||
useCommandsConnections,
|
useCommandsConnections,
|
||||||
@@ -63,128 +61,129 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
|||||||
const mapUserRoutes = useUserRoutes();
|
const mapUserRoutes = useUserRoutes();
|
||||||
const { addComment, removeComment } = useCommandComments();
|
const { addComment, removeComment } = useCommandComments();
|
||||||
const { pingAdded, pingCancelled } = useCommandPings();
|
const { pingAdded, pingCancelled } = useCommandPings();
|
||||||
const { pingBlocked } = useCommandPingBlocked();
|
|
||||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||||
|
|
||||||
useImperativeHandle(ref, () => {
|
useImperativeHandle(
|
||||||
return {
|
ref,
|
||||||
command(type, data) {
|
() => {
|
||||||
switch (type) {
|
return {
|
||||||
case Commands.init: // USED
|
command(type, data) {
|
||||||
mapInit(data as CommandInit);
|
switch (type) {
|
||||||
break;
|
case Commands.init: // USED
|
||||||
case Commands.addSystems: // USED
|
mapInit(data as CommandInit);
|
||||||
addSystems(data as CommandAddSystems);
|
break;
|
||||||
break;
|
case Commands.addSystems: // USED
|
||||||
case Commands.updateSystems: // USED
|
addSystems(data as CommandAddSystems);
|
||||||
updateSystems(data as CommandUpdateSystems);
|
break;
|
||||||
break;
|
case Commands.updateSystems: // USED
|
||||||
case Commands.removeSystems: // USED
|
updateSystems(data as CommandUpdateSystems);
|
||||||
removeSystems(data as CommandRemoveSystems);
|
break;
|
||||||
break;
|
case Commands.removeSystems: // USED
|
||||||
case Commands.addConnections: // USED
|
removeSystems(data as CommandRemoveSystems);
|
||||||
addConnections(data as CommandAddConnections);
|
break;
|
||||||
break;
|
case Commands.addConnections: // USED
|
||||||
case Commands.removeConnections: // USED
|
addConnections(data as CommandAddConnections);
|
||||||
removeConnections(data as CommandRemoveConnections);
|
break;
|
||||||
break;
|
case Commands.removeConnections: // USED
|
||||||
case Commands.updateConnection: // USED
|
removeConnections(data as CommandRemoveConnections);
|
||||||
updateConnection(data as CommandUpdateConnection);
|
break;
|
||||||
break;
|
case Commands.updateConnection: // USED
|
||||||
case Commands.charactersUpdated: // USED
|
updateConnection(data as CommandUpdateConnection);
|
||||||
charactersUpdated(data as CommandCharactersUpdated);
|
break;
|
||||||
break;
|
case Commands.charactersUpdated: // USED
|
||||||
case Commands.characterAdded: // USED
|
charactersUpdated(data as CommandCharactersUpdated);
|
||||||
characterAdded(data as CommandCharacterAdded);
|
break;
|
||||||
break;
|
case Commands.characterAdded: // USED
|
||||||
case Commands.characterRemoved: // USED
|
characterAdded(data as CommandCharacterAdded);
|
||||||
characterRemoved(data as CommandCharacterRemoved);
|
break;
|
||||||
break;
|
case Commands.characterRemoved: // USED
|
||||||
case Commands.characterUpdated: // USED
|
characterRemoved(data as CommandCharacterRemoved);
|
||||||
characterUpdated(data as CommandCharacterUpdated);
|
break;
|
||||||
break;
|
case Commands.characterUpdated: // USED
|
||||||
case Commands.presentCharacters: // USED
|
characterUpdated(data as CommandCharacterUpdated);
|
||||||
presentCharacters(data as CommandPresentCharacters);
|
break;
|
||||||
break;
|
case Commands.presentCharacters: // USED
|
||||||
case Commands.mapUpdated: // USED
|
presentCharacters(data as CommandPresentCharacters);
|
||||||
mapUpdated(data as CommandMapUpdated);
|
break;
|
||||||
break;
|
case Commands.mapUpdated: // USED
|
||||||
case Commands.routes:
|
mapUpdated(data as CommandMapUpdated);
|
||||||
mapRoutes(data as CommandRoutes);
|
break;
|
||||||
break;
|
case Commands.routes:
|
||||||
case Commands.userRoutes:
|
mapRoutes(data as CommandRoutes);
|
||||||
mapUserRoutes(data as CommandRoutes);
|
break;
|
||||||
break;
|
case Commands.userRoutes:
|
||||||
|
mapUserRoutes(data as CommandRoutes);
|
||||||
|
break;
|
||||||
|
|
||||||
case Commands.signaturesUpdated: // USED
|
case Commands.signaturesUpdated: // USED
|
||||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.linkSignatureToSystem: // USED
|
case Commands.linkSignatureToSystem: // USED
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||||
}, 200);
|
}, 200);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.centerSystem: // USED
|
case Commands.centerSystem: // USED
|
||||||
// do nothing here
|
// do nothing here
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.selectSystem: // USED
|
case Commands.selectSystem: // USED
|
||||||
// do nothing here
|
// do nothing here
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.killsUpdated:
|
case Commands.killsUpdated:
|
||||||
// do nothing here
|
// do nothing here
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.detailedKillsUpdated:
|
case Commands.detailedKillsUpdated:
|
||||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.characterActivityData:
|
case Commands.characterActivityData:
|
||||||
characterActivityData(data as CommandCharacterActivityData);
|
characterActivityData(data as CommandCharacterActivityData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.trackingCharactersData:
|
case Commands.trackingCharactersData:
|
||||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.updateActivity:
|
case Commands.updateActivity:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.updateTracking:
|
case Commands.updateTracking:
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.userSettingsUpdated:
|
case Commands.userSettingsUpdated:
|
||||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.systemCommentAdded:
|
case Commands.systemCommentAdded:
|
||||||
addComment(data as CommandCommentAdd);
|
addComment(data as CommandCommentAdd);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.systemCommentRemoved:
|
case Commands.systemCommentRemoved:
|
||||||
removeComment(data as CommandCommentRemoved);
|
removeComment(data as CommandCommentRemoved);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.pingAdded:
|
case Commands.pingAdded:
|
||||||
pingAdded(data as CommandPingAdded);
|
pingAdded(data as CommandPingAdded);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Commands.pingCancelled:
|
case Commands.pingCancelled:
|
||||||
pingCancelled(data as CommandPingCancelled);
|
pingCancelled(data as CommandPingCancelled);
|
||||||
break;
|
break;
|
||||||
case Commands.pingBlocked:
|
|
||||||
pingBlocked(data as CommandPingBlocked);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
emitMapEvent({ name: type, data });
|
default:
|
||||||
},
|
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||||
};
|
break;
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
|
emitMapEvent({ name: type, data });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ export type CommentSystem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface UseCommentsData {
|
export interface UseCommentsData {
|
||||||
loadComments: (systemId: number) => Promise<void>;
|
loadComments: (systemId: string) => Promise<void>;
|
||||||
addComment: (systemId: number, comment: CommentType) => void;
|
addComment: (systemId: string, comment: CommentType) => void;
|
||||||
removeComment: (systemId: number, commentId: string) => void;
|
removeComment: (systemId: string, commentId: string) => void;
|
||||||
comments: Map<number, CommentSystem>;
|
comments: Map<string, CommentSystem>;
|
||||||
lastUpdateKey: number;
|
lastUpdateKey: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,10 +38,8 @@ export enum Commands {
|
|||||||
updateTracking = 'update_tracking',
|
updateTracking = 'update_tracking',
|
||||||
userSettingsUpdated = 'user_settings_updated',
|
userSettingsUpdated = 'user_settings_updated',
|
||||||
showTracking = 'show_tracking',
|
showTracking = 'show_tracking',
|
||||||
refreshTrackingData = 'refresh_tracking_data',
|
|
||||||
pingAdded = 'ping_added',
|
pingAdded = 'ping_added',
|
||||||
pingCancelled = 'ping_cancelled',
|
pingCancelled = 'ping_cancelled',
|
||||||
pingBlocked = 'ping_blocked',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Command =
|
export type Command =
|
||||||
@@ -76,10 +74,8 @@ export type Command =
|
|||||||
| Commands.updateActivity
|
| Commands.updateActivity
|
||||||
| Commands.updateTracking
|
| Commands.updateTracking
|
||||||
| Commands.showTracking
|
| Commands.showTracking
|
||||||
| Commands.refreshTrackingData
|
|
||||||
| Commands.pingAdded
|
| Commands.pingAdded
|
||||||
| Commands.pingCancelled
|
| Commands.pingCancelled;
|
||||||
| Commands.pingBlocked;
|
|
||||||
|
|
||||||
export type CommandInit = {
|
export type CommandInit = {
|
||||||
systems: SolarSystemRawType[];
|
systems: SolarSystemRawType[];
|
||||||
@@ -135,7 +131,7 @@ export type CommandLinkSignatureToSystem = {
|
|||||||
};
|
};
|
||||||
export type CommandLinkSignaturesUpdated = number;
|
export type CommandLinkSignaturesUpdated = number;
|
||||||
export type CommandCommentAdd = {
|
export type CommandCommentAdd = {
|
||||||
solarSystemId: number;
|
solarSystemId: string;
|
||||||
comment: CommentType;
|
comment: CommentType;
|
||||||
};
|
};
|
||||||
export type CommandCommentRemoved = {
|
export type CommandCommentRemoved = {
|
||||||
@@ -149,7 +145,6 @@ export type CommandUserSettingsUpdated = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type CommandShowTracking = null;
|
export type CommandShowTracking = null;
|
||||||
export type CommandRefreshTrackingData = Record<string, never>;
|
|
||||||
export type CommandUpdateActivity = {
|
export type CommandUpdateActivity = {
|
||||||
characterId: number;
|
characterId: number;
|
||||||
systemId: number;
|
systemId: number;
|
||||||
@@ -163,10 +158,6 @@ export type CommandUpdateTracking = {
|
|||||||
};
|
};
|
||||||
export type CommandPingAdded = PingData[];
|
export type CommandPingAdded = PingData[];
|
||||||
export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>;
|
export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>;
|
||||||
export type CommandPingBlocked = {
|
|
||||||
reason: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface UserSettings {
|
export interface UserSettings {
|
||||||
primaryCharacterId?: string;
|
primaryCharacterId?: string;
|
||||||
@@ -215,10 +206,8 @@ export interface CommandData {
|
|||||||
[Commands.systemCommentRemoved]: CommandCommentRemoved;
|
[Commands.systemCommentRemoved]: CommandCommentRemoved;
|
||||||
[Commands.systemCommentsUpdated]: unknown;
|
[Commands.systemCommentsUpdated]: unknown;
|
||||||
[Commands.showTracking]: CommandShowTracking;
|
[Commands.showTracking]: CommandShowTracking;
|
||||||
[Commands.refreshTrackingData]: CommandRefreshTrackingData;
|
|
||||||
[Commands.pingAdded]: CommandPingAdded;
|
[Commands.pingAdded]: CommandPingAdded;
|
||||||
[Commands.pingCancelled]: CommandPingCancelled;
|
[Commands.pingCancelled]: CommandPingCancelled;
|
||||||
[Commands.pingBlocked]: CommandPingBlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MapHandlers {
|
export interface MapHandlers {
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
refreshZone.addEventListener('click', handleUpdate);
|
refreshZone.addEventListener('click', handleUpdate);
|
||||||
// refreshZone.addEventListener('mouseover', handleUpdate);
|
refreshZone.addEventListener('mouseover', handleUpdate);
|
||||||
|
|
||||||
this.updated();
|
this.updated();
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 34 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
@@ -63,7 +63,6 @@ config :wanderer_app, WandererAppWeb.Endpoint,
|
|||||||
]
|
]
|
||||||
|
|
||||||
config :wanderer_app,
|
config :wanderer_app,
|
||||||
environment: :dev,
|
|
||||||
dev_routes: true
|
dev_routes: true
|
||||||
|
|
||||||
# Do not include metadata nor timestamps in development logs
|
# Do not include metadata nor timestamps in development logs
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
# Set environment at compile time for modules using Application.compile_env
|
|
||||||
config :wanderer_app, environment: :prod
|
|
||||||
|
|
||||||
# Note we also include the path to a cache manifest
|
# Note we also include the path to a cache manifest
|
||||||
# containing the digested version of static files. This
|
# containing the digested version of static files. This
|
||||||
# manifest is generated by the `mix assets.deploy` task,
|
# manifest is generated by the `mix assets.deploy` task,
|
||||||
|
|||||||
@@ -92,31 +92,6 @@ map_subscription_extra_hubs_10_price =
|
|||||||
config_dir
|
config_dir
|
||||||
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
|
|> get_int_from_path_or_env("WANDERER_MAP_SUBSCRIPTION_EXTRA_HUBS_10_PRICE", 10_000_000)
|
||||||
|
|
||||||
# Parse promo codes from environment variable
|
|
||||||
# Format: "CODE1:10,CODE2:20" where numbers are discount percentages
|
|
||||||
promo_codes =
|
|
||||||
config_dir
|
|
||||||
|> get_var_from_path_or_env("WANDERER_PROMO_CODES", "")
|
|
||||||
|> case do
|
|
||||||
"" ->
|
|
||||||
%{}
|
|
||||||
|
|
||||||
codes_string ->
|
|
||||||
codes_string
|
|
||||||
|> String.split(",")
|
|
||||||
|> Enum.map(fn entry ->
|
|
||||||
case String.split(String.trim(entry), ":") do
|
|
||||||
[code, discount] ->
|
|
||||||
{String.upcase(String.trim(code)), String.to_integer(String.trim(discount))}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
nil
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
|> Enum.reject(&is_nil/1)
|
|
||||||
|> Map.new()
|
|
||||||
end
|
|
||||||
|
|
||||||
map_connection_auto_expire_hours =
|
map_connection_auto_expire_hours =
|
||||||
config_dir
|
config_dir
|
||||||
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_AUTO_EXPIRE_HOURS", 24)
|
|> get_int_from_path_or_env("WANDERER_MAP_CONNECTION_AUTO_EXPIRE_HOURS", 24)
|
||||||
@@ -201,8 +176,7 @@ config :wanderer_app,
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
extra_characters_50: map_subscription_extra_characters_50_price,
|
extra_characters_50: map_subscription_extra_characters_50_price,
|
||||||
extra_hubs_10: map_subscription_extra_hubs_10_price,
|
extra_hubs_10: map_subscription_extra_hubs_10_price
|
||||||
promo_codes: promo_codes
|
|
||||||
},
|
},
|
||||||
# Finch pool configuration - separate pools for different services
|
# Finch pool configuration - separate pools for different services
|
||||||
# ESI Character Tracking pool - high capacity for bulk character operations
|
# ESI Character Tracking pool - high capacity for bulk character operations
|
||||||
@@ -290,7 +264,7 @@ config :logger,
|
|||||||
case config_env() do
|
case config_env() do
|
||||||
:prod -> "info"
|
:prod -> "info"
|
||||||
:dev -> "info"
|
:dev -> "info"
|
||||||
:test -> "warning"
|
:test -> "debug"
|
||||||
end
|
end
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -458,7 +432,7 @@ config :wanderer_app, :license_manager,
|
|||||||
config :wanderer_app, :sse,
|
config :wanderer_app, :sse,
|
||||||
enabled:
|
enabled:
|
||||||
config_dir
|
config_dir
|
||||||
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "false")
|
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "true")
|
||||||
|> String.to_existing_atom(),
|
|> String.to_existing_atom(),
|
||||||
max_connections_total:
|
max_connections_total:
|
||||||
config_dir |> get_int_from_path_or_env("WANDERER_SSE_MAX_CONNECTIONS", 1000),
|
config_dir |> get_int_from_path_or_env("WANDERER_SSE_MAX_CONNECTIONS", 1000),
|
||||||
@@ -473,6 +447,6 @@ config :wanderer_app, :sse,
|
|||||||
config :wanderer_app, :external_events,
|
config :wanderer_app, :external_events,
|
||||||
webhooks_enabled:
|
webhooks_enabled:
|
||||||
config_dir
|
config_dir
|
||||||
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "false")
|
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "true")
|
||||||
|> String.to_existing_atom(),
|
|> String.to_existing_atom(),
|
||||||
webhook_timeout_ms: config_dir |> get_int_from_path_or_env("WANDERER_WEBHOOK_TIMEOUT_MS", 15000)
|
webhook_timeout_ms: config_dir |> get_int_from_path_or_env("WANDERER_WEBHOOK_TIMEOUT_MS", 15000)
|
||||||
|
|||||||
@@ -1,9 +1,5 @@
|
|||||||
import Config
|
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
|
# Configure your database
|
||||||
#
|
#
|
||||||
# The MIX_TEST_PARTITION environment variable can be used
|
# The MIX_TEST_PARTITION environment variable can be used
|
||||||
@@ -28,11 +24,7 @@ config :wanderer_app,
|
|||||||
pubsub_client: Test.PubSubMock,
|
pubsub_client: Test.PubSubMock,
|
||||||
cached_info: WandererApp.CachedInfo.Mock,
|
cached_info: WandererApp.CachedInfo.Mock,
|
||||||
character_api_disabled: false,
|
character_api_disabled: false,
|
||||||
environment: :test,
|
environment: :test
|
||||||
map_subscriptions_enabled: false,
|
|
||||||
wanderer_kills_service_enabled: false,
|
|
||||||
sse: [enabled: false],
|
|
||||||
external_events: [webhooks_enabled: false]
|
|
||||||
|
|
||||||
# We don't run a server during test. If one is required,
|
# We don't run a server during test. If one is required,
|
||||||
# you can enable the server option below.
|
# you can enable the server option below.
|
||||||
|
|||||||
@@ -16,11 +16,6 @@ defmodule WandererApp.Api.AccessList do
|
|||||||
|
|
||||||
includes([:owner, :members])
|
includes([:owner, :members])
|
||||||
|
|
||||||
default_fields([
|
|
||||||
:name,
|
|
||||||
:description
|
|
||||||
])
|
|
||||||
|
|
||||||
derive_filter?(true)
|
derive_filter?(true)
|
||||||
derive_sort?(true)
|
derive_sort?(true)
|
||||||
|
|
||||||
@@ -65,17 +60,19 @@ defmodule WandererApp.Api.AccessList do
|
|||||||
# Added :api_key to the accepted attributes
|
# Added :api_key to the accepted attributes
|
||||||
accept [:name, :description, :owner_id, :api_key]
|
accept [:name, :description, :owner_id, :api_key]
|
||||||
primary?(true)
|
primary?(true)
|
||||||
|
|
||||||
|
argument :owner_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
|
change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update do
|
update :update do
|
||||||
accept [:name, :description, :owner_id, :api_key]
|
accept [:name, :description, :owner_id, :api_key]
|
||||||
primary?(true)
|
primary?(true)
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :assign_owner do
|
update :assign_owner do
|
||||||
accept [:owner_id]
|
accept [:owner_id]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -84,15 +81,12 @@ defmodule WandererApp.Api.AccessList do
|
|||||||
|
|
||||||
attribute :name, :string do
|
attribute :name, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :description, :string do
|
attribute :description, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Note: api_key intentionally not public for security
|
|
||||||
attribute :api_key, :string do
|
attribute :api_key, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,14 +16,6 @@ defmodule WandererApp.Api.AccessListMember do
|
|||||||
|
|
||||||
includes([:access_list])
|
includes([:access_list])
|
||||||
|
|
||||||
default_fields([
|
|
||||||
:name,
|
|
||||||
:eve_character_id,
|
|
||||||
:eve_corporation_id,
|
|
||||||
:eve_alliance_id,
|
|
||||||
:role
|
|
||||||
])
|
|
||||||
|
|
||||||
derive_filter?(true)
|
derive_filter?(true)
|
||||||
derive_sort?(true)
|
derive_sort?(true)
|
||||||
|
|
||||||
@@ -61,11 +53,7 @@ defmodule WandererApp.Api.AccessListMember do
|
|||||||
:role
|
:role
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:create, :read, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
read :read_by_access_list do
|
read :read_by_access_list do
|
||||||
argument(:access_list_id, :string, allow_nil?: false)
|
argument(:access_list_id, :string, allow_nil?: false)
|
||||||
@@ -79,14 +67,12 @@ defmodule WandererApp.Api.AccessListMember do
|
|||||||
|
|
||||||
update :block do
|
update :block do
|
||||||
accept([])
|
accept([])
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(set_attribute(:blocked, true))
|
change(set_attribute(:blocked, true))
|
||||||
end
|
end
|
||||||
|
|
||||||
update :unblock do
|
update :unblock do
|
||||||
accept([])
|
accept([])
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(set_attribute(:blocked, false))
|
change(set_attribute(:blocked, false))
|
||||||
end
|
end
|
||||||
@@ -97,27 +83,22 @@ defmodule WandererApp.Api.AccessListMember do
|
|||||||
|
|
||||||
attribute :name, :string do
|
attribute :name, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :eve_character_id, :string do
|
attribute :eve_character_id, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :eve_corporation_id, :string do
|
attribute :eve_corporation_id, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :eve_alliance_id, :string do
|
attribute :eve_alliance_id, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :role, :atom do
|
attribute :role, :atom do
|
||||||
default "viewer"
|
default "viewer"
|
||||||
public? true
|
|
||||||
|
|
||||||
constraints(
|
constraints(
|
||||||
one_of: [
|
one_of: [
|
||||||
|
|||||||
@@ -1,80 +0,0 @@
|
|||||||
defmodule WandererApp.Api.ActorHelpers do
|
|
||||||
@moduledoc """
|
|
||||||
Utilities for extracting actor information from Ash contexts.
|
|
||||||
|
|
||||||
Provides helper functions for working with ActorWithMap and extracting
|
|
||||||
user, map, and character information from various context formats.
|
|
||||||
"""
|
|
||||||
|
|
||||||
alias WandererApp.Api.ActorWithMap
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Extract map from actor or context.
|
|
||||||
|
|
||||||
Handles various context formats:
|
|
||||||
- Direct ActorWithMap struct
|
|
||||||
- Context map with :actor key
|
|
||||||
- Context map with :map key
|
|
||||||
- Ash.Resource.Change.Context struct
|
|
||||||
"""
|
|
||||||
def get_map(%{actor: %ActorWithMap{map: %{} = map}}), do: map
|
|
||||||
def get_map(%{map: %{} = map}), do: map
|
|
||||||
|
|
||||||
# Handle Ash.Resource.Change.Context struct
|
|
||||||
def get_map(%Ash.Resource.Change.Context{actor: %ActorWithMap{map: %{} = map}}), do: map
|
|
||||||
def get_map(%Ash.Resource.Change.Context{actor: _}), do: nil
|
|
||||||
|
|
||||||
def get_map(context) when is_map(context) do
|
|
||||||
# For plain maps, check private.actor
|
|
||||||
with private when is_map(private) <- Map.get(context, :private),
|
|
||||||
%ActorWithMap{map: %{} = map} <- Map.get(private, :actor) do
|
|
||||||
map
|
|
||||||
else
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_map(_), do: nil
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Extract user from actor.
|
|
||||||
|
|
||||||
Handles:
|
|
||||||
- ActorWithMap struct
|
|
||||||
- Direct user struct with :id field
|
|
||||||
"""
|
|
||||||
def get_user(%ActorWithMap{user: user}), do: user
|
|
||||||
def get_user(%{id: _} = user), do: user
|
|
||||||
def get_user(_), do: nil
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Get character IDs for the actor.
|
|
||||||
|
|
||||||
Used for ACL filtering to determine which resources the user can access.
|
|
||||||
Returns {:ok, list} or {:ok, []} if no characters found.
|
|
||||||
"""
|
|
||||||
def get_character_ids(%ActorWithMap{user: user}), do: get_character_ids(user)
|
|
||||||
|
|
||||||
def get_character_ids(%{characters: characters}) when is_list(characters) do
|
|
||||||
{:ok, Enum.map(characters, & &1.id)}
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_character_ids(%{characters: %Ecto.Association.NotLoaded{}, id: user_id}) do
|
|
||||||
# Load characters from database
|
|
||||||
load_characters_by_id(user_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_character_ids(%{id: user_id}) do
|
|
||||||
# Fallback: load user with characters
|
|
||||||
load_characters_by_id(user_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
def get_character_ids(_), do: {:ok, []}
|
|
||||||
|
|
||||||
defp load_characters_by_id(user_id) do
|
|
||||||
case WandererApp.Api.User.by_id(user_id, load: [:characters]) do
|
|
||||||
{:ok, user} -> {:ok, Enum.map(user.characters, & &1.id)}
|
|
||||||
_ -> {:ok, []}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
defmodule WandererApp.Api.ActorWithMap do
|
|
||||||
@moduledoc """
|
|
||||||
Wraps a user and map together as an actor for token-based authentication.
|
|
||||||
|
|
||||||
When API requests use Bearer token auth, the token identifies both the user
|
|
||||||
(map owner) and the map. This struct allows passing both through Ash's actor system.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@enforce_keys [:user, :map]
|
|
||||||
defstruct [:user, :map]
|
|
||||||
|
|
||||||
def new(user, map) do
|
|
||||||
%__MODULE__{user: user, map: map}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
defmodule WandererApp.Api.Changes.InjectMapFromActor do
|
|
||||||
@moduledoc """
|
|
||||||
Ash change that injects map_id from the authenticated actor.
|
|
||||||
|
|
||||||
For token-based auth, the map is determined by the API token.
|
|
||||||
This change automatically sets map_id, so clients don't need to provide it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Ash.Resource.Change
|
|
||||||
|
|
||||||
alias WandererApp.Api.ActorHelpers
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def change(changeset, _opts, context) do
|
|
||||||
case ActorHelpers.get_map(context) do
|
|
||||||
%{id: map_id} ->
|
|
||||||
Ash.Changeset.force_change_attribute(changeset, :map_id, map_id)
|
|
||||||
|
|
||||||
_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)
|
|
||||||
|
|
||||||
case map_id do
|
|
||||||
nil ->
|
|
||||||
Ash.Changeset.add_error(changeset,
|
|
||||||
field: :map_id,
|
|
||||||
message: "map_id is required (provide via token or attribute)"
|
|
||||||
)
|
|
||||||
|
|
||||||
_map_id ->
|
|
||||||
# map_id provided directly (internal calls, tests)
|
|
||||||
changeset
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -69,6 +69,11 @@ defmodule WandererApp.Api.Character do
|
|||||||
filter(expr(user_id == ^arg(:user_id) and deleted == false))
|
filter(expr(user_id == ^arg(:user_id) and deleted == false))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
read :available_by_map do
|
||||||
|
argument(:map_id, :uuid, allow_nil?: false)
|
||||||
|
filter(expr(user_id == ^arg(:user_id) and deleted == false))
|
||||||
|
end
|
||||||
|
|
||||||
read :last_active do
|
read :last_active do
|
||||||
argument(:from, :utc_datetime, allow_nil?: false)
|
argument(:from, :utc_datetime, allow_nil?: false)
|
||||||
|
|
||||||
@@ -95,7 +100,6 @@ defmodule WandererApp.Api.Character do
|
|||||||
|
|
||||||
update :mark_as_deleted do
|
update :mark_as_deleted do
|
||||||
accept([])
|
accept([])
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(atomic_update(:deleted, true))
|
change(atomic_update(:deleted, true))
|
||||||
change(atomic_update(:user_id, nil))
|
change(atomic_update(:user_id, nil))
|
||||||
@@ -103,7 +107,6 @@ defmodule WandererApp.Api.Character do
|
|||||||
|
|
||||||
update :update_online do
|
update :update_online do
|
||||||
accept([:online])
|
accept([:online])
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_location do
|
update :update_location do
|
||||||
|
|||||||
@@ -33,11 +33,7 @@ defmodule WandererApp.Api.CorpWalletTransaction do
|
|||||||
:ref_type
|
:ref_type
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:create, :read, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
create :new do
|
create :new do
|
||||||
accept [
|
accept [
|
||||||
|
|||||||
@@ -36,11 +36,7 @@ defmodule WandererApp.Api.License do
|
|||||||
:expire_at
|
:expire_at
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:read, :destroy]
|
defaults [:read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
create :create do
|
create :create do
|
||||||
primary? true
|
primary? true
|
||||||
@@ -62,14 +58,12 @@ defmodule WandererApp.Api.License do
|
|||||||
|
|
||||||
update :invalidate do
|
update :invalidate do
|
||||||
accept([])
|
accept([])
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(set_attribute(:is_valid, false))
|
change(set_attribute(:is_valid, false))
|
||||||
end
|
end
|
||||||
|
|
||||||
update :set_valid do
|
update :set_valid do
|
||||||
accept([])
|
accept([])
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(set_attribute(:is_valid, true))
|
change(set_attribute(:is_valid, true))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,13 +8,9 @@ defmodule WandererApp.Api.Map do
|
|||||||
|
|
||||||
alias Ash.Resource.Change.Builtins
|
alias Ash.Resource.Change.Builtins
|
||||||
|
|
||||||
require Logger
|
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
repo(WandererApp.Repo)
|
repo(WandererApp.Repo)
|
||||||
table("maps_v1")
|
table("maps_v1")
|
||||||
|
|
||||||
migration_defaults scopes: "'{wormholes}'"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
json_api do
|
json_api do
|
||||||
@@ -48,7 +44,6 @@ defmodule WandererApp.Api.Map do
|
|||||||
code_interface do
|
code_interface do
|
||||||
define(:available, action: :available)
|
define(:available, action: :available)
|
||||||
define(:get_map_by_slug, action: :by_slug, args: [:slug])
|
define(:get_map_by_slug, action: :by_slug, args: [:slug])
|
||||||
define(:by_api_key, action: :by_api_key, args: [:api_key])
|
|
||||||
define(:new, action: :new)
|
define(:new, action: :new)
|
||||||
define(:create, action: :create)
|
define(:create, action: :create)
|
||||||
define(:update, action: :update)
|
define(:update, action: :update)
|
||||||
@@ -59,7 +54,6 @@ defmodule WandererApp.Api.Map do
|
|||||||
define(:mark_as_deleted, action: :mark_as_deleted)
|
define(:mark_as_deleted, action: :mark_as_deleted)
|
||||||
define(:update_api_key, action: :update_api_key)
|
define(:update_api_key, action: :update_api_key)
|
||||||
define(:toggle_webhooks, action: :toggle_webhooks)
|
define(:toggle_webhooks, action: :toggle_webhooks)
|
||||||
define(:toggle_sse, action: :toggle_sse)
|
|
||||||
|
|
||||||
define(:by_id,
|
define(:by_id,
|
||||||
get_by: [:id],
|
get_by: [:id],
|
||||||
@@ -67,8 +61,6 @@ defmodule WandererApp.Api.Map do
|
|||||||
)
|
)
|
||||||
|
|
||||||
define(:duplicate, action: :duplicate)
|
define(:duplicate, action: :duplicate)
|
||||||
define(:admin_all, action: :admin_all)
|
|
||||||
define(:restore, action: :restore)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
calculations do
|
calculations do
|
||||||
@@ -98,41 +90,22 @@ defmodule WandererApp.Api.Map do
|
|||||||
filter expr(slug == ^arg(:slug))
|
filter expr(slug == ^arg(:slug))
|
||||||
end
|
end
|
||||||
|
|
||||||
read :by_api_key do
|
|
||||||
get? true
|
|
||||||
argument :api_key, :string, allow_nil?: false
|
|
||||||
|
|
||||||
prepare WandererApp.Api.Preparations.SecureApiKeyLookup
|
|
||||||
end
|
|
||||||
|
|
||||||
read :available do
|
read :available do
|
||||||
prepare WandererApp.Api.Preparations.FilterMapsByRoles
|
prepare WandererApp.Api.Preparations.FilterMapsByRoles
|
||||||
end
|
end
|
||||||
|
|
||||||
read :admin_all do
|
|
||||||
# Admin-only action that bypasses FilterMapsByRoles
|
|
||||||
# Returns ALL maps including soft-deleted ones with owner and ACLs loaded
|
|
||||||
prepare build(load: [:owner, :acls])
|
|
||||||
end
|
|
||||||
|
|
||||||
create :new do
|
create :new do
|
||||||
accept [
|
accept [:name, :slug, :description, :scope, :only_tracked_characters, :owner_id]
|
||||||
:name,
|
|
||||||
:slug,
|
|
||||||
:description,
|
|
||||||
:scope,
|
|
||||||
:scopes,
|
|
||||||
:only_tracked_characters,
|
|
||||||
:owner_id,
|
|
||||||
:sse_enabled
|
|
||||||
]
|
|
||||||
|
|
||||||
primary?(true)
|
primary?(true)
|
||||||
|
|
||||||
|
argument :owner_id, :uuid, allow_nil?: false
|
||||||
argument :create_default_acl, :boolean, allow_nil?: true
|
argument :create_default_acl, :boolean, allow_nil?: true
|
||||||
argument :acls, {:array, :uuid}, allow_nil?: true
|
argument :acls, {:array, :uuid}, allow_nil?: true
|
||||||
argument :acls_text_input, :string, allow_nil?: true
|
argument :acls_text_input, :string, allow_nil?: true
|
||||||
argument :scope_text_input, :string, allow_nil?: true
|
argument :scope_text_input, :string, allow_nil?: true
|
||||||
argument :acls_empty_selection, :string, allow_nil?: true
|
argument :acls_empty_selection, :string, allow_nil?: true
|
||||||
|
|
||||||
|
change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
|
||||||
change manage_relationship(:acls, type: :append_and_remove)
|
change manage_relationship(:acls, type: :append_and_remove)
|
||||||
change WandererApp.Api.Changes.SlugifyName
|
change WandererApp.Api.Changes.SlugifyName
|
||||||
end
|
end
|
||||||
@@ -140,17 +113,7 @@ defmodule WandererApp.Api.Map do
|
|||||||
update :update do
|
update :update do
|
||||||
primary? true
|
primary? true
|
||||||
require_atomic? false
|
require_atomic? false
|
||||||
|
accept [:name, :slug, :description, :scope, :only_tracked_characters, :owner_id]
|
||||||
accept [
|
|
||||||
:name,
|
|
||||||
:slug,
|
|
||||||
:description,
|
|
||||||
:scope,
|
|
||||||
:scopes,
|
|
||||||
:only_tracked_characters,
|
|
||||||
:owner_id,
|
|
||||||
:sse_enabled
|
|
||||||
]
|
|
||||||
|
|
||||||
argument :owner_id_text_input, :string, allow_nil?: true
|
argument :owner_id_text_input, :string, allow_nil?: true
|
||||||
argument :acls_text_input, :string, allow_nil?: true
|
argument :acls_text_input, :string, allow_nil?: true
|
||||||
@@ -165,9 +128,6 @@ defmodule WandererApp.Api.Map do
|
|||||||
)
|
)
|
||||||
|
|
||||||
change WandererApp.Api.Changes.SlugifyName
|
change WandererApp.Api.Changes.SlugifyName
|
||||||
|
|
||||||
# Validate subscription when enabling SSE
|
|
||||||
validate &validate_sse_subscription/2
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_acls do
|
update :update_acls do
|
||||||
@@ -182,64 +142,33 @@ defmodule WandererApp.Api.Map do
|
|||||||
|
|
||||||
update :assign_owner do
|
update :assign_owner do
|
||||||
accept [:owner_id]
|
accept [:owner_id]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_hubs do
|
update :update_hubs do
|
||||||
accept [:hubs]
|
accept [:hubs]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_options do
|
update :update_options do
|
||||||
accept [:options]
|
accept [:options]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :mark_as_deleted do
|
update :mark_as_deleted do
|
||||||
accept([])
|
accept([])
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(set_attribute(:deleted, true))
|
change(set_attribute(:deleted, true))
|
||||||
end
|
end
|
||||||
|
|
||||||
update :restore do
|
|
||||||
# Admin-only action to restore a soft-deleted map
|
|
||||||
accept([])
|
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(set_attribute(:deleted, false))
|
|
||||||
end
|
|
||||||
|
|
||||||
update :update_api_key do
|
update :update_api_key do
|
||||||
accept [:public_api_key]
|
accept [:public_api_key]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :toggle_webhooks do
|
update :toggle_webhooks do
|
||||||
accept [:webhooks_enabled]
|
accept [:webhooks_enabled]
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change after_action(fn _changeset, record, _context ->
|
|
||||||
WandererApp.Map.update_webhooks_enabled(record.id, record.webhooks_enabled)
|
|
||||||
{:ok, record}
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
update :toggle_sse do
|
|
||||||
require_atomic? false
|
|
||||||
accept [:sse_enabled]
|
|
||||||
|
|
||||||
# Validate subscription when enabling SSE
|
|
||||||
validate &validate_sse_subscription/2
|
|
||||||
|
|
||||||
change after_action(fn _changeset, record, _context ->
|
|
||||||
WandererApp.Map.update_sse_enabled(record.id, record.sse_enabled)
|
|
||||||
{:ok, record}
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create :duplicate do
|
create :duplicate do
|
||||||
accept [:name, :description, :scope, :scopes, :only_tracked_characters]
|
accept [:name, :description, :scope, :only_tracked_characters]
|
||||||
|
|
||||||
argument :source_map_id, :uuid, allow_nil?: false
|
argument :source_map_id, :uuid, allow_nil?: false
|
||||||
argument :copy_acls, :boolean, default: true
|
argument :copy_acls, :boolean, default: true
|
||||||
argument :copy_user_settings, :boolean, default: true
|
argument :copy_user_settings, :boolean, default: true
|
||||||
@@ -255,14 +184,9 @@ defmodule WandererApp.Api.Map do
|
|||||||
description =
|
description =
|
||||||
Ash.Changeset.get_attribute(changeset, :description) || source_map.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
|
changeset
|
||||||
|> Ash.Changeset.change_attribute(:description, description)
|
|> Ash.Changeset.change_attribute(:description, description)
|
||||||
|> Ash.Changeset.change_attribute(:scope, source_map.scope)
|
|> Ash.Changeset.change_attribute(:scope, source_map.scope)
|
||||||
|> Ash.Changeset.change_attribute(:scopes, scopes)
|
|
||||||
|> Ash.Changeset.change_attribute(
|
|> Ash.Changeset.change_attribute(
|
||||||
:only_tracked_characters,
|
:only_tracked_characters,
|
||||||
source_map.only_tracked_characters
|
source_map.only_tracked_characters
|
||||||
@@ -388,37 +312,12 @@ defmodule WandererApp.Api.Map do
|
|||||||
public?(true)
|
public?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :sse_enabled, :boolean do
|
|
||||||
default(false)
|
|
||||||
allow_nil?(false)
|
|
||||||
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)
|
create_timestamp(:inserted_at)
|
||||||
update_timestamp(:updated_at)
|
update_timestamp(:updated_at)
|
||||||
end
|
end
|
||||||
|
|
||||||
identities do
|
identities do
|
||||||
identity :unique_slug, [:slug]
|
identity :unique_slug, [:slug]
|
||||||
identity :unique_public_api_key, [:public_api_key]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
@@ -445,49 +344,4 @@ defmodule WandererApp.Api.Map do
|
|||||||
public? false
|
public? false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# SSE Subscription Validation
|
|
||||||
#
|
|
||||||
# This validation ensures that SSE can only be enabled when:
|
|
||||||
# 1. SSE is being disabled (always allowed)
|
|
||||||
# 2. Map is being created (skip validation, will be checked on first update)
|
|
||||||
# 3. Community Edition mode (always allowed)
|
|
||||||
# 4. Enterprise mode with active subscription
|
|
||||||
defp validate_sse_subscription(changeset, _context) do
|
|
||||||
sse_enabled = Ash.Changeset.get_attribute(changeset, :sse_enabled)
|
|
||||||
map_id = changeset.data.id
|
|
||||||
subscriptions_enabled = WandererApp.Env.map_subscriptions_enabled?()
|
|
||||||
|
|
||||||
cond do
|
|
||||||
# Not enabling SSE - no validation needed
|
|
||||||
not sse_enabled ->
|
|
||||||
:ok
|
|
||||||
|
|
||||||
# Map creation (no ID yet) - skip validation
|
|
||||||
is_nil(map_id) ->
|
|
||||||
:ok
|
|
||||||
|
|
||||||
# Community Edition mode - always allow
|
|
||||||
not subscriptions_enabled ->
|
|
||||||
:ok
|
|
||||||
|
|
||||||
# Enterprise mode - check subscription
|
|
||||||
true ->
|
|
||||||
validate_active_subscription(map_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp validate_active_subscription(map_id) do
|
|
||||||
case WandererApp.Map.is_subscription_active?(map_id) do
|
|
||||||
{:ok, true} ->
|
|
||||||
:ok
|
|
||||||
|
|
||||||
{:ok, false} ->
|
|
||||||
{:error, field: :sse_enabled, message: "Active subscription required to enable SSE"}
|
|
||||||
|
|
||||||
{:error, reason} ->
|
|
||||||
Logger.error("Error checking subscription status: #{inspect(reason)}")
|
|
||||||
{:error, field: :sse_enabled, message: "Unable to verify subscription status"}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -61,11 +61,7 @@ defmodule WandererApp.Api.MapAccessList do
|
|||||||
:access_list_id
|
:access_list_id
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:create, :read, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
read :read_by_map do
|
read :read_by_map do
|
||||||
argument(:map_id, :string, allow_nil?: false)
|
argument(:map_id, :string, allow_nil?: false)
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ defmodule WandererApp.Api.MapChainPassages do
|
|||||||
:solar_system_target_id
|
:solar_system_target_id
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:create, :read, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
create :new do
|
create :new do
|
||||||
accept [
|
accept [
|
||||||
@@ -44,6 +40,12 @@ defmodule WandererApp.Api.MapChainPassages do
|
|||||||
]
|
]
|
||||||
|
|
||||||
primary?(true)
|
primary?(true)
|
||||||
|
|
||||||
|
argument :map_id, :uuid, allow_nil?: false
|
||||||
|
argument :character_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
|
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
|
||||||
|
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
action :by_map_id, {:array, :struct} do
|
action :by_map_id, {:array, :struct} do
|
||||||
|
|||||||
@@ -27,11 +27,6 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
|||||||
|
|
||||||
includes([:map, :character])
|
includes([:map, :character])
|
||||||
|
|
||||||
default_fields([
|
|
||||||
:tracked,
|
|
||||||
:followed
|
|
||||||
])
|
|
||||||
|
|
||||||
derive_filter?(true)
|
derive_filter?(true)
|
||||||
derive_sort?(true)
|
derive_sort?(true)
|
||||||
|
|
||||||
@@ -86,6 +81,12 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
|||||||
:character_id,
|
:character_id,
|
||||||
:tracked
|
:tracked
|
||||||
]
|
]
|
||||||
|
|
||||||
|
argument :map_id, :uuid, allow_nil?: false
|
||||||
|
argument :character_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
|
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
|
||||||
|
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
read :by_map_filtered do
|
read :by_map_filtered do
|
||||||
@@ -133,8 +134,6 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
|||||||
require_atomic? false
|
require_atomic? false
|
||||||
|
|
||||||
accept([
|
accept([
|
||||||
:tracked,
|
|
||||||
:followed,
|
|
||||||
:ship,
|
:ship,
|
||||||
:ship_name,
|
:ship_name,
|
||||||
:ship_item_id,
|
:ship_item_id,
|
||||||
@@ -146,7 +145,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
|||||||
|
|
||||||
update :track do
|
update :track do
|
||||||
accept [:map_id, :character_id]
|
accept [:map_id, :character_id]
|
||||||
require_atomic? false
|
argument :map_id, :string, allow_nil?: false
|
||||||
|
argument :character_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
# Load the record first
|
# Load the record first
|
||||||
load do
|
load do
|
||||||
@@ -159,7 +159,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
|||||||
|
|
||||||
update :untrack do
|
update :untrack do
|
||||||
accept [:map_id, :character_id]
|
accept [:map_id, :character_id]
|
||||||
require_atomic? false
|
argument :map_id, :string, allow_nil?: false
|
||||||
|
argument :character_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
# Load the record first
|
# Load the record first
|
||||||
load do
|
load do
|
||||||
@@ -172,7 +173,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
|||||||
|
|
||||||
update :follow do
|
update :follow do
|
||||||
accept [:map_id, :character_id]
|
accept [:map_id, :character_id]
|
||||||
require_atomic? false
|
argument :map_id, :string, allow_nil?: false
|
||||||
|
argument :character_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
# Load the record first
|
# Load the record first
|
||||||
load do
|
load do
|
||||||
@@ -185,7 +187,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
|||||||
|
|
||||||
update :unfollow do
|
update :unfollow do
|
||||||
accept [:map_id, :character_id]
|
accept [:map_id, :character_id]
|
||||||
require_atomic? false
|
argument :map_id, :string, allow_nil?: false
|
||||||
|
argument :character_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
# Load the record first
|
# Load the record first
|
||||||
load do
|
load do
|
||||||
@@ -224,17 +227,14 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
|||||||
|
|
||||||
attribute :tracked, :boolean do
|
attribute :tracked, :boolean do
|
||||||
default false
|
default false
|
||||||
public? true
|
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :followed, :boolean do
|
attribute :followed, :boolean do
|
||||||
default false
|
default false
|
||||||
public? true
|
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|
||||||
# Note: These attributes are encrypted (AshCloak) and intentionally not public
|
|
||||||
attribute :solar_system_id, :integer
|
attribute :solar_system_id, :integer
|
||||||
attribute :structure_id, :integer
|
attribute :structure_id, :integer
|
||||||
attribute :station_id, :integer
|
attribute :station_id, :integer
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ defmodule WandererApp.Api.MapConnection do
|
|||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: WandererApp.Api,
|
domain: WandererApp.Api,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshJsonApi.Resource],
|
extensions: [AshJsonApi.Resource]
|
||||||
primary_read_warning?: false
|
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
repo(WandererApp.Repo)
|
repo(WandererApp.Repo)
|
||||||
@@ -22,19 +21,6 @@ defmodule WandererApp.Api.MapConnection do
|
|||||||
|
|
||||||
includes([:map])
|
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_filter?(true)
|
||||||
derive_sort?(true)
|
derive_sort?(true)
|
||||||
|
|
||||||
@@ -87,56 +73,7 @@ defmodule WandererApp.Api.MapConnection do
|
|||||||
:custom_info
|
:custom_info
|
||||||
]
|
]
|
||||||
|
|
||||||
create :create do
|
defaults [:create, :read, :update, :destroy]
|
||||||
primary? true
|
|
||||||
|
|
||||||
accept [
|
|
||||||
:map_id,
|
|
||||||
:solar_system_source,
|
|
||||||
:solar_system_target,
|
|
||||||
:type,
|
|
||||||
:ship_size_type,
|
|
||||||
:mass_status,
|
|
||||||
:time_status,
|
|
||||||
:wormhole_type,
|
|
||||||
:count_of_passage,
|
|
||||||
:locked,
|
|
||||||
:custom_info
|
|
||||||
]
|
|
||||||
|
|
||||||
# Inject map_id from token
|
|
||||||
change WandererApp.Api.Changes.InjectMapFromActor
|
|
||||||
end
|
|
||||||
|
|
||||||
read :read do
|
|
||||||
primary? true
|
|
||||||
|
|
||||||
# Security: Filter to only connections from actor's map
|
|
||||||
prepare WandererApp.Api.Preparations.FilterConnectionsByActorMap
|
|
||||||
end
|
|
||||||
|
|
||||||
update :update do
|
|
||||||
primary? true
|
|
||||||
|
|
||||||
accept [
|
|
||||||
:solar_system_source,
|
|
||||||
:solar_system_target,
|
|
||||||
:type,
|
|
||||||
:ship_size_type,
|
|
||||||
:mass_status,
|
|
||||||
:time_status,
|
|
||||||
:wormhole_type,
|
|
||||||
:count_of_passage,
|
|
||||||
:locked,
|
|
||||||
:custom_info
|
|
||||||
]
|
|
||||||
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
destroy :destroy do
|
|
||||||
primary? true
|
|
||||||
end
|
|
||||||
|
|
||||||
read :read_by_map do
|
read :read_by_map do
|
||||||
argument(:map_id, :string, allow_nil?: false)
|
argument(:map_id, :string, allow_nil?: false)
|
||||||
@@ -173,57 +110,45 @@ defmodule WandererApp.Api.MapConnection do
|
|||||||
|
|
||||||
update :update_mass_status do
|
update :update_mass_status do
|
||||||
accept [:mass_status]
|
accept [:mass_status]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_time_status do
|
update :update_time_status do
|
||||||
accept [:time_status]
|
accept [:time_status]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_ship_size_type do
|
update :update_ship_size_type do
|
||||||
accept [:ship_size_type]
|
accept [:ship_size_type]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_locked do
|
update :update_locked do
|
||||||
accept [:locked]
|
accept [:locked]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_custom_info do
|
update :update_custom_info do
|
||||||
accept [:custom_info]
|
accept [:custom_info]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_type do
|
update :update_type do
|
||||||
accept [:type]
|
accept [:type]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_wormhole_type do
|
update :update_wormhole_type do
|
||||||
accept [:wormhole_type]
|
accept [:wormhole_type]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
|
||||||
attribute :solar_system_source, :integer do
|
attribute :solar_system_source, :integer
|
||||||
public? true
|
attribute :solar_system_target, :integer
|
||||||
end
|
|
||||||
|
|
||||||
attribute :solar_system_target, :integer do
|
|
||||||
public? true
|
|
||||||
end
|
|
||||||
|
|
||||||
# where 0 - greater than half
|
# where 0 - greater than half
|
||||||
# where 1 - less than half
|
# where 1 - less than half
|
||||||
# where 2 - critical less than 10%
|
# where 2 - critical less than 10%
|
||||||
attribute :mass_status, :integer do
|
attribute :mass_status, :integer do
|
||||||
default(0)
|
default(0)
|
||||||
public? true
|
|
||||||
allow_nil?(true)
|
allow_nil?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -236,7 +161,7 @@ defmodule WandererApp.Api.MapConnection do
|
|||||||
# 6 - EOL 48h
|
# 6 - EOL 48h
|
||||||
attribute :time_status, :integer do
|
attribute :time_status, :integer do
|
||||||
default(0)
|
default(0)
|
||||||
public? true
|
|
||||||
allow_nil?(true)
|
allow_nil?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -247,7 +172,7 @@ defmodule WandererApp.Api.MapConnection do
|
|||||||
# where 4 - Capital
|
# where 4 - Capital
|
||||||
attribute :ship_size_type, :integer do
|
attribute :ship_size_type, :integer do
|
||||||
default(2)
|
default(2)
|
||||||
public? true
|
|
||||||
allow_nil?(true)
|
allow_nil?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -256,26 +181,21 @@ defmodule WandererApp.Api.MapConnection do
|
|||||||
# where 2 - Bridge
|
# where 2 - Bridge
|
||||||
attribute :type, :integer do
|
attribute :type, :integer do
|
||||||
default(0)
|
default(0)
|
||||||
public? true
|
|
||||||
allow_nil?(true)
|
allow_nil?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :wormhole_type, :string do
|
attribute :wormhole_type, :string
|
||||||
public? true
|
|
||||||
end
|
|
||||||
|
|
||||||
attribute :count_of_passage, :integer do
|
attribute :count_of_passage, :integer do
|
||||||
default(0)
|
default(0)
|
||||||
public? true
|
|
||||||
allow_nil?(true)
|
allow_nil?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :locked, :boolean do
|
attribute :locked, :boolean
|
||||||
public? true
|
|
||||||
end
|
|
||||||
|
|
||||||
attribute :custom_info, :string do
|
attribute :custom_info, :string do
|
||||||
public? true
|
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -23,10 +23,6 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
|||||||
:updated_by
|
:updated_by
|
||||||
])
|
])
|
||||||
|
|
||||||
default_fields([
|
|
||||||
:settings
|
|
||||||
])
|
|
||||||
|
|
||||||
routes do
|
routes do
|
||||||
base("/map_default_settings")
|
base("/map_default_settings")
|
||||||
|
|
||||||
@@ -97,7 +93,6 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
|||||||
|
|
||||||
attribute :settings, :string do
|
attribute :settings, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
constraints min_length: 2
|
constraints min_length: 2
|
||||||
description "JSON string containing the default map settings"
|
description "JSON string containing the default map settings"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -30,11 +30,7 @@ defmodule WandererApp.Api.MapInvite do
|
|||||||
:token
|
:token
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:read, :destroy]
|
defaults [:read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
create :new do
|
create :new do
|
||||||
accept [
|
accept [
|
||||||
@@ -45,6 +41,10 @@ defmodule WandererApp.Api.MapInvite do
|
|||||||
]
|
]
|
||||||
|
|
||||||
primary?(true)
|
primary?(true)
|
||||||
|
|
||||||
|
argument :map_id, :uuid, allow_nil?: true
|
||||||
|
|
||||||
|
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
read :by_map do
|
read :by_map do
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ defmodule WandererApp.Api.MapPing do
|
|||||||
|
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: WandererApp.Api,
|
domain: WandererApp.Api,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer
|
||||||
primary_read_warning?: false
|
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
repo(WandererApp.Repo)
|
repo(WandererApp.Repo)
|
||||||
@@ -37,18 +36,7 @@ defmodule WandererApp.Api.MapPing do
|
|||||||
:message
|
:message
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:destroy]
|
defaults [:read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
read :read do
|
|
||||||
primary? true
|
|
||||||
|
|
||||||
# Security: Filter to only pings from actor's map
|
|
||||||
prepare WandererApp.Api.Preparations.FilterPingsByActorMap
|
|
||||||
end
|
|
||||||
|
|
||||||
create :new do
|
create :new do
|
||||||
accept [
|
accept [
|
||||||
@@ -60,6 +48,14 @@ defmodule WandererApp.Api.MapPing do
|
|||||||
]
|
]
|
||||||
|
|
||||||
primary?(true)
|
primary?(true)
|
||||||
|
|
||||||
|
argument :map_id, :uuid, allow_nil?: false
|
||||||
|
argument :system_id, :uuid, allow_nil?: false
|
||||||
|
argument :character_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
|
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
|
||||||
|
change manage_relationship(:system_id, :system, on_lookup: :relate, on_no_match: nil)
|
||||||
|
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
read :by_map do
|
read :by_map do
|
||||||
@@ -80,10 +76,6 @@ defmodule WandererApp.Api.MapPing do
|
|||||||
|
|
||||||
filter(expr(inserted_at <= ^arg(:inserted_before)))
|
filter(expr(inserted_at <= ^arg(:inserted_before)))
|
||||||
end
|
end
|
||||||
|
|
||||||
# Admin action for cleanup - no actor filtering
|
|
||||||
read :all_pings do
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
|
|||||||
@@ -65,11 +65,7 @@ defmodule WandererApp.Api.MapSolarSystem do
|
|||||||
:sun_type_id
|
:sun_type_id
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:read, :destroy]
|
defaults [:read, :destroy, :update]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
create :create do
|
create :create do
|
||||||
primary? true
|
primary? true
|
||||||
|
|||||||
@@ -24,11 +24,7 @@ defmodule WandererApp.Api.MapSolarSystemJumps do
|
|||||||
:to_solar_system_id
|
:to_solar_system_id
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:read, :destroy]
|
defaults [:read, :destroy, :update]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
create :create do
|
create :create do
|
||||||
primary? true
|
primary? true
|
||||||
|
|||||||
@@ -45,11 +45,7 @@ defmodule WandererApp.Api.MapState do
|
|||||||
:connections_start_time
|
:connections_start_time
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:read, :destroy]
|
defaults [:read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
create :create do
|
create :create do
|
||||||
primary? true
|
primary? true
|
||||||
|
|||||||
@@ -18,15 +18,6 @@ defmodule WandererApp.Api.MapSubscription do
|
|||||||
:map
|
:map
|
||||||
])
|
])
|
||||||
|
|
||||||
default_fields([
|
|
||||||
:plan,
|
|
||||||
:status,
|
|
||||||
:characters_limit,
|
|
||||||
:hubs_limit,
|
|
||||||
:active_till,
|
|
||||||
:auto_renew?
|
|
||||||
])
|
|
||||||
|
|
||||||
# Enable automatic filtering and sorting
|
# Enable automatic filtering and sorting
|
||||||
derive_filter?(true)
|
derive_filter?(true)
|
||||||
derive_sort?(true)
|
derive_sort?(true)
|
||||||
@@ -71,11 +62,7 @@ defmodule WandererApp.Api.MapSubscription do
|
|||||||
:auto_renew?
|
:auto_renew?
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:create, :read, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
read :all_active do
|
read :all_active do
|
||||||
prepare build(sort: [updated_at: :asc], load: [:map])
|
prepare build(sort: [updated_at: :asc], load: [:map])
|
||||||
@@ -101,39 +88,32 @@ defmodule WandererApp.Api.MapSubscription do
|
|||||||
|
|
||||||
update :update_plan do
|
update :update_plan do
|
||||||
accept [:plan]
|
accept [:plan]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_characters_limit do
|
update :update_characters_limit do
|
||||||
accept [:characters_limit]
|
accept [:characters_limit]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_hubs_limit do
|
update :update_hubs_limit do
|
||||||
accept [:hubs_limit]
|
accept [:hubs_limit]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_active_till do
|
update :update_active_till do
|
||||||
accept [:active_till]
|
accept [:active_till]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_auto_renew do
|
update :update_auto_renew do
|
||||||
accept [:auto_renew?]
|
accept [:auto_renew?]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :cancel do
|
update :cancel do
|
||||||
accept([])
|
accept([])
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(set_attribute(:status, :cancelled))
|
change(set_attribute(:status, :cancelled))
|
||||||
end
|
end
|
||||||
|
|
||||||
update :expire do
|
update :expire do
|
||||||
accept([])
|
accept([])
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(set_attribute(:status, :expired))
|
change(set_attribute(:status, :expired))
|
||||||
end
|
end
|
||||||
@@ -144,7 +124,6 @@ defmodule WandererApp.Api.MapSubscription do
|
|||||||
|
|
||||||
attribute :plan, :atom do
|
attribute :plan, :atom do
|
||||||
default "alpha"
|
default "alpha"
|
||||||
public? true
|
|
||||||
|
|
||||||
constraints(
|
constraints(
|
||||||
one_of: [
|
one_of: [
|
||||||
@@ -160,7 +139,6 @@ defmodule WandererApp.Api.MapSubscription do
|
|||||||
|
|
||||||
attribute :status, :atom do
|
attribute :status, :atom do
|
||||||
default "active"
|
default "active"
|
||||||
public? true
|
|
||||||
|
|
||||||
constraints(
|
constraints(
|
||||||
one_of: [
|
one_of: [
|
||||||
@@ -175,24 +153,22 @@ defmodule WandererApp.Api.MapSubscription do
|
|||||||
|
|
||||||
attribute :characters_limit, :integer do
|
attribute :characters_limit, :integer do
|
||||||
default(100)
|
default(100)
|
||||||
public? true
|
|
||||||
allow_nil?(true)
|
allow_nil?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :hubs_limit, :integer do
|
attribute :hubs_limit, :integer do
|
||||||
default(10)
|
default(10)
|
||||||
public? true
|
|
||||||
allow_nil?(true)
|
allow_nil?(true)
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :active_till, :utc_datetime do
|
attribute :active_till, :utc_datetime do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :auto_renew?, :boolean do
|
attribute :auto_renew?, :boolean do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_timestamp(:inserted_at)
|
create_timestamp(:inserted_at)
|
||||||
|
|||||||
@@ -24,12 +24,16 @@ defmodule WandererApp.Api.MapSystem do
|
|||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: WandererApp.Api,
|
domain: WandererApp.Api,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshJsonApi.Resource],
|
extensions: [AshJsonApi.Resource]
|
||||||
primary_read_warning?: false
|
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
repo(WandererApp.Repo)
|
repo(WandererApp.Repo)
|
||||||
table("map_system_v1")
|
table("map_system_v1")
|
||||||
|
|
||||||
|
custom_indexes do
|
||||||
|
# Partial index for efficient visible systems query
|
||||||
|
index [:map_id], where: "visible = true", name: "map_system_v1_map_id_visible_index"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
json_api do
|
json_api do
|
||||||
@@ -66,7 +70,10 @@ defmodule WandererApp.Api.MapSystem do
|
|||||||
define(:upsert, action: :upsert)
|
define(:upsert, action: :upsert)
|
||||||
define(:destroy, action: :destroy)
|
define(:destroy, action: :destroy)
|
||||||
|
|
||||||
define :by_id, action: :get_by_id, args: [:id], get?: true
|
define(:by_id,
|
||||||
|
get_by: [:id],
|
||||||
|
action: :read
|
||||||
|
)
|
||||||
|
|
||||||
define(:by_solar_system_id,
|
define(:by_solar_system_id,
|
||||||
get_by: [:solar_system_id],
|
get_by: [:solar_system_id],
|
||||||
@@ -96,7 +103,6 @@ defmodule WandererApp.Api.MapSystem do
|
|||||||
define(:update_status, action: :update_status)
|
define(:update_status, action: :update_status)
|
||||||
define(:update_tag, action: :update_tag)
|
define(:update_tag, action: :update_tag)
|
||||||
define(:update_temporary_name, action: :update_temporary_name)
|
define(:update_temporary_name, action: :update_temporary_name)
|
||||||
define(:update_custom_name, action: :update_custom_name)
|
|
||||||
define(:update_labels, action: :update_labels)
|
define(:update_labels, action: :update_labels)
|
||||||
define(:update_linked_sig_eve_id, action: :update_linked_sig_eve_id)
|
define(:update_linked_sig_eve_id, action: :update_linked_sig_eve_id)
|
||||||
define(:update_position, action: :update_position)
|
define(:update_position, action: :update_position)
|
||||||
@@ -122,56 +128,7 @@ defmodule WandererApp.Api.MapSystem do
|
|||||||
:linked_sig_eve_id
|
:linked_sig_eve_id
|
||||||
]
|
]
|
||||||
|
|
||||||
create :create do
|
defaults [:create, :update, :destroy]
|
||||||
primary? true
|
|
||||||
|
|
||||||
accept [
|
|
||||||
:map_id,
|
|
||||||
:name,
|
|
||||||
:solar_system_id,
|
|
||||||
:position_x,
|
|
||||||
:position_y,
|
|
||||||
:status,
|
|
||||||
:visible,
|
|
||||||
:locked,
|
|
||||||
:custom_name,
|
|
||||||
:description,
|
|
||||||
:tag,
|
|
||||||
:temporary_name,
|
|
||||||
:labels,
|
|
||||||
:added_at,
|
|
||||||
:linked_sig_eve_id
|
|
||||||
]
|
|
||||||
|
|
||||||
# Inject map_id from token
|
|
||||||
change WandererApp.Api.Changes.InjectMapFromActor
|
|
||||||
end
|
|
||||||
|
|
||||||
update :update do
|
|
||||||
primary? true
|
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
# Note: name and solar_system_id are not in accept
|
|
||||||
# - solar_system_id should be immutable (identifier)
|
|
||||||
# - name has allow_nil? false which makes it required in JSON:API
|
|
||||||
accept [
|
|
||||||
:position_x,
|
|
||||||
:position_y,
|
|
||||||
:status,
|
|
||||||
:visible,
|
|
||||||
:locked,
|
|
||||||
:custom_name,
|
|
||||||
:description,
|
|
||||||
:tag,
|
|
||||||
:temporary_name,
|
|
||||||
:labels,
|
|
||||||
:linked_sig_eve_id
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
destroy :destroy do
|
|
||||||
primary? true
|
|
||||||
end
|
|
||||||
|
|
||||||
create :upsert do
|
create :upsert do
|
||||||
primary? false
|
primary? false
|
||||||
@@ -201,9 +158,6 @@ defmodule WandererApp.Api.MapSystem do
|
|||||||
read :read do
|
read :read do
|
||||||
primary?(true)
|
primary?(true)
|
||||||
|
|
||||||
# Security: Filter to only systems from actor's map
|
|
||||||
prepare WandererApp.Api.Preparations.FilterSystemsByActorMap
|
|
||||||
|
|
||||||
pagination offset?: true,
|
pagination offset?: true,
|
||||||
default_limit: 100,
|
default_limit: 100,
|
||||||
max_page_size: 500,
|
max_page_size: 500,
|
||||||
@@ -211,11 +165,6 @@ defmodule WandererApp.Api.MapSystem do
|
|||||||
required?: false
|
required?: false
|
||||||
end
|
end
|
||||||
|
|
||||||
read :get_by_id do
|
|
||||||
argument(:id, :string, allow_nil?: false)
|
|
||||||
filter(expr(id == ^arg(:id)))
|
|
||||||
end
|
|
||||||
|
|
||||||
read :read_all_by_map do
|
read :read_all_by_map do
|
||||||
argument(:map_id, :string, allow_nil?: false)
|
argument(:map_id, :string, allow_nil?: false)
|
||||||
filter(expr(map_id == ^arg(:map_id)))
|
filter(expr(map_id == ^arg(:map_id)))
|
||||||
@@ -237,59 +186,44 @@ defmodule WandererApp.Api.MapSystem do
|
|||||||
|
|
||||||
update :update_name do
|
update :update_name do
|
||||||
accept [:name]
|
accept [:name]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_description do
|
update :update_description do
|
||||||
accept [:description]
|
accept [:description]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_locked do
|
update :update_locked do
|
||||||
accept [:locked]
|
accept [:locked]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_status do
|
update :update_status do
|
||||||
accept [:status]
|
accept [:status]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_tag do
|
update :update_tag do
|
||||||
accept [:tag]
|
accept [:tag]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_temporary_name do
|
update :update_temporary_name do
|
||||||
accept [:temporary_name]
|
accept [:temporary_name]
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
update :update_custom_name do
|
|
||||||
accept [:custom_name]
|
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_labels do
|
update :update_labels do
|
||||||
accept [:labels]
|
accept [:labels]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_position do
|
update :update_position do
|
||||||
accept [:position_x, :position_y]
|
accept [:position_x, :position_y]
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change(set_attribute(:visible, true))
|
change(set_attribute(:visible, true))
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_linked_sig_eve_id do
|
update :update_linked_sig_eve_id do
|
||||||
accept [:linked_sig_eve_id]
|
accept [:linked_sig_eve_id]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_visible do
|
update :update_visible do
|
||||||
accept [:visible]
|
accept [:visible]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -19,10 +19,6 @@ defmodule WandererApp.Api.MapSystemComment do
|
|||||||
:character
|
:character
|
||||||
])
|
])
|
||||||
|
|
||||||
default_fields([
|
|
||||||
:text
|
|
||||||
])
|
|
||||||
|
|
||||||
routes do
|
routes do
|
||||||
base("/map_system_comments")
|
base("/map_system_comments")
|
||||||
|
|
||||||
@@ -63,6 +59,12 @@ defmodule WandererApp.Api.MapSystemComment do
|
|||||||
:character_id,
|
:character_id,
|
||||||
:text
|
:text
|
||||||
]
|
]
|
||||||
|
|
||||||
|
argument :system_id, :uuid, allow_nil?: false
|
||||||
|
argument :character_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
|
change manage_relationship(:system_id, :system, on_lookup: :relate, on_no_match: nil)
|
||||||
|
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
read :by_system_id do
|
read :by_system_id do
|
||||||
@@ -77,7 +79,6 @@ defmodule WandererApp.Api.MapSystemComment do
|
|||||||
|
|
||||||
attribute :text, :string do
|
attribute :text, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_timestamp(:inserted_at)
|
create_timestamp(:inserted_at)
|
||||||
|
|||||||
@@ -16,20 +16,6 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
|
|
||||||
includes([:system])
|
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_filter?(true)
|
||||||
derive_sort?(true)
|
derive_sort?(true)
|
||||||
|
|
||||||
@@ -123,9 +109,12 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
:group,
|
:group,
|
||||||
:type,
|
:type,
|
||||||
:custom_info,
|
:custom_info,
|
||||||
:deleted,
|
:deleted
|
||||||
:linked_system_id
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
argument :system_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
|
change manage_relationship(:system_id, :system, on_lookup: :relate, on_no_match: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update do
|
update :update do
|
||||||
@@ -141,8 +130,7 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
:type,
|
:type,
|
||||||
:custom_info,
|
:custom_info,
|
||||||
:deleted,
|
:deleted,
|
||||||
:update_forced_at,
|
:update_forced_at
|
||||||
:linked_system_id
|
|
||||||
]
|
]
|
||||||
|
|
||||||
primary? true
|
primary? true
|
||||||
@@ -151,17 +139,14 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
|
|
||||||
update :update_linked_system do
|
update :update_linked_system do
|
||||||
accept [:linked_system_id]
|
accept [:linked_system_id]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_type do
|
update :update_type do
|
||||||
accept [:type]
|
accept [:type]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_group do
|
update :update_group do
|
||||||
accept [:group]
|
accept [:group]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
read :by_system_id do
|
read :by_system_id do
|
||||||
@@ -200,56 +185,42 @@ defmodule WandererApp.Api.MapSystemSignature do
|
|||||||
|
|
||||||
attribute :eve_id, :string do
|
attribute :eve_id, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :character_eve_id, :string do
|
attribute :character_eve_id, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :name, :string do
|
attribute :name, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :description, :string do
|
attribute :description, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :temporary_name, :string do
|
attribute :temporary_name, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :type, :string do
|
attribute :type, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :linked_system_id, :integer do
|
attribute :linked_system_id, :integer do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :kind, :string do
|
attribute :kind, :string
|
||||||
public? true
|
attribute :group, :string
|
||||||
end
|
|
||||||
|
|
||||||
attribute :group, :string do
|
|
||||||
public? true
|
|
||||||
end
|
|
||||||
|
|
||||||
attribute :custom_info, :string do
|
attribute :custom_info, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :deleted, :boolean do
|
attribute :deleted, :boolean do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
default false
|
default false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :update_forced_at, :utc_datetime do
|
attribute :update_forced_at, :utc_datetime do
|
||||||
|
|||||||
@@ -41,21 +41,6 @@ defmodule WandererApp.Api.MapSystemStructure do
|
|||||||
:system
|
: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
|
# Enable automatic filtering and sorting
|
||||||
derive_filter?(true)
|
derive_filter?(true)
|
||||||
derive_sort?(true)
|
derive_sort?(true)
|
||||||
@@ -137,6 +122,13 @@ defmodule WandererApp.Api.MapSystemStructure do
|
|||||||
:status,
|
:status,
|
||||||
:end_time
|
:end_time
|
||||||
]
|
]
|
||||||
|
|
||||||
|
argument :system_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
|
change manage_relationship(:system_id, :system,
|
||||||
|
on_lookup: :relate,
|
||||||
|
on_no_match: nil
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update do
|
update :update do
|
||||||
@@ -166,62 +158,50 @@ defmodule WandererApp.Api.MapSystemStructure do
|
|||||||
|
|
||||||
attribute :structure_type_id, :string do
|
attribute :structure_type_id, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :structure_type, :string do
|
attribute :structure_type, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :character_eve_id, :string do
|
attribute :character_eve_id, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :solar_system_name, :string do
|
attribute :solar_system_name, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :solar_system_id, :integer do
|
attribute :solar_system_id, :integer do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :name, :string do
|
attribute :name, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :notes, :string do
|
attribute :notes, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :owner_name, :string do
|
attribute :owner_name, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :owner_ticker, :string do
|
attribute :owner_ticker, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :owner_id, :string do
|
attribute :owner_id, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :status, :string do
|
attribute :status, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :end_time, :utc_datetime_usec do
|
attribute :end_time, :utc_datetime_usec do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
create_timestamp :inserted_at
|
create_timestamp :inserted_at
|
||||||
|
|||||||
@@ -29,11 +29,7 @@ defmodule WandererApp.Api.MapTransaction do
|
|||||||
:amount
|
:amount
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:create, :read, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
read :by_map do
|
read :by_map do
|
||||||
argument(:map_id, :string, allow_nil?: false)
|
argument(:map_id, :string, allow_nil?: false)
|
||||||
|
|||||||
@@ -24,13 +24,6 @@ defmodule WandererApp.Api.MapUserSettings do
|
|||||||
:user
|
:user
|
||||||
])
|
])
|
||||||
|
|
||||||
default_fields([
|
|
||||||
:settings,
|
|
||||||
:main_character_eve_id,
|
|
||||||
:following_character_eve_id,
|
|
||||||
:hubs
|
|
||||||
])
|
|
||||||
|
|
||||||
routes do
|
routes do
|
||||||
base("/map_user_settings")
|
base("/map_user_settings")
|
||||||
|
|
||||||
@@ -60,30 +53,22 @@ defmodule WandererApp.Api.MapUserSettings do
|
|||||||
:settings
|
:settings
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:create, :read, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
update :update_settings do
|
update :update_settings do
|
||||||
accept [:settings]
|
accept [:settings]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_main_character do
|
update :update_main_character do
|
||||||
accept [:main_character_eve_id]
|
accept [:main_character_eve_id]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_following_character do
|
update :update_following_character do
|
||||||
accept [:following_character_eve_id]
|
accept [:following_character_eve_id]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_hubs do
|
update :update_hubs do
|
||||||
accept [:hubs]
|
accept [:hubs]
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -92,22 +77,19 @@ defmodule WandererApp.Api.MapUserSettings do
|
|||||||
|
|
||||||
attribute :settings, :string do
|
attribute :settings, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :main_character_eve_id, :string do
|
attribute :main_character_eve_id, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :following_character_eve_id, :string do
|
attribute :following_character_eve_id, :string do
|
||||||
allow_nil? true
|
allow_nil? true
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :hubs, {:array, :string} do
|
attribute :hubs, {:array, :string} do
|
||||||
allow_nil?(true)
|
allow_nil?(true)
|
||||||
public? true
|
|
||||||
default([])
|
default([])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -45,17 +45,7 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
|||||||
:active?
|
:active?
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:read]
|
defaults [:read, :destroy]
|
||||||
|
|
||||||
# Custom destroy to invalidate cache
|
|
||||||
destroy :destroy do
|
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
change after_action(fn _changeset, record, _context ->
|
|
||||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
|
||||||
{:ok, record}
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
update :update do
|
update :update do
|
||||||
accept [
|
accept [
|
||||||
@@ -68,14 +58,6 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
|||||||
:consecutive_failures,
|
:consecutive_failures,
|
||||||
:secret
|
:secret
|
||||||
]
|
]
|
||||||
|
|
||||||
require_atomic? false
|
|
||||||
|
|
||||||
# Invalidate cache when subscription is updated
|
|
||||||
change after_action(fn _changeset, record, _context ->
|
|
||||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
|
||||||
{:ok, record}
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
read :by_map do
|
read :by_map do
|
||||||
@@ -140,12 +122,6 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
|||||||
secret = generate_webhook_secret()
|
secret = generate_webhook_secret()
|
||||||
Ash.Changeset.force_change_attribute(changeset, :secret, secret)
|
Ash.Changeset.force_change_attribute(changeset, :secret, secret)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Invalidate cache when subscription is created
|
|
||||||
change after_action(fn _changeset, record, _context ->
|
|
||||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
|
||||||
{:ok, record}
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :rotate_secret do
|
update :rotate_secret do
|
||||||
@@ -156,11 +132,6 @@ defmodule WandererApp.Api.MapWebhookSubscription do
|
|||||||
new_secret = generate_webhook_secret()
|
new_secret = generate_webhook_secret()
|
||||||
Ash.Changeset.change_attribute(changeset, :secret, new_secret)
|
Ash.Changeset.change_attribute(changeset, :secret, new_secret)
|
||||||
end
|
end
|
||||||
|
|
||||||
change after_action(fn _changeset, record, _context ->
|
|
||||||
WandererApp.ExternalEvents.WebhookDispatcher.invalidate_cache(record.map_id)
|
|
||||||
{:ok, record}
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -1,64 +0,0 @@
|
|||||||
defmodule WandererApp.Api.Preparations.FilterByActorMap do
|
|
||||||
@moduledoc """
|
|
||||||
Shared filtering logic for actor map context.
|
|
||||||
|
|
||||||
Filters queries to only return resources belonging to the actor's map.
|
|
||||||
Used by preparations for MapSystem, MapConnection, and MapPing resources.
|
|
||||||
"""
|
|
||||||
|
|
||||||
require Ash.Query
|
|
||||||
|
|
||||||
alias WandererApp.Api.ActorHelpers
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Filter a query by the actor's map context.
|
|
||||||
|
|
||||||
If a map is found in the context, filters the query to only return
|
|
||||||
resources where map_id matches. If no map context exists, returns
|
|
||||||
a query that will return no results.
|
|
||||||
|
|
||||||
## Parameters
|
|
||||||
|
|
||||||
* `query` - The Ash query to filter
|
|
||||||
* `context` - The Ash context containing actor/map information
|
|
||||||
* `resource_name` - Name of the resource for telemetry (atom)
|
|
||||||
|
|
||||||
## Examples
|
|
||||||
|
|
||||||
iex> query = Ash.Query.new(WandererApp.Api.MapSystem)
|
|
||||||
iex> context = %{map: %{id: "map-123"}}
|
|
||||||
iex> result = FilterByActorMap.filter_by_map(query, context, :map_system)
|
|
||||||
# Returns query filtered by map_id == "map-123"
|
|
||||||
"""
|
|
||||||
def filter_by_map(query, context, resource_name) do
|
|
||||||
case ActorHelpers.get_map(context) do
|
|
||||||
%{id: map_id} ->
|
|
||||||
emit_telemetry(resource_name, map_id)
|
|
||||||
Ash.Query.filter(query, map_id == ^map_id)
|
|
||||||
|
|
||||||
nil ->
|
|
||||||
emit_telemetry_no_context(resource_name)
|
|
||||||
Ash.Query.filter(query, false)
|
|
||||||
|
|
||||||
_other ->
|
|
||||||
emit_telemetry_no_context(resource_name)
|
|
||||||
Ash.Query.filter(query, false)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp emit_telemetry(resource_name, map_id) do
|
|
||||||
:telemetry.execute(
|
|
||||||
[:wanderer_app, :ash, :preparation, :filter_by_map],
|
|
||||||
%{count: 1},
|
|
||||||
%{resource: resource_name, map_id: map_id}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
defp emit_telemetry_no_context(resource_name) do
|
|
||||||
:telemetry.execute(
|
|
||||||
[:wanderer_app, :ash, :preparation, :filter_by_map, :no_context],
|
|
||||||
%{count: 1},
|
|
||||||
%{resource: resource_name}
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
defmodule WandererApp.Api.Preparations.FilterConnectionsByActorMap do
|
|
||||||
@moduledoc """
|
|
||||||
Ash preparation that filters connections to only those from the actor's map.
|
|
||||||
|
|
||||||
For token-based auth, this ensures the API only returns connections
|
|
||||||
from the map associated with the token.
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Ash.Resource.Preparation
|
|
||||||
|
|
||||||
alias WandererApp.Api.Preparations.FilterByActorMap
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def prepare(query, _opts, context) do
|
|
||||||
FilterByActorMap.filter_by_map(query, context, :map_connection)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
defmodule WandererApp.Api.Preparations.FilterPingsByActorMap do
|
|
||||||
@moduledoc """
|
|
||||||
Ash preparation that filters pings to only those from the actor's map.
|
|
||||||
|
|
||||||
For token-based auth, this ensures the API only returns pings
|
|
||||||
from the map associated with the token.
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Ash.Resource.Preparation
|
|
||||||
|
|
||||||
alias WandererApp.Api.Preparations.FilterByActorMap
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def prepare(query, _opts, context) do
|
|
||||||
FilterByActorMap.filter_by_map(query, context, :map_ping)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
defmodule WandererApp.Api.Preparations.FilterSystemsByActorMap do
|
|
||||||
@moduledoc """
|
|
||||||
Ash preparation that filters systems to only those from the actor's map.
|
|
||||||
|
|
||||||
For token-based auth, this ensures the API only returns systems
|
|
||||||
from the map associated with the token.
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Ash.Resource.Preparation
|
|
||||||
|
|
||||||
alias WandererApp.Api.Preparations.FilterByActorMap
|
|
||||||
|
|
||||||
@impl true
|
|
||||||
def prepare(query, _opts, context) do
|
|
||||||
FilterByActorMap.filter_by_map(query, context, :map_system)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
defmodule WandererApp.Api.Preparations.SecureApiKeyLookup do
|
|
||||||
@moduledoc """
|
|
||||||
Preparation that performs secure API key lookup using constant-time comparison.
|
|
||||||
|
|
||||||
This preparation:
|
|
||||||
1. Queries for the map with the given API key using database index
|
|
||||||
2. Performs constant-time comparison to verify the key matches
|
|
||||||
3. Returns the map only if the secure comparison passes
|
|
||||||
|
|
||||||
The constant-time comparison prevents timing attacks where an attacker
|
|
||||||
could deduce information about valid API keys by measuring response times.
|
|
||||||
"""
|
|
||||||
|
|
||||||
use Ash.Resource.Preparation
|
|
||||||
require Ash.Query
|
|
||||||
|
|
||||||
@dummy_key "dummy_key_for_timing_consistency_00000000"
|
|
||||||
|
|
||||||
def prepare(query, _params, _context) do
|
|
||||||
api_key = Ash.Query.get_argument(query, :api_key)
|
|
||||||
|
|
||||||
if is_nil(api_key) or api_key == "" do
|
|
||||||
# Return empty result for invalid input
|
|
||||||
Ash.Query.filter(query, expr(false))
|
|
||||||
else
|
|
||||||
# First, do the database lookup using the index
|
|
||||||
# Then apply constant-time comparison in after_action
|
|
||||||
query
|
|
||||||
|> Ash.Query.filter(expr(public_api_key == ^api_key))
|
|
||||||
|> Ash.Query.after_action(fn _query, results ->
|
|
||||||
verify_results_with_secure_compare(results, api_key)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp verify_results_with_secure_compare(results, provided_key) do
|
|
||||||
case results do
|
|
||||||
[map] ->
|
|
||||||
# Map found - verify with constant-time comparison
|
|
||||||
stored_key = map.public_api_key || @dummy_key
|
|
||||||
|
|
||||||
if Plug.Crypto.secure_compare(stored_key, provided_key) do
|
|
||||||
{:ok, [map]}
|
|
||||||
else
|
|
||||||
# Keys don't match (shouldn't happen if DB returned it, but safety check)
|
|
||||||
{:ok, []}
|
|
||||||
end
|
|
||||||
|
|
||||||
[] ->
|
|
||||||
# No map found - still do a comparison to maintain consistent timing
|
|
||||||
# This prevents timing attacks from distinguishing "not found" from "found but wrong"
|
|
||||||
_result = Plug.Crypto.secure_compare(@dummy_key, provided_key)
|
|
||||||
{:ok, []}
|
|
||||||
|
|
||||||
_multiple ->
|
|
||||||
# Multiple results - shouldn't happen with unique constraint
|
|
||||||
# Do comparison for timing consistency and return error
|
|
||||||
_result = Plug.Crypto.secure_compare(@dummy_key, provided_key)
|
|
||||||
{:ok, []}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -49,11 +49,7 @@ defmodule WandererApp.Api.ShipTypeInfo do
|
|||||||
:volume
|
:volume
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:read, :destroy]
|
defaults [:read, :destroy, :update]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
create :create do
|
create :create do
|
||||||
primary? true
|
primary? true
|
||||||
|
|||||||
@@ -51,15 +51,10 @@ defmodule WandererApp.Api.User do
|
|||||||
:hash
|
:hash
|
||||||
]
|
]
|
||||||
|
|
||||||
defaults [:create, :read, :destroy]
|
defaults [:create, :read, :update, :destroy]
|
||||||
|
|
||||||
update :update do
|
|
||||||
require_atomic? false
|
|
||||||
end
|
|
||||||
|
|
||||||
update :update_last_map do
|
update :update_last_map do
|
||||||
accept([:last_map_id])
|
accept([:last_map_id])
|
||||||
require_atomic? false
|
|
||||||
end
|
end
|
||||||
|
|
||||||
update :update_balance do
|
update :update_balance do
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ defmodule WandererApp.Api.UserActivity do
|
|||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: WandererApp.Api,
|
domain: WandererApp.Api,
|
||||||
data_layer: AshPostgres.DataLayer,
|
data_layer: AshPostgres.DataLayer,
|
||||||
extensions: [AshJsonApi.Resource],
|
extensions: [AshJsonApi.Resource]
|
||||||
primary_read_warning?: false
|
|
||||||
|
|
||||||
require Ash.Expr
|
require Ash.Expr
|
||||||
|
|
||||||
@@ -31,13 +30,6 @@ defmodule WandererApp.Api.UserActivity do
|
|||||||
|
|
||||||
includes([:character, :user])
|
includes([:character, :user])
|
||||||
|
|
||||||
default_fields([
|
|
||||||
:entity_id,
|
|
||||||
:entity_type,
|
|
||||||
:event_type,
|
|
||||||
:event_data
|
|
||||||
])
|
|
||||||
|
|
||||||
derive_filter?(true)
|
derive_filter?(true)
|
||||||
derive_sort?(true)
|
derive_sort?(true)
|
||||||
|
|
||||||
@@ -63,8 +55,7 @@ defmodule WandererApp.Api.UserActivity do
|
|||||||
:entity_type,
|
:entity_type,
|
||||||
:event_type,
|
:event_type,
|
||||||
:event_data,
|
:event_data,
|
||||||
:user_id,
|
:user_id
|
||||||
:character_id
|
|
||||||
]
|
]
|
||||||
|
|
||||||
read :read do
|
read :read do
|
||||||
@@ -79,8 +70,14 @@ defmodule WandererApp.Api.UserActivity do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create :new do
|
create :new do
|
||||||
accept [:entity_id, :entity_type, :event_type, :event_data, :user_id, :character_id]
|
accept [:entity_id, :entity_type, :event_type, :event_data]
|
||||||
primary?(true)
|
primary?(true)
|
||||||
|
|
||||||
|
argument :user_id, :uuid, allow_nil?: true
|
||||||
|
argument :character_id, :uuid, allow_nil?: true
|
||||||
|
|
||||||
|
change manage_relationship(:user_id, :user, on_lookup: :relate, on_no_match: nil)
|
||||||
|
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
destroy :archive do
|
destroy :archive do
|
||||||
@@ -93,12 +90,10 @@ defmodule WandererApp.Api.UserActivity do
|
|||||||
|
|
||||||
attribute :entity_id, :string do
|
attribute :entity_id, :string do
|
||||||
allow_nil? false
|
allow_nil? false
|
||||||
public? true
|
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :entity_type, :atom do
|
attribute :entity_type, :atom do
|
||||||
default "map"
|
default "map"
|
||||||
public? true
|
|
||||||
|
|
||||||
constraints(
|
constraints(
|
||||||
one_of: [
|
one_of: [
|
||||||
@@ -113,7 +108,6 @@ defmodule WandererApp.Api.UserActivity do
|
|||||||
|
|
||||||
attribute :event_type, :atom do
|
attribute :event_type, :atom do
|
||||||
default "custom"
|
default "custom"
|
||||||
public? true
|
|
||||||
|
|
||||||
constraints(
|
constraints(
|
||||||
one_of: [
|
one_of: [
|
||||||
@@ -163,9 +157,7 @@ defmodule WandererApp.Api.UserActivity do
|
|||||||
allow_nil?(false)
|
allow_nil?(false)
|
||||||
end
|
end
|
||||||
|
|
||||||
attribute :event_data, :string do
|
attribute :event_data, :string
|
||||||
public? true
|
|
||||||
end
|
|
||||||
|
|
||||||
create_timestamp(:inserted_at)
|
create_timestamp(:inserted_at)
|
||||||
update_timestamp(:updated_at)
|
update_timestamp(:updated_at)
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ defmodule WandererApp.Api.UserTransaction do
|
|||||||
create :new do
|
create :new do
|
||||||
accept [:journal_ref_id, :user_id, :date, :amount, :corporation_id]
|
accept [:journal_ref_id, :user_id, :date, :amount, :corporation_id]
|
||||||
primary?(true)
|
primary?(true)
|
||||||
|
|
||||||
|
argument :user_id, :uuid, allow_nil?: false
|
||||||
|
|
||||||
|
change manage_relationship(:user_id, :user, on_lookup: :relate, on_no_match: nil)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -86,11 +86,6 @@ defmodule WandererApp.Application do
|
|||||||
Supervisor.child_spec({Cachex, name: :wanderer_app_cache},
|
Supervisor.child_spec({Cachex, name: :wanderer_app_cache},
|
||||||
id: :wanderer_app_cache_worker
|
id: :wanderer_app_cache_worker
|
||||||
),
|
),
|
||||||
# Cache for webhook subscriptions - 5 minute TTL to reduce DB load
|
|
||||||
Supervisor.child_spec(
|
|
||||||
{Cachex, name: :webhook_subscriptions_cache, default_ttl: :timer.minutes(5)},
|
|
||||||
id: :webhook_subscriptions_cache_worker
|
|
||||||
),
|
|
||||||
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
|
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
|
||||||
{PartitionSupervisor,
|
{PartitionSupervisor,
|
||||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||||
@@ -158,16 +153,13 @@ defmodule WandererApp.Application do
|
|||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_start_corp_wallet_tracker(true) do
|
defp maybe_start_corp_wallet_tracker(true),
|
||||||
# Don't start corp wallet tracker in test environment
|
do: [
|
||||||
if Application.get_env(:wanderer_app, :environment) == :test do
|
WandererApp.StartCorpWalletTrackerTask
|
||||||
[]
|
]
|
||||||
else
|
|
||||||
[WandererApp.StartCorpWalletTrackerTask]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp maybe_start_corp_wallet_tracker(_), do: []
|
defp maybe_start_corp_wallet_tracker(_),
|
||||||
|
do: []
|
||||||
|
|
||||||
defp maybe_start_kills_services do
|
defp maybe_start_kills_services do
|
||||||
# Don't start kills services in test environment
|
# Don't start kills services in test environment
|
||||||
|
|||||||
@@ -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)),
|
def insert({id, key}, value, opts) when is_binary(id) and (is_binary(key) or is_atom(key)),
|
||||||
do: insert("#{id}:#{key}", value, opts)
|
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(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 \\ [])
|
def insert_or_update(key, value, update_fn, opts \\ [])
|
||||||
|
|||||||
@@ -93,8 +93,6 @@ defmodule WandererApp.CachedInfo do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_system_static_info(nil), do: {:ok, nil}
|
|
||||||
|
|
||||||
def get_system_static_info(solar_system_id) do
|
def get_system_static_info(solar_system_id) do
|
||||||
{:ok, solar_system_id} = APIUtils.parse_int(solar_system_id)
|
{:ok, solar_system_id} = APIUtils.parse_int(solar_system_id)
|
||||||
|
|
||||||
|
|||||||
@@ -598,6 +598,9 @@ defmodule WandererApp.Character.Tracker do
|
|||||||
|
|
||||||
{:error, :skipped}
|
{:error, :skipped}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
{:error, :skipped}
|
||||||
end
|
end
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
@@ -731,14 +734,6 @@ defmodule WandererApp.Character.Tracker do
|
|||||||
{:character_alliance, {character_id, character_update}}
|
{:character_alliance, {character_id, character_update}}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Broadcast permission update to trigger LiveView refresh
|
|
||||||
# This ensures users are kicked off maps they no longer have access to
|
|
||||||
@pubsub_client.broadcast(
|
|
||||||
WandererApp.PubSub,
|
|
||||||
"character:#{character.eve_id}",
|
|
||||||
:update_permissions
|
|
||||||
)
|
|
||||||
|
|
||||||
state
|
state
|
||||||
|> Map.merge(%{alliance_id: nil})
|
|> Map.merge(%{alliance_id: nil})
|
||||||
end
|
end
|
||||||
@@ -777,14 +772,6 @@ defmodule WandererApp.Character.Tracker do
|
|||||||
{:character_alliance, {character_id, character_update}}
|
{:character_alliance, {character_id, character_update}}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Broadcast permission update to trigger LiveView refresh
|
|
||||||
# This ensures users are kicked off maps they no longer have access to
|
|
||||||
@pubsub_client.broadcast(
|
|
||||||
WandererApp.PubSub,
|
|
||||||
"character:#{character.eve_id}",
|
|
||||||
:update_permissions
|
|
||||||
)
|
|
||||||
|
|
||||||
state
|
state
|
||||||
|> Map.merge(%{alliance_id: alliance_id})
|
|> Map.merge(%{alliance_id: alliance_id})
|
||||||
|
|
||||||
@@ -812,7 +799,7 @@ defmodule WandererApp.Character.Tracker do
|
|||||||
corporation_id
|
corporation_id
|
||||||
|> WandererApp.Esi.get_corporation_info()
|
|> WandererApp.Esi.get_corporation_info()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker}} ->
|
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
|
||||||
{:ok, character} =
|
{:ok, character} =
|
||||||
WandererApp.Character.get_character(character_id)
|
WandererApp.Character.get_character(character_id)
|
||||||
|
|
||||||
@@ -839,14 +826,6 @@ defmodule WandererApp.Character.Tracker do
|
|||||||
}}}
|
}}}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Broadcast permission update to trigger LiveView refresh
|
|
||||||
# This ensures users are kicked off maps they no longer have access to
|
|
||||||
@pubsub_client.broadcast(
|
|
||||||
WandererApp.PubSub,
|
|
||||||
"character:#{character.eve_id}",
|
|
||||||
:update_permissions
|
|
||||||
)
|
|
||||||
|
|
||||||
state
|
state
|
||||||
|> Map.merge(%{corporation_id: corporation_id})
|
|> Map.merge(%{corporation_id: corporation_id})
|
||||||
|
|
||||||
@@ -1023,7 +1002,7 @@ defmodule WandererApp.Character.Tracker do
|
|||||||
defp maybe_update_active_maps(
|
defp maybe_update_active_maps(
|
||||||
%{character_id: character_id, active_maps: active_maps} =
|
%{character_id: character_id, active_maps: active_maps} =
|
||||||
state,
|
state,
|
||||||
%{map_id: map_id, track: true}
|
%{map_id: map_id, track: true} = track_settings
|
||||||
) do
|
) do
|
||||||
if not Enum.member?(active_maps, map_id) do
|
if not Enum.member?(active_maps, map_id) do
|
||||||
WandererApp.Cache.put(
|
WandererApp.Cache.put(
|
||||||
|
|||||||
@@ -1,18 +1,5 @@
|
|||||||
defmodule WandererApp.Character.TrackerManager.Impl do
|
defmodule WandererApp.Character.TrackerManager.Impl do
|
||||||
@moduledoc """
|
@moduledoc false
|
||||||
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
|
require Logger
|
||||||
|
|
||||||
defstruct [
|
defstruct [
|
||||||
@@ -40,13 +27,6 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
||||||
Process.send_after(self(), :untrack_characters, @untrack_characters_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: [],
|
characters: [],
|
||||||
opts: args
|
opts: args
|
||||||
@@ -58,12 +38,6 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||||
WandererApp.Cache.insert("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
|
tracked_characters
|
||||||
|> Enum.each(fn character_id ->
|
|> Enum.each(fn character_id ->
|
||||||
start_tracking(state, character_id)
|
start_tracking(state, character_id)
|
||||||
@@ -79,9 +53,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
Logger.debug(fn ->
|
Logger.debug(fn -> "Add character to track_characters_queue: #{inspect(character_id)}" end)
|
||||||
"[TrackerManager] Queuing character #{character_id} for tracking start"
|
|
||||||
end)
|
|
||||||
|
|
||||||
WandererApp.Cache.insert_or_update(
|
WandererApp.Cache.insert_or_update(
|
||||||
"track_characters_queue",
|
"track_characters_queue",
|
||||||
@@ -99,33 +71,13 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||||
true <- Enum.member?(characters, character_id),
|
true <- Enum.member?(characters, character_id),
|
||||||
false <- WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
false <- WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
||||||
Logger.debug(fn ->
|
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||||
"[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.Cache.delete("character:#{character_id}:last_active_time")
|
||||||
WandererApp.Character.delete_character_state(character_id)
|
WandererApp.Character.delete_character_state(character_id)
|
||||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||||
|
|
||||||
:telemetry.execute(
|
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||||
[: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
|
end
|
||||||
|
|
||||||
WandererApp.Cache.insert_or_update(
|
WandererApp.Cache.insert_or_update(
|
||||||
@@ -149,35 +101,13 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
} = track_settings
|
} = track_settings
|
||||||
) do
|
) do
|
||||||
if track 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)
|
remove_from_untrack_queue(map_id, character_id)
|
||||||
|
|
||||||
case WandererApp.Character.Tracker.update_settings(character_id, track_settings) do
|
{:ok, character_state} =
|
||||||
{:ok, character_state} ->
|
WandererApp.Character.Tracker.update_settings(character_id, track_settings)
|
||||||
WandererApp.Character.update_character_state(character_id, character_state)
|
|
||||||
|
|
||||||
{:error, :not_found} ->
|
WandererApp.Character.update_character_state(character_id, character_state)
|
||||||
# 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
|
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)
|
add_to_untrack_queue(map_id, character_id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -200,20 +130,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
"character_untrack_queue",
|
"character_untrack_queue",
|
||||||
[],
|
[],
|
||||||
fn untrack_queue ->
|
fn untrack_queue ->
|
||||||
original_length = length(untrack_queue)
|
untrack_queue
|
||||||
|
|> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)
|
||||||
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
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -252,12 +170,6 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
|
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
|
||||||
{:ok, track_characters_queue} = WandererApp.Cache.lookup("track_characters_queue", [])
|
{: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
|
track_characters_queue
|
||||||
|> Enum.each(fn character_id ->
|
|> Enum.each(fn character_id ->
|
||||||
track_character(character_id, %{})
|
track_character(character_id, %{})
|
||||||
@@ -274,66 +186,35 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
|
|
||||||
{:ok, characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
{:ok, characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||||
|
|
||||||
Logger.debug(fn ->
|
characters
|
||||||
"[TrackerManager] Running garbage collection on #{length(characters)} tracked characters"
|
|> Task.async_stream(
|
||||||
end)
|
fn character_id ->
|
||||||
|
case WandererApp.Cache.lookup("character:#{character_id}:last_active_time") do
|
||||||
|
{:ok, nil} ->
|
||||||
|
:skip
|
||||||
|
|
||||||
inactive_characters =
|
{:ok, last_active_time} ->
|
||||||
characters
|
duration = DateTime.diff(DateTime.utc_now(), last_active_time, :second)
|
||||||
|> Task.async_stream(
|
|
||||||
fn character_id ->
|
if duration * 1000 > @inactive_character_timeout do
|
||||||
case WandererApp.Cache.lookup("character:#{character_id}:last_active_time") do
|
{:stop, character_id}
|
||||||
{:ok, nil} ->
|
else
|
||||||
# Character is still active (no last_active_time set)
|
|
||||||
:skip
|
:skip
|
||||||
|
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
|
||||||
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)
|
||||||
|
|
||||||
if length(inactive_characters) > 0 do
|
_ ->
|
||||||
Logger.debug(fn ->
|
:ok
|
||||||
"[TrackerManager] Garbage collection found #{length(inactive_characters)} inactive characters to stop"
|
end
|
||||||
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)
|
end)
|
||||||
|
|
||||||
state
|
state
|
||||||
@@ -345,22 +226,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
) do
|
) do
|
||||||
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
|
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
|
||||||
|
|
||||||
untrack_queue = WandererApp.Cache.lookup!("character_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(
|
|> Task.async_stream(
|
||||||
fn {map_id, character_id} ->
|
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)
|
remove_from_untrack_queue(map_id, character_id)
|
||||||
|
|
||||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
||||||
@@ -387,36 +255,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
|
|
||||||
WandererApp.Character.update_character_state(character_id, character_state)
|
WandererApp.Character.update_character_state(character_id, character_state)
|
||||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
|
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,
|
end,
|
||||||
max_concurrency: System.schedulers_online() * 4,
|
max_concurrency: System.schedulers_online() * 4,
|
||||||
on_timeout: :kill_task,
|
on_timeout: :kill_task,
|
||||||
timeout: :timer.seconds(30)
|
timeout: :timer.seconds(30)
|
||||||
)
|
)
|
||||||
|> Enum.each(fn result ->
|
|> Enum.each(fn _result -> :ok end)
|
||||||
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
|
state
|
||||||
end
|
end
|
||||||
@@ -424,17 +268,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
def handle_info({:stop_track, character_id}, state) do
|
def handle_info({:stop_track, character_id}, state) do
|
||||||
if not WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking") do
|
if not WandererApp.Cache.has_key?("character:#{character_id}:is_stop_tracking") do
|
||||||
WandererApp.Cache.insert("character:#{character_id}:is_stop_tracking", true)
|
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)
|
stop_tracking(state, character_id)
|
||||||
WandererApp.Cache.delete("character:#{character_id}:is_stop_tracking")
|
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
|
end
|
||||||
|
|
||||||
state
|
state
|
||||||
@@ -443,9 +279,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
def track_character(character_id, opts) do
|
def track_character(character_id, opts) do
|
||||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||||
false <- Enum.member?(characters, character_id) do
|
false <- Enum.member?(characters, character_id) do
|
||||||
Logger.debug(fn ->
|
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
|
||||||
"[TrackerManager] Starting tracker for character #{character_id}"
|
|
||||||
end)
|
|
||||||
|
|
||||||
WandererApp.Cache.insert_or_update(
|
WandererApp.Cache.insert_or_update(
|
||||||
"tracked_characters",
|
"tracked_characters",
|
||||||
@@ -478,30 +312,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
|||||||
character_id,
|
character_id,
|
||||||
%{opts: opts}
|
%{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
|
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(
|
WandererApp.Cache.insert_or_update(
|
||||||
"track_characters_queue",
|
"track_characters_queue",
|
||||||
|
|||||||
@@ -88,4 +88,15 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
|
|||||||
{:ok, pid}
|
{:ok, pid}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ defmodule WandererApp.Character.TrackingConfigUtils do
|
|||||||
%{id: "default", title: "Default", value: default_count}
|
%{id: "default", title: "Default", value: default_count}
|
||||||
]
|
]
|
||||||
|
|
||||||
{:ok, _pools_count} =
|
{:ok, pools_count} =
|
||||||
Cachex.get(
|
Cachex.get(
|
||||||
:esi_auth_cache,
|
:esi_auth_cache,
|
||||||
"configs_total_count"
|
"configs_total_count"
|
||||||
|
|||||||
@@ -53,27 +53,24 @@ defmodule WandererApp.Character.TrackingUtils do
|
|||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Builds tracking data for all characters with access to a map.
|
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
|
def build_tracking_data(map_id, current_user_id) do
|
||||||
with {:ok, map} <- WandererApp.MapRepo.get(map_id),
|
with {:ok, map} <-
|
||||||
|
WandererApp.MapRepo.get(map_id,
|
||||||
|
acls: [
|
||||||
|
:owner_id,
|
||||||
|
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||||
|
]
|
||||||
|
),
|
||||||
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||||
{:ok, %{characters: characters_with_access}} <-
|
{:ok, %{characters: characters_with_access}} <-
|
||||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
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
|
# Map characters to tracking data
|
||||||
{:ok, characters_data} =
|
{:ok, characters_data} =
|
||||||
build_character_tracking_data(characters_with_tracking_permission)
|
build_character_tracking_data(characters_with_access)
|
||||||
|
|
||||||
{:ok, main_character} =
|
{:ok, main_character} =
|
||||||
get_main_character(
|
get_main_character(user_settings, characters_with_access, characters_with_access)
|
||||||
user_settings,
|
|
||||||
characters_with_tracking_permission,
|
|
||||||
characters_with_tracking_permission
|
|
||||||
)
|
|
||||||
|
|
||||||
following_character_eve_id =
|
following_character_eve_id =
|
||||||
case user_settings do
|
case user_settings do
|
||||||
@@ -115,160 +112,10 @@ defmodule WandererApp.Character.TrackingUtils do
|
|||||||
end)}
|
end)}
|
||||||
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
|
# Private implementation of update character tracking
|
||||||
defp do_update_character_tracking(character, map_id, track, caller_pid) do
|
defp do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||||
# First check current tracking state to avoid unnecessary permission checks
|
WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|
||||||
current_settings = WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|
|> case do
|
||||||
|
|
||||||
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
|
# Untracking flow
|
||||||
{:ok, %{tracked: true} = existing_settings} ->
|
{:ok, %{tracked: true} = existing_settings} ->
|
||||||
if not track do
|
if not track do
|
||||||
@@ -285,9 +132,6 @@ defmodule WandererApp.Character.TrackingUtils do
|
|||||||
{:ok, %{tracked: false} = existing_settings} ->
|
{:ok, %{tracked: false} = existing_settings} ->
|
||||||
if track do
|
if track do
|
||||||
{:ok, updated_settings} = WandererApp.MapCharacterSettingsRepo.track(existing_settings)
|
{: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 = track([character], map_id, true, caller_pid)
|
||||||
{:ok, updated_settings}
|
{:ok, updated_settings}
|
||||||
else
|
else
|
||||||
@@ -304,9 +148,6 @@ defmodule WandererApp.Character.TrackingUtils do
|
|||||||
tracked: true
|
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 = track([character], map_id, true, caller_pid)
|
||||||
{:ok, settings}
|
{:ok, settings}
|
||||||
else
|
else
|
||||||
@@ -369,31 +210,6 @@ defmodule WandererApp.Character.TrackingUtils do
|
|||||||
|
|
||||||
if is_track_allowed do
|
if is_track_allowed do
|
||||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
: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
|
end
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ defmodule WandererApp.Env do
|
|||||||
def invites(), do: get_key(:invites, false)
|
def invites(), do: get_key(:invites, false)
|
||||||
|
|
||||||
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
|
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
|
||||||
|
def websocket_events_enabled?(), do: get_key(:websocket_events_enabled, false)
|
||||||
def public_api_disabled?(), do: get_key(:public_api_disabled, false)
|
def public_api_disabled?(), do: get_key(:public_api_disabled, false)
|
||||||
|
|
||||||
@decorate cacheable(
|
@decorate cacheable(
|
||||||
@@ -42,35 +43,6 @@ defmodule WandererApp.Env do
|
|||||||
def corp_eve_id(), do: get_key(:corp_id, -1)
|
def corp_eve_id(), do: get_key(:corp_id, -1)
|
||||||
def subscription_settings(), do: get_key(:subscription_settings)
|
def subscription_settings(), do: get_key(:subscription_settings)
|
||||||
|
|
||||||
@doc """
|
|
||||||
Returns the promo code configuration map.
|
|
||||||
Keys are uppercase code strings, values are discount percentages.
|
|
||||||
"""
|
|
||||||
def promo_codes() do
|
|
||||||
case subscription_settings() do
|
|
||||||
%{promo_codes: codes} when is_map(codes) -> codes
|
|
||||||
_ -> %{}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Validates a promo code and returns the discount percentage.
|
|
||||||
Returns {:ok, discount_percent} if valid, {:error, :invalid_code} otherwise.
|
|
||||||
Codes are case-insensitive.
|
|
||||||
"""
|
|
||||||
def validate_promo_code(nil), do: {:error, :invalid_code}
|
|
||||||
def validate_promo_code(""), do: {:error, :invalid_code}
|
|
||||||
|
|
||||||
def validate_promo_code(code) when is_binary(code) do
|
|
||||||
normalized = String.upcase(String.trim(code))
|
|
||||||
|
|
||||||
case Map.get(promo_codes(), normalized) do
|
|
||||||
nil -> {:error, :invalid_code}
|
|
||||||
discount when is_integer(discount) and discount > 0 and discount <= 100 -> {:ok, discount}
|
|
||||||
_ -> {:error, :invalid_code}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@decorate cacheable(
|
@decorate cacheable(
|
||||||
cache: WandererApp.Cache,
|
cache: WandererApp.Cache,
|
||||||
key: "restrict_maps_creation"
|
key: "restrict_maps_creation"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ defmodule WandererApp.Esi.ApiClient do
|
|||||||
@ttl :timer.hours(1)
|
@ttl :timer.hours(1)
|
||||||
|
|
||||||
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
|
@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]
|
@cache_opts [cache: true]
|
||||||
@retry_opts [retry: false, retry_log_level: :warning]
|
@retry_opts [retry: false, retry_log_level: :warning]
|
||||||
@@ -73,7 +74,7 @@ defmodule WandererApp.Esi.ApiClient do
|
|||||||
|> Keyword.merge(@timeout_opts)
|
|> Keyword.merge(@timeout_opts)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_routes_eve(hubs, origin, _params, _opts),
|
def get_routes_eve(hubs, origin, params, opts),
|
||||||
do:
|
do:
|
||||||
{:ok,
|
{:ok,
|
||||||
hubs
|
hubs
|
||||||
@@ -100,6 +101,33 @@ defmodule WandererApp.Esi.ApiClient do
|
|||||||
end
|
end
|
||||||
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(
|
@decorate cacheable(
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
key: "group-info-#{group_id}",
|
key: "group-info-#{group_id}",
|
||||||
@@ -245,8 +273,6 @@ defmodule WandererApp.Esi.ApiClient do
|
|||||||
opts: [ttl: @ttl]
|
opts: [ttl: @ttl]
|
||||||
)
|
)
|
||||||
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
|
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)
|
get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -322,7 +348,7 @@ defmodule WandererApp.Esi.ApiClient do
|
|||||||
defp with_cache_opts(opts),
|
defp with_cache_opts(opts),
|
||||||
do: opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
|
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
|
case Cachex.get(:api_cache, path) do
|
||||||
{:ok, cached_data} when not is_nil(cached_data) ->
|
{:ok, cached_data} when not is_nil(cached_data) ->
|
||||||
{:ok, cached_data}
|
{:ok, cached_data}
|
||||||
@@ -332,7 +358,7 @@ defmodule WandererApp.Esi.ApiClient do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp do_get_request(path, api_opts, opts, pool) do
|
defp do_get_request(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||||
try do
|
try do
|
||||||
req_options_for_pool(pool)
|
req_options_for_pool(pool)
|
||||||
|> Req.new()
|
|> Req.new()
|
||||||
@@ -422,7 +448,7 @@ defmodule WandererApp.Esi.ApiClient do
|
|||||||
{:ok, %{status: status} = _error} when status in [401, 403] ->
|
{:ok, %{status: status} = _error} when status in [401, 403] ->
|
||||||
do_get_retry(path, api_opts, opts)
|
do_get_retry(path, api_opts, opts)
|
||||||
|
|
||||||
{:ok, %{status: status}} ->
|
{:ok, %{status: status, headers: headers}} ->
|
||||||
{:error, "Unexpected status: #{status}"}
|
{:error, "Unexpected status: #{status}"}
|
||||||
|
|
||||||
{:error, %Mint.TransportError{reason: :timeout}} ->
|
{:error, %Mint.TransportError{reason: :timeout}} ->
|
||||||
@@ -806,10 +832,10 @@ defmodule WandererApp.Esi.ApiClient do
|
|||||||
|
|
||||||
defp handle_refresh_token_result(
|
defp handle_refresh_token_result(
|
||||||
{:error, %OAuth2.Error{reason: :econnrefused} = error},
|
{:error, %OAuth2.Error{reason: :econnrefused} = error},
|
||||||
_character,
|
character,
|
||||||
character_id,
|
character_id,
|
||||||
expires_at,
|
expires_at,
|
||||||
_scopes
|
scopes
|
||||||
) do
|
) do
|
||||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ defmodule WandererApp.EveDataService do
|
|||||||
|
|
||||||
alias WandererApp.Utils.JSONUtil
|
alias WandererApp.Utils.JSONUtil
|
||||||
|
|
||||||
# @eve_db_dump_url "https://www.fuzzwork.co.uk/dump/latest"
|
@eve_db_dump_url "https://www.fuzzwork.co.uk/dump/latest"
|
||||||
@eve_db_dump_url "https://wanderer-industries.github.io/wanderer-assets/sde-files"
|
|
||||||
|
|
||||||
@dump_file_names [
|
@dump_file_names [
|
||||||
"invGroups.csv",
|
"invGroups.csv",
|
||||||
@@ -394,6 +393,9 @@ defmodule WandererApp.EveDataService do
|
|||||||
end
|
end
|
||||||
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
|
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
|
case Enum.find(triglavian_systems, fn system -> system.solar_system_id == solar_system_id end) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -411,12 +413,8 @@ defmodule WandererApp.EveDataService do
|
|||||||
|
|
||||||
defp get_security(security) do
|
defp get_security(security) do
|
||||||
case security do
|
case security do
|
||||||
nil ->
|
nil -> {:ok, ""}
|
||||||
{:ok, ""}
|
_ -> {:ok, String.to_float(security) |> get_true_security() |> Float.to_string(decimals: 1)}
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:ok,
|
|
||||||
String.to_float(security) |> get_true_security() |> :erlang.float_to_binary(decimals: 1)}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -498,23 +496,23 @@ defmodule WandererApp.EveDataService do
|
|||||||
do: {:ok, 10_100}
|
do: {:ok, 10_100}
|
||||||
|
|
||||||
defp get_wormhole_class_id(systems, region_id, constellation_id, solar_system_id) do
|
defp get_wormhole_class_id(systems, region_id, constellation_id, solar_system_id) do
|
||||||
region =
|
with region <-
|
||||||
Enum.find(systems, fn system ->
|
Enum.find(systems, fn system ->
|
||||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||||
end)
|
end),
|
||||||
|
constellation <-
|
||||||
constellation =
|
Enum.find(systems, fn system ->
|
||||||
Enum.find(systems, fn system ->
|
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
||||||
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
end),
|
||||||
end)
|
solar_system <-
|
||||||
|
Enum.find(systems, fn system ->
|
||||||
solar_system =
|
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||||
Enum.find(systems, fn system ->
|
end),
|
||||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
wormhole_class_id <- get_wormhole_class_id(region, constellation, solar_system) do
|
||||||
end)
|
{:ok, wormhole_class_id}
|
||||||
|
else
|
||||||
wormhole_class_id = get_wormhole_class_id(region, constellation, solar_system)
|
_ -> {:ok, -1}
|
||||||
{:ok, wormhole_class_id}
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_wormhole_class_id(_region, _constellation, solar_system)
|
defp get_wormhole_class_id(_region, _constellation, solar_system)
|
||||||
|
|||||||
@@ -178,10 +178,6 @@ defmodule WandererApp.ExternalEvents.Event do
|
|||||||
end
|
end
|
||||||
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
|
# Get allowed fields based on struct type
|
||||||
defp get_allowed_fields(module) do
|
defp get_allowed_fields(module) do
|
||||||
module_name = module |> Module.split() |> List.last()
|
module_name = module |> Module.split() |> List.last()
|
||||||
@@ -196,6 +192,10 @@ defmodule WandererApp.ExternalEvents.Event do
|
|||||||
end
|
end
|
||||||
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
|
defp serialize_fields(fields, visited) do
|
||||||
Enum.reduce(fields, %{}, fn {k, v}, acc ->
|
Enum.reduce(fields, %{}, fn {k, v}, acc ->
|
||||||
if is_nil(v) do
|
if is_nil(v) do
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
|
|||||||
"id" => payload["system_id"] || payload[:system_id],
|
"id" => payload["system_id"] || payload[:system_id],
|
||||||
"attributes" => %{
|
"attributes" => %{
|
||||||
"locked" => payload["locked"] || payload[:locked],
|
"locked" => payload["locked"] || payload[:locked],
|
||||||
"position_x" => payload["position_x"] || payload[:position_x],
|
"x" => payload["x"] || payload[:x],
|
||||||
"position_y" => payload["position_y"] || payload[:position_y],
|
"y" => payload["y"] || payload[:y],
|
||||||
"updated_at" => event.timestamp
|
"updated_at" => event.timestamp
|
||||||
},
|
},
|
||||||
"relationships" => %{
|
"relationships" => %{
|
||||||
|
|||||||
@@ -155,23 +155,26 @@ defmodule WandererApp.ExternalEvents.MapEventRelay do
|
|||||||
# 1. Store in ETS for backfill
|
# 1. Store in ETS for backfill
|
||||||
store_event(event, state.ets_table)
|
store_event(event, state.ets_table)
|
||||||
|
|
||||||
|
# 2. Convert event to JSON for delivery methods
|
||||||
event_json = Event.to_json(event)
|
event_json = Event.to_json(event)
|
||||||
|
|
||||||
|
Logger.debug(fn ->
|
||||||
|
"MapEventRelay converted event to JSON: #{inspect(String.slice(inspect(event_json), 0, 200))}..."
|
||||||
|
end)
|
||||||
|
|
||||||
|
# 3. Send to webhook subscriptions via WebhookDispatcher
|
||||||
WebhookDispatcher.dispatch_event(event.map_id, event)
|
WebhookDispatcher.dispatch_event(event.map_id, event)
|
||||||
|
|
||||||
case WandererApp.ExternalEvents.SseAccessControl.sse_allowed?(event.map_id) do
|
# 4. Broadcast to SSE clients
|
||||||
:ok ->
|
Logger.debug(fn -> "MapEventRelay broadcasting to SSE clients for map #{event.map_id}" end)
|
||||||
WandererApp.ExternalEvents.SseStreamManager.broadcast_event(event.map_id, event_json)
|
WandererApp.ExternalEvents.SseStreamManager.broadcast_event(event.map_id, event_json)
|
||||||
|
|
||||||
:telemetry.execute(
|
# Emit delivered telemetry
|
||||||
[:wanderer_app, :external_events, :relay, :delivered],
|
:telemetry.execute(
|
||||||
%{count: 1},
|
[:wanderer_app, :external_events, :relay, :delivered],
|
||||||
%{map_id: event.map_id, event_type: event.type}
|
%{count: 1},
|
||||||
)
|
%{map_id: event.map_id, event_type: event.type}
|
||||||
|
)
|
||||||
{:error, _reason} ->
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
%{state | event_count: state.event_count + 1}
|
%{state | event_count: state.event_count + 1}
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
defmodule WandererApp.ExternalEvents.SseAccessControl do
|
|
||||||
@moduledoc """
|
|
||||||
Handles SSE access control checks including subscription validation.
|
|
||||||
|
|
||||||
IMPORTANT: This module is optimized for high-frequency calls during event delivery.
|
|
||||||
All checks use cached data to avoid database queries on every event.
|
|
||||||
|
|
||||||
Note: Community Edition mode is automatically handled - when subscriptions are
|
|
||||||
disabled globally, we skip the subscription check entirely.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks if SSE is allowed for a given map.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
- :ok if SSE is allowed
|
|
||||||
- {:error, reason} if SSE is not allowed
|
|
||||||
|
|
||||||
Checks in order:
|
|
||||||
1. Global SSE enabled (config check - no DB)
|
|
||||||
2. Map SSE enabled (cache check - no DB)
|
|
||||||
3. Subscription active (cache check or skipped in CE mode - no DB)
|
|
||||||
"""
|
|
||||||
def sse_allowed?(map_id) do
|
|
||||||
with :ok <- check_sse_globally_enabled(),
|
|
||||||
:ok <- check_map_sse_enabled_cached(map_id),
|
|
||||||
:ok <- check_subscription_or_ce_cached(map_id) do
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp check_sse_globally_enabled do
|
|
||||||
if WandererApp.Env.sse_enabled?() do
|
|
||||||
:ok
|
|
||||||
else
|
|
||||||
{:error, :sse_globally_disabled}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Uses the map cache with fallback to DB query
|
|
||||||
defp check_map_sse_enabled_cached(map_id) do
|
|
||||||
case WandererApp.Map.sse_enabled_with_status(map_id) do
|
|
||||||
{:ok, true} -> :ok
|
|
||||||
{:ok, false} -> {:error, :sse_disabled_for_map}
|
|
||||||
{:error, :not_found} -> {:error, :map_not_found}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Checks subscription status using cached data.
|
|
||||||
# In CE mode (subscriptions disabled globally), this is a fast config check.
|
|
||||||
# In Enterprise mode, uses cached map state's subscription settings.
|
|
||||||
defp check_subscription_or_ce_cached(map_id) do
|
|
||||||
# Fast path: CE mode - subscriptions disabled globally
|
|
||||||
if not WandererApp.Env.map_subscriptions_enabled?() do
|
|
||||||
:ok
|
|
||||||
else
|
|
||||||
# Enterprise mode: check cached subscription status from map state
|
|
||||||
check_subscription_from_cache(map_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Checks subscription status from the map cache.
|
|
||||||
# Falls back to DB query only if cache miss.
|
|
||||||
defp check_subscription_from_cache(map_id) do
|
|
||||||
case WandererApp.Map.subscription_active_cached?(map_id) do
|
|
||||||
{:ok, true} ->
|
|
||||||
:ok
|
|
||||||
|
|
||||||
{:ok, false} ->
|
|
||||||
{:error, :subscription_required}
|
|
||||||
|
|
||||||
{:error, :not_cached} ->
|
|
||||||
# Cache miss - fall back to DB check
|
|
||||||
# This should be rare as maps are initialized when accessed
|
|
||||||
fallback_subscription_check(map_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Fallback to DB query - only used when cache miss
|
|
||||||
defp fallback_subscription_check(map_id) do
|
|
||||||
case WandererApp.Map.is_subscription_active?(map_id) do
|
|
||||||
{:ok, true} -> :ok
|
|
||||||
{:ok, false} -> {:error, :subscription_required}
|
|
||||||
{:error, _reason} = error -> error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -166,37 +166,6 @@ defmodule WandererApp.ExternalEvents.WebhookDispatcher do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp get_active_subscriptions(map_id) do
|
defp get_active_subscriptions(map_id) do
|
||||||
# Use cache to avoid DB query on every event
|
|
||||||
cache_key = "map:#{map_id}"
|
|
||||||
|
|
||||||
case Cachex.get(:webhook_subscriptions_cache, cache_key) do
|
|
||||||
{:ok, nil} ->
|
|
||||||
# Cache miss - fetch from DB and cache
|
|
||||||
fetch_and_cache_subscriptions(map_id, cache_key)
|
|
||||||
|
|
||||||
{:ok, subscriptions} ->
|
|
||||||
# Cache hit
|
|
||||||
{:ok, subscriptions}
|
|
||||||
|
|
||||||
{:error, _reason} ->
|
|
||||||
# Cache error - fall back to DB
|
|
||||||
fetch_subscriptions_from_db(map_id)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_and_cache_subscriptions(map_id, cache_key) do
|
|
||||||
case fetch_subscriptions_from_db(map_id) do
|
|
||||||
{:ok, subscriptions} = result ->
|
|
||||||
# Cache for 5 minutes (TTL set on cache, but explicit here for clarity)
|
|
||||||
Cachex.put(:webhook_subscriptions_cache, cache_key, subscriptions)
|
|
||||||
result
|
|
||||||
|
|
||||||
error ->
|
|
||||||
error
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp fetch_subscriptions_from_db(map_id) do
|
|
||||||
try do
|
try do
|
||||||
subscriptions = MapWebhookSubscription.active_by_map!(map_id)
|
subscriptions = MapWebhookSubscription.active_by_map!(map_id)
|
||||||
{:ok, subscriptions}
|
{:ok, subscriptions}
|
||||||
@@ -440,25 +409,17 @@ defmodule WandererApp.ExternalEvents.WebhookDispatcher do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp webhooks_allowed?(map_id, webhooks_globally_enabled) do
|
defp webhooks_allowed?(map_id, webhooks_globally_enabled) do
|
||||||
cond do
|
with true <- webhooks_globally_enabled,
|
||||||
not webhooks_globally_enabled ->
|
{:ok, map} <- WandererApp.Api.Map.by_id(map_id),
|
||||||
{:error, :webhooks_globally_disabled}
|
true <- map.webhooks_enabled do
|
||||||
|
:ok
|
||||||
not WandererApp.Map.webhooks_enabled?(map_id) ->
|
else
|
||||||
{:error, :webhooks_disabled_for_map}
|
false -> {:error, :webhooks_globally_disabled}
|
||||||
|
nil -> {:error, :webhooks_globally_disabled}
|
||||||
true ->
|
{:error, :not_found} -> {:error, :map_not_found}
|
||||||
:ok
|
%{webhooks_enabled: false} -> {:error, :webhooks_disabled_for_map}
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
error -> {:error, {:unexpected_error, error}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Invalidates the webhook subscriptions cache for a map.
|
|
||||||
Called when subscriptions are created, updated, or deleted.
|
|
||||||
"""
|
|
||||||
def invalidate_cache(map_id) do
|
|
||||||
cache_key = "map:#{map_id}"
|
|
||||||
Cachex.del(:webhook_subscriptions_cache, cache_key)
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -182,7 +182,7 @@ defmodule WandererApp.Kills.Client do
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Guard against duplicate disconnection events
|
# 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}
|
{:noreply, state}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -566,7 +566,7 @@ defmodule WandererApp.Kills.Client do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp check_health(%{socket_pid: pid}) do
|
defp check_health(%{socket_pid: pid} = state) do
|
||||||
if socket_alive?(pid) do
|
if socket_alive?(pid) do
|
||||||
:healthy
|
:healthy
|
||||||
else
|
else
|
||||||
@@ -590,6 +590,22 @@ defmodule WandererApp.Kills.Client do
|
|||||||
Process.send_after(self(), :health_check, @health_check_interval)
|
Process.send_after(self(), :health_check, @health_check_interval)
|
||||||
end
|
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
|
# Handler module for WebSocket events
|
||||||
defmodule Handler do
|
defmodule Handler do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
@@ -624,7 +640,7 @@ defmodule WandererApp.Kills.Client do
|
|||||||
}
|
}
|
||||||
|
|
||||||
case GenSocketClient.join(transport, "killmails:lobby", join_params) do
|
case GenSocketClient.join(transport, "killmails:lobby", join_params) do
|
||||||
{:ok, _response} ->
|
{:ok, response} ->
|
||||||
send(state.parent, {:connected, self()})
|
send(state.parent, {:connected, self()})
|
||||||
# Reset disconnected flag on successful connection
|
# Reset disconnected flag on successful connection
|
||||||
{:ok, %{state | disconnected: false}}
|
{:ok, %{state | disconnected: false}}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
|||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@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)}
|
{:noreply, schedule_subscription_update(state)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
|||||||
# Client is not connected, retry with backoff
|
# Client is not connected, retry with backoff
|
||||||
schedule_retry_update(state)
|
schedule_retry_update(state)
|
||||||
|
|
||||||
_error ->
|
error ->
|
||||||
schedule_retry_update(state)
|
schedule_retry_update(state)
|
||||||
end
|
end
|
||||||
rescue
|
rescue
|
||||||
|
|||||||
@@ -403,24 +403,10 @@ defmodule WandererApp.Kills.MessageHandler do
|
|||||||
|
|
||||||
defp extract_field(_data, _field_names), do: nil
|
defp extract_field(_data, _field_names), do: nil
|
||||||
|
|
||||||
# Generic nested field extraction - tries flat keys first, then nested object
|
# Specific field extractors using the generic function
|
||||||
@spec extract_nested_field(map(), list(String.t()), String.t(), String.t()) :: String.t() | nil
|
|
||||||
defp extract_nested_field(data, flat_keys, nested_key, field) when is_map(data) do
|
|
||||||
case extract_field(data, flat_keys) do
|
|
||||||
nil ->
|
|
||||||
case data[nested_key] do
|
|
||||||
%{^field => value} when is_binary(value) and value != "" -> value
|
|
||||||
_ -> nil
|
|
||||||
end
|
|
||||||
|
|
||||||
value ->
|
|
||||||
value
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Specific field extractors using the generic functions
|
|
||||||
@spec get_character_name(map() | any()) :: String.t() | nil
|
@spec get_character_name(map() | any()) :: String.t() | nil
|
||||||
defp get_character_name(data) when is_map(data) do
|
defp get_character_name(data) when is_map(data) do
|
||||||
|
# Try multiple possible field names
|
||||||
field_names = ["attacker_name", "victim_name", "character_name", "name"]
|
field_names = ["attacker_name", "victim_name", "character_name", "name"]
|
||||||
|
|
||||||
extract_field(data, field_names) ||
|
extract_field(data, field_names) ||
|
||||||
@@ -433,26 +419,30 @@ defmodule WandererApp.Kills.MessageHandler do
|
|||||||
defp get_character_name(_), do: nil
|
defp get_character_name(_), do: nil
|
||||||
|
|
||||||
@spec get_corp_ticker(map() | any()) :: String.t() | nil
|
@spec get_corp_ticker(map() | any()) :: String.t() | nil
|
||||||
defp get_corp_ticker(data) when is_map(data),
|
defp get_corp_ticker(data) when is_map(data) do
|
||||||
do: extract_nested_field(data, ["corporation_ticker", "corp_ticker"], "corporation", "ticker")
|
extract_field(data, ["corporation_ticker", "corp_ticker"])
|
||||||
|
end
|
||||||
|
|
||||||
defp get_corp_ticker(_), do: nil
|
defp get_corp_ticker(_), do: nil
|
||||||
|
|
||||||
@spec get_corp_name(map() | any()) :: String.t() | nil
|
@spec get_corp_name(map() | any()) :: String.t() | nil
|
||||||
defp get_corp_name(data) when is_map(data),
|
defp get_corp_name(data) when is_map(data) do
|
||||||
do: extract_nested_field(data, ["corporation_name", "corp_name"], "corporation", "name")
|
extract_field(data, ["corporation_name", "corp_name"])
|
||||||
|
end
|
||||||
|
|
||||||
defp get_corp_name(_), do: nil
|
defp get_corp_name(_), do: nil
|
||||||
|
|
||||||
@spec get_alliance_ticker(map() | any()) :: String.t() | nil
|
@spec get_alliance_ticker(map() | any()) :: String.t() | nil
|
||||||
defp get_alliance_ticker(data) when is_map(data),
|
defp get_alliance_ticker(data) when is_map(data) do
|
||||||
do: extract_nested_field(data, ["alliance_ticker"], "alliance", "ticker")
|
extract_field(data, ["alliance_ticker"])
|
||||||
|
end
|
||||||
|
|
||||||
defp get_alliance_ticker(_), do: nil
|
defp get_alliance_ticker(_), do: nil
|
||||||
|
|
||||||
@spec get_alliance_name(map() | any()) :: String.t() | nil
|
@spec get_alliance_name(map() | any()) :: String.t() | nil
|
||||||
defp get_alliance_name(data) when is_map(data),
|
defp get_alliance_name(data) when is_map(data) do
|
||||||
do: extract_nested_field(data, ["alliance_name"], "alliance", "name")
|
extract_field(data, ["alliance_name"])
|
||||||
|
end
|
||||||
|
|
||||||
defp get_alliance_name(_), do: nil
|
defp get_alliance_name(_), do: nil
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,10 @@ defmodule WandererApp.Map do
|
|||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
@map_state_cache :map_state_cache
|
@map_state_cache :map_state_cache
|
||||||
# Default plan indicates no active subscription (free tier)
|
|
||||||
@default_subscription_plan :alpha
|
|
||||||
|
|
||||||
defstruct map_id: nil,
|
defstruct map_id: nil,
|
||||||
name: nil,
|
name: nil,
|
||||||
scope: :none,
|
scope: :none,
|
||||||
scopes: nil,
|
|
||||||
owner_id: nil,
|
owner_id: nil,
|
||||||
characters: [],
|
characters: [],
|
||||||
systems: Map.new(),
|
systems: Map.new(),
|
||||||
@@ -23,32 +20,17 @@ defmodule WandererApp.Map do
|
|||||||
acls: [],
|
acls: [],
|
||||||
options: Map.new(),
|
options: Map.new(),
|
||||||
characters_limit: nil,
|
characters_limit: nil,
|
||||||
hubs_limit: nil,
|
hubs_limit: nil
|
||||||
sse_enabled: false,
|
|
||||||
webhooks_enabled: false,
|
|
||||||
subscription_plan: @default_subscription_plan
|
|
||||||
|
|
||||||
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)
|
|
||||||
# Extract SSE/webhooks settings (default to false if not present)
|
|
||||||
sse_enabled = Map.get(input, :sse_enabled, false)
|
|
||||||
webhooks_enabled = Map.get(input, :webhooks_enabled, false)
|
|
||||||
|
|
||||||
|
def new(%{id: map_id, name: name, scope: scope, owner_id: owner_id, acls: acls, hubs: hubs}) do
|
||||||
map =
|
map =
|
||||||
struct!(__MODULE__,
|
struct!(__MODULE__,
|
||||||
map_id: map_id,
|
map_id: map_id,
|
||||||
scope: scope,
|
scope: scope,
|
||||||
scopes: scopes,
|
|
||||||
owner_id: owner_id,
|
owner_id: owner_id,
|
||||||
name: name,
|
name: name,
|
||||||
acls: acls,
|
acls: acls,
|
||||||
hubs: hubs,
|
hubs: hubs
|
||||||
sse_enabled: sse_enabled,
|
|
||||||
webhooks_enabled: webhooks_enabled
|
|
||||||
)
|
)
|
||||||
|
|
||||||
update_map(map_id, map)
|
update_map(map_id, map)
|
||||||
@@ -146,7 +128,7 @@ defmodule WandererApp.Map do
|
|||||||
|
|
||||||
def is_subscription_active?(map_id, _map_subscriptions_enabled) do
|
def is_subscription_active?(map_id, _map_subscriptions_enabled) do
|
||||||
{:ok, %{plan: plan}} = WandererApp.Map.SubscriptionManager.get_active_map_subscription(map_id)
|
{:ok, %{plan: plan}} = WandererApp.Map.SubscriptionManager.get_active_map_subscription(map_id)
|
||||||
{:ok, plan != @default_subscription_plan}
|
{:ok, plan != :alpha}
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_options(map_id),
|
def get_options(map_id),
|
||||||
@@ -195,7 +177,7 @@ defmodule WandererApp.Map do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def list_hubs(map_id, hubs) do
|
def list_hubs(map_id, hubs) do
|
||||||
{:ok, _map} = map_id |> get_map()
|
{:ok, map} = map_id |> get_map()
|
||||||
|
|
||||||
{:ok, hubs}
|
{:ok, hubs}
|
||||||
end
|
end
|
||||||
@@ -223,7 +205,7 @@ defmodule WandererApp.Map do
|
|||||||
|
|
||||||
characters_ids =
|
characters_ids =
|
||||||
characters
|
characters
|
||||||
|> Enum.map(fn %{character_id: char_id} -> char_id end)
|
|> Enum.map(fn %{id: char_id} -> char_id end)
|
||||||
|
|
||||||
# Filter out characters that already exist
|
# Filter out characters that already exist
|
||||||
new_character_ids =
|
new_character_ids =
|
||||||
@@ -333,23 +315,18 @@ defmodule WandererApp.Map do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_subscription_settings!(%{map_id: map_id} = _map, subscription_settings) do
|
def update_subscription_settings!(%{map_id: map_id} = map, %{
|
||||||
characters_limit = Map.get(subscription_settings, :characters_limit)
|
characters_limit: characters_limit,
|
||||||
hubs_limit = Map.get(subscription_settings, :hubs_limit)
|
hubs_limit: hubs_limit
|
||||||
plan = Map.get(subscription_settings, :plan, @default_subscription_plan)
|
}) do
|
||||||
|
|
||||||
map_id
|
map_id
|
||||||
|> update_map(%{
|
|> update_map(%{characters_limit: characters_limit, hubs_limit: hubs_limit})
|
||||||
characters_limit: characters_limit,
|
|
||||||
hubs_limit: hubs_limit,
|
|
||||||
subscription_plan: plan
|
|
||||||
})
|
|
||||||
|
|
||||||
map_id
|
map_id
|
||||||
|> get_map!()
|
|> get_map!()
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_options!(%{map_id: map_id} = _map, options) do
|
def update_options!(%{map_id: map_id} = map, options) do
|
||||||
map_id
|
map_id
|
||||||
|> update_map(%{options: options})
|
|> update_map(%{options: options})
|
||||||
|
|
||||||
@@ -357,99 +334,6 @@ defmodule WandererApp.Map do
|
|||||||
|> get_map!()
|
|> get_map!()
|
||||||
end
|
end
|
||||||
|
|
||||||
@doc """
|
|
||||||
Updates SSE enabled setting in the map cache.
|
|
||||||
Called when the map's sse_enabled setting changes.
|
|
||||||
"""
|
|
||||||
def update_sse_enabled(map_id, sse_enabled)
|
|
||||||
when is_binary(map_id) and is_boolean(sse_enabled) do
|
|
||||||
update_map(map_id, %{sse_enabled: sse_enabled})
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Updates webhooks enabled setting in the map cache.
|
|
||||||
Called when the map's webhooks_enabled setting changes.
|
|
||||||
"""
|
|
||||||
def update_webhooks_enabled(map_id, webhooks_enabled)
|
|
||||||
when is_binary(map_id) and is_boolean(webhooks_enabled) do
|
|
||||||
update_map(map_id, %{webhooks_enabled: webhooks_enabled})
|
|
||||||
:ok
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks if SSE is enabled for a map using the cache.
|
|
||||||
Falls back to DB query if map is not in cache.
|
|
||||||
Returns a boolean (defaults to false if map not found).
|
|
||||||
"""
|
|
||||||
def sse_enabled?(map_id) do
|
|
||||||
case get_map(map_id) do
|
|
||||||
{:ok, map} ->
|
|
||||||
Map.get(map, :sse_enabled, false)
|
|
||||||
|
|
||||||
{:error, :not_found} ->
|
|
||||||
# Cache miss - fall back to DB
|
|
||||||
case WandererApp.Api.Map.by_id(map_id) do
|
|
||||||
{:ok, db_map} -> db_map.sse_enabled
|
|
||||||
_ -> false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks if SSE is enabled for a map with explicit not_found handling.
|
|
||||||
Returns {:ok, boolean} or {:error, :not_found}.
|
|
||||||
"""
|
|
||||||
def sse_enabled_with_status(map_id) do
|
|
||||||
case get_map(map_id) do
|
|
||||||
{:ok, map} ->
|
|
||||||
{:ok, Map.get(map, :sse_enabled, false)}
|
|
||||||
|
|
||||||
{:error, :not_found} ->
|
|
||||||
# Cache miss - fall back to DB
|
|
||||||
case WandererApp.Api.Map.by_id(map_id) do
|
|
||||||
{:ok, db_map} -> {:ok, db_map.sse_enabled}
|
|
||||||
_ -> {:error, :not_found}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks if webhooks are enabled for a map using the cache.
|
|
||||||
Falls back to DB query if map is not in cache.
|
|
||||||
"""
|
|
||||||
def webhooks_enabled?(map_id) do
|
|
||||||
case get_map(map_id) do
|
|
||||||
{:ok, map} ->
|
|
||||||
Map.get(map, :webhooks_enabled, false)
|
|
||||||
|
|
||||||
{:error, :not_found} ->
|
|
||||||
# Cache miss - fall back to DB
|
|
||||||
case WandererApp.Api.Map.by_id(map_id) do
|
|
||||||
{:ok, db_map} -> db_map.webhooks_enabled
|
|
||||||
_ -> false
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
@doc """
|
|
||||||
Checks if subscription is active for a map using the cache.
|
|
||||||
Returns {:ok, true} if active, {:ok, false} if not, or {:error, :not_cached} if not in cache.
|
|
||||||
|
|
||||||
Note: In CE mode (subscriptions disabled), use is_subscription_active?/1 which
|
|
||||||
handles this case without cache lookup.
|
|
||||||
"""
|
|
||||||
def subscription_active_cached?(map_id) do
|
|
||||||
case get_map(map_id) do
|
|
||||||
{:ok, map} ->
|
|
||||||
plan = Map.get(map, :subscription_plan, @default_subscription_plan)
|
|
||||||
{:ok, plan != @default_subscription_plan}
|
|
||||||
|
|
||||||
_ ->
|
|
||||||
{:error, :not_cached}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_systems!(map, []), do: map
|
def add_systems!(map, []), do: map
|
||||||
|
|
||||||
def add_systems!(%{map_id: map_id} = map, [system | rest]) do
|
def add_systems!(%{map_id: map_id} = map, [system | rest]) do
|
||||||
|
|||||||
@@ -348,9 +348,9 @@ defmodule WandererApp.Map.CacheRTree do
|
|||||||
[{x1_min, x1_max}, {y1_min, y1_max}] = box1
|
[{x1_min, x1_max}, {y1_min, y1_max}] = box1
|
||||||
[{x2_min, x2_max}, {y2_min, y2_max}] = box2
|
[{x2_min, x2_max}, {y2_min, y2_max}] = box2
|
||||||
|
|
||||||
# Boxes intersect if they overlap on both axes (strict intersection - not just touching)
|
# Boxes intersect if they overlap on both axes
|
||||||
x_overlap = x1_min < x2_max and x2_min < x1_max
|
x_overlap = x1_min <= x2_max and x2_min <= x1_max
|
||||||
y_overlap = y1_min < y2_max and y2_min < y1_max
|
y_overlap = y1_min <= y2_max and y2_min <= y1_max
|
||||||
|
|
||||||
x_overlap and y_overlap
|
x_overlap and y_overlap
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,19 +9,17 @@ defmodule WandererApp.Map.Manager do
|
|||||||
|
|
||||||
alias WandererApp.Map.Server
|
alias WandererApp.Map.Server
|
||||||
|
|
||||||
@environment Application.compile_env(:wanderer_app, :environment)
|
|
||||||
|
|
||||||
@maps_start_chunk_size 20
|
@maps_start_chunk_size 20
|
||||||
@maps_start_interval 500
|
@maps_start_interval 500
|
||||||
@maps_queue :maps_queue
|
@maps_queue :maps_queue
|
||||||
@check_maps_queue_interval :timer.seconds(1)
|
@check_maps_queue_interval :timer.seconds(1)
|
||||||
|
|
||||||
@pings_cleanup_interval :timer.minutes(5)
|
@pings_cleanup_interval :timer.minutes(10)
|
||||||
@pings_expire_minutes 60
|
@pings_expire_minutes 60
|
||||||
|
|
||||||
# Test-aware async task runner
|
# Test-aware async task runner
|
||||||
defp safe_async_task(fun) do
|
defp safe_async_task(fun) do
|
||||||
if @environment == :test do
|
if Mix.env() == :test do
|
||||||
# In tests, run synchronously to avoid database ownership issues
|
# In tests, run synchronously to avoid database ownership issues
|
||||||
try do
|
try do
|
||||||
fun.()
|
fun.()
|
||||||
@@ -99,7 +97,6 @@ defmodule WandererApp.Map.Manager do
|
|||||||
def handle_info(:cleanup_pings, state) do
|
def handle_info(:cleanup_pings, state) do
|
||||||
try do
|
try do
|
||||||
cleanup_expired_pings()
|
cleanup_expired_pings()
|
||||||
cleanup_orphaned_pings()
|
|
||||||
{:noreply, state}
|
{:noreply, state}
|
||||||
rescue
|
rescue
|
||||||
e ->
|
e ->
|
||||||
@@ -116,20 +113,11 @@ defmodule WandererApp.Map.Manager do
|
|||||||
Enum.each(pings, fn %{id: ping_id, map_id: map_id, type: type} = ping ->
|
Enum.each(pings, fn %{id: ping_id, map_id: map_id, type: type} = ping ->
|
||||||
{:ok, %{system: system}} = ping |> Ash.load([:system])
|
{:ok, %{system: system}} = ping |> Ash.load([:system])
|
||||||
|
|
||||||
# Handle case where parent system was already deleted
|
Server.Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||||
case system do
|
id: ping_id,
|
||||||
nil ->
|
solar_system_id: system.solar_system_id,
|
||||||
Logger.warning(
|
type: type
|
||||||
"[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)
|
Ash.destroy!(ping)
|
||||||
end)
|
end)
|
||||||
@@ -142,55 +130,6 @@ defmodule WandererApp.Map.Manager do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp cleanup_orphaned_pings() do
|
|
||||||
case WandererApp.MapPingsRepo.get_orphaned_pings() do
|
|
||||||
{:ok, []} ->
|
|
||||||
:ok
|
|
||||||
|
|
||||||
{:ok, orphaned_pings} ->
|
|
||||||
Logger.info(
|
|
||||||
"[cleanup_orphaned_pings] Found #{length(orphaned_pings)} orphaned pings, cleaning up..."
|
|
||||||
)
|
|
||||||
|
|
||||||
Enum.each(orphaned_pings, fn %{id: ping_id, map_id: map_id, type: type, system: system} =
|
|
||||||
ping ->
|
|
||||||
reason =
|
|
||||||
cond do
|
|
||||||
is_nil(ping.system) -> "system deleted"
|
|
||||||
is_nil(ping.character) -> "character deleted"
|
|
||||||
is_nil(ping.map) -> "map deleted"
|
|
||||||
not is_nil(system) and system.visible == false -> "system hidden (visible=false)"
|
|
||||||
true -> "unknown"
|
|
||||||
end
|
|
||||||
|
|
||||||
Logger.warning(
|
|
||||||
"[cleanup_orphaned_pings] Destroying orphaned ping #{ping_id} (map_id: #{map_id}, reason: #{reason})"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Broadcast cancellation if map_id is still valid
|
|
||||||
if map_id do
|
|
||||||
Server.Impl.broadcast!(map_id, :ping_cancelled, %{
|
|
||||||
id: ping_id,
|
|
||||||
solar_system_id: nil,
|
|
||||||
type: type
|
|
||||||
})
|
|
||||||
end
|
|
||||||
|
|
||||||
Ash.destroy!(ping)
|
|
||||||
end)
|
|
||||||
|
|
||||||
Logger.info(
|
|
||||||
"[cleanup_orphaned_pings] Cleaned up #{length(orphaned_pings)} orphaned pings"
|
|
||||||
)
|
|
||||||
|
|
||||||
:ok
|
|
||||||
|
|
||||||
{:error, error} ->
|
|
||||||
Logger.error("Failed to fetch orphaned pings: #{inspect(error)}")
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
defp start_maps() do
|
defp start_maps() do
|
||||||
chunks =
|
chunks =
|
||||||
@maps_queue
|
@maps_queue
|
||||||
@@ -200,7 +139,7 @@ defmodule WandererApp.Map.Manager do
|
|||||||
|
|
||||||
WandererApp.Queue.clear(@maps_queue)
|
WandererApp.Queue.clear(@maps_queue)
|
||||||
|
|
||||||
if @environment == :test do
|
if Mix.env() == :test do
|
||||||
# In tests, run synchronously to avoid database ownership issues
|
# In tests, run synchronously to avoid database ownership issues
|
||||||
Logger.debug(fn -> "Starting maps synchronously in test mode" end)
|
Logger.debug(fn -> "Starting maps synchronously in test mode" end)
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,11 @@ defmodule WandererApp.Map.Operations do
|
|||||||
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
||||||
defdelegate create_connection(map_id, attrs, char_id), to: Connections
|
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"
|
@doc "Update a connection"
|
||||||
@spec update_connection(String.t(), String.t(), map()) ::
|
@spec update_connection(String.t(), String.t(), map()) ::
|
||||||
{:ok, map()} | {:error, String.t()}
|
{:ok, map()} | {:error, String.t()}
|
||||||
@@ -126,12 +131,4 @@ defmodule WandererApp.Map.Operations do
|
|||||||
@doc "Delete a signature in a map"
|
@doc "Delete a signature in a map"
|
||||||
@spec delete_signature(String.t(), String.t()) :: :ok | {:error, String.t()}
|
@spec delete_signature(String.t(), String.t()) :: :ok | {:error, String.t()}
|
||||||
defdelegate delete_signature(map_id, sig_id), to: Signatures
|
defdelegate delete_signature(map_id, sig_id), to: Signatures
|
||||||
|
|
||||||
@doc "Link a signature to a target system"
|
|
||||||
@spec link_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
|
||||||
defdelegate link_signature(conn, sig_id, params), to: Signatures
|
|
||||||
|
|
||||||
@doc "Unlink a signature from its target system"
|
|
||||||
@spec unlink_signature(Plug.Conn.t(), String.t()) :: {:ok, map()} | {:error, atom()}
|
|
||||||
defdelegate unlink_signature(conn, sig_id), to: Signatures
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,22 +18,10 @@ defmodule WandererApp.Map.MapPool do
|
|||||||
@map_pool_limit 10
|
@map_pool_limit 10
|
||||||
|
|
||||||
@garbage_collection_interval :timer.hours(4)
|
@garbage_collection_interval :timer.hours(4)
|
||||||
# Use very long timeouts in test environment to prevent background tasks from running during tests
|
@systems_cleanup_timeout :timer.minutes(30)
|
||||||
# This avoids database connection ownership errors when tests finish before async tasks complete
|
@characters_cleanup_timeout :timer.minutes(5)
|
||||||
@environment Application.compile_env(:wanderer_app, :environment)
|
@connections_cleanup_timeout :timer.minutes(5)
|
||||||
|
@backup_state_timeout :timer.minutes(1)
|
||||||
@systems_cleanup_timeout if @environment == :test,
|
|
||||||
do: :timer.hours(24),
|
|
||||||
else: :timer.minutes(30)
|
|
||||||
@characters_cleanup_timeout if @environment == :test,
|
|
||||||
do: :timer.hours(24),
|
|
||||||
else: :timer.minutes(5)
|
|
||||||
@connections_cleanup_timeout if @environment == :test,
|
|
||||||
do: :timer.hours(24),
|
|
||||||
else: :timer.minutes(5)
|
|
||||||
@backup_state_timeout if @environment == :test,
|
|
||||||
do: :timer.hours(24),
|
|
||||||
else: :timer.minutes(1)
|
|
||||||
|
|
||||||
def new(), do: __struct__()
|
def new(), do: __struct__()
|
||||||
def new(args), do: __struct__(args)
|
def new(args), do: __struct__(args)
|
||||||
@@ -199,7 +187,7 @@ defmodule WandererApp.Map.MapPool do
|
|||||||
|
|
||||||
# Schedule periodic tasks
|
# Schedule periodic tasks
|
||||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||||
Process.send_after(self(), :cleanup_systems, @systems_cleanup_timeout)
|
Process.send_after(self(), :cleanup_systems, 15_000)
|
||||||
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
|
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
|
||||||
Process.send_after(self(), :cleanup_connections, @connections_cleanup_timeout)
|
Process.send_after(self(), :cleanup_connections, @connections_cleanup_timeout)
|
||||||
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
||||||
@@ -329,9 +317,6 @@ defmodule WandererApp.Map.MapPool do
|
|||||||
end
|
end
|
||||||
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
|
defp do_start_map(map_id, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||||
if map_id in map_ids do
|
if map_id in map_ids do
|
||||||
# Map already started
|
# Map already started
|
||||||
@@ -347,6 +332,8 @@ defmodule WandererApp.Map.MapPool do
|
|||||||
[map_id | r_map_ids]
|
[map_id | r_map_ids]
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
completed_operations = [:registry | completed_operations]
|
||||||
|
|
||||||
case registry_result do
|
case registry_result do
|
||||||
{new_value, _old_value} when is_list(new_value) ->
|
{new_value, _old_value} when is_list(new_value) ->
|
||||||
:ok
|
:ok
|
||||||
@@ -364,9 +351,13 @@ defmodule WandererApp.Map.MapPool do
|
|||||||
raise "Failed to add to cache: #{inspect(reason)}"
|
raise "Failed to add to cache: #{inspect(reason)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
completed_operations = [:cache | completed_operations]
|
||||||
|
|
||||||
# Step 3: Start the map server using extracted helper
|
# Step 3: Start the map server using extracted helper
|
||||||
do_initialize_map_server(map_id)
|
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)
|
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
||||||
new_state = %{state | map_ids: [map_id | map_ids]}
|
new_state = %{state | map_ids: [map_id | map_ids]}
|
||||||
|
|
||||||
@@ -442,6 +433,8 @@ defmodule WandererApp.Map.MapPool do
|
|||||||
r_map_ids |> Enum.reject(fn id -> id == map_id end)
|
r_map_ids |> Enum.reject(fn id -> id == map_id end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
completed_operations = [:registry | completed_operations]
|
||||||
|
|
||||||
case registry_result do
|
case registry_result do
|
||||||
{new_value, _old_value} when is_list(new_value) ->
|
{new_value, _old_value} when is_list(new_value) ->
|
||||||
:ok
|
:ok
|
||||||
@@ -459,10 +452,14 @@ defmodule WandererApp.Map.MapPool do
|
|||||||
raise "Failed to delete from cache: #{inspect(reason)}"
|
raise "Failed to delete from cache: #{inspect(reason)}"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
completed_operations = [:cache | completed_operations]
|
||||||
|
|
||||||
# Step 3: Stop the map server (clean up all map resources)
|
# Step 3: Stop the map server (clean up all map resources)
|
||||||
map_id
|
map_id
|
||||||
|> Server.Impl.stop_map()
|
|> Server.Impl.stop_map()
|
||||||
|
|
||||||
|
completed_operations = [:map_server | completed_operations]
|
||||||
|
|
||||||
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
# 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)}
|
new_state = %{state | map_ids: map_ids |> Enum.reject(fn id -> id == map_id end)}
|
||||||
|
|
||||||
@@ -551,6 +548,9 @@ defmodule WandererApp.Map.MapPool do
|
|||||||
# and the cleanup operations are safe to leave in a "stopped" state
|
# and the cleanup operations are safe to leave in a "stopped" state
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@impl true
|
||||||
|
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_info(:backup_state, %{map_ids: map_ids, uuid: uuid} = state) do
|
def handle_info(:backup_state, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||||
|
|||||||
@@ -179,4 +179,15 @@ defmodule WandererApp.Map.MapPoolDynamicSupervisor do
|
|||||||
{:ok, pid}
|
{:ok, pid}
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|||||||
@@ -106,9 +106,6 @@ defmodule WandererApp.Map.PositionCalculator do
|
|||||||
|
|
||||||
defp get_start_index(n, "top_to_bottom"), do: div(n, 2) + n - 1
|
defp get_start_index(n, "top_to_bottom"), do: div(n, 2) + n - 1
|
||||||
|
|
||||||
# Default to left_to_right when layout is nil
|
|
||||||
defp get_start_index(n, nil), do: div(n, 2)
|
|
||||||
|
|
||||||
defp adjusted_coordinates(n, start_x, start_y, opts) when n > 1 do
|
defp adjusted_coordinates(n, start_x, start_y, opts) when n > 1 do
|
||||||
sorted_coords = sorted_edge_coordinates(n, opts)
|
sorted_coords = sorted_edge_coordinates(n, opts)
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ defmodule WandererApp.Map.Routes do
|
|||||||
end
|
end
|
||||||
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()
|
origin = origin |> String.to_integer()
|
||||||
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
|
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user