mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-08 00:35:53 +00:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d313ae8cd2 | ||
|
|
06d5d8072e | ||
|
|
cae958a1e6 | ||
|
|
283b36c882 | ||
|
|
051e71f1a6 | ||
|
|
20a50e8db0 | ||
|
|
79d7f7ce7d | ||
|
|
6c4b65c446 | ||
|
|
2b07af5e12 | ||
|
|
d0901eecb4 | ||
|
|
ee85d29c54 | ||
|
|
a237d6513d | ||
|
|
02979588c1 | ||
|
|
3abe40855f | ||
|
|
d0d9418a89 | ||
|
|
3ce742eb01 | ||
|
|
ae566fb907 | ||
|
|
fa32c62f63 | ||
|
|
6880be11c5 | ||
|
|
5289893264 | ||
|
|
f15370a3df | ||
|
|
cfac867c0a | ||
|
|
f50ea40b15 | ||
|
|
04b2d57081 | ||
|
|
b235ea52e0 | ||
|
|
2cb2dc526c | ||
|
|
f3c38ba62a | ||
|
|
29473f2d3b | ||
|
|
48654250e8 | ||
|
|
7aa24245b6 | ||
|
|
6070d74684 | ||
|
|
c3de3c4e35 | ||
|
|
5c513f3e50 | ||
|
|
5a980c6b89 | ||
|
|
85c075c5a6 | ||
|
|
f068afd16e | ||
|
|
ac71b0af64 | ||
|
|
5c515d6acd | ||
|
|
4585c3a94b | ||
|
|
cf2c27c961 | ||
|
|
f8e403025c | ||
|
|
46a1898be9 | ||
|
|
25fa7c07bc | ||
|
|
e7219e0eec | ||
|
|
45130fcffa | ||
|
|
5f75d4440d | ||
|
|
34210f63e3 | ||
|
|
5f60fd4922 | ||
|
|
47ef7dda55 | ||
|
|
0f3550a687 | ||
|
|
8f242f3535 | ||
|
|
1ce39e5394 | ||
|
|
cca7b912aa | ||
|
|
be7bbe6872 |
110
.github/workflows/test.yml
vendored
110
.github/workflows/test.yml
vendored
@@ -19,19 +19,15 @@ env:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test Suite (Partition ${{ matrix.partition }})
|
||||
name: Test Suite
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
partition: [1, 2, 3, 4]
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: wanderer_test${{ matrix.partition }}
|
||||
POSTGRES_DB: wanderer_test
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
@@ -43,13 +39,13 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
- name: Setup Elixir/OTP
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
elixir-version: ${{ env.ELIXIR_VERSION }}
|
||||
otp-version: ${{ env.OTP_VERSION }}
|
||||
|
||||
|
||||
- name: Cache Elixir dependencies
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
@@ -58,12 +54,12 @@ jobs:
|
||||
_build
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: ${{ runner.os }}-mix-
|
||||
|
||||
|
||||
- name: Install Elixir dependencies
|
||||
run: |
|
||||
mix deps.get
|
||||
mix deps.compile
|
||||
|
||||
|
||||
- name: Check code formatting
|
||||
id: format
|
||||
run: |
|
||||
@@ -75,42 +71,40 @@ jobs:
|
||||
echo "count=1" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Compile code and capture warnings
|
||||
id: compile
|
||||
run: |
|
||||
# Capture compilation output
|
||||
output=$(mix compile 2>&1 || true)
|
||||
echo "$output" > compile_output.txt
|
||||
|
||||
|
||||
# Count warnings
|
||||
warning_count=$(echo "$output" | grep -c "warning:" || echo "0")
|
||||
|
||||
|
||||
# Check if compilation succeeded
|
||||
if mix compile > /dev/null 2>&1; then
|
||||
echo "status=✅ Success" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "status=❌ Failed" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
|
||||
echo "warnings=$warning_count" >> $GITHUB_OUTPUT
|
||||
echo "output<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$output" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Setup database
|
||||
run: |
|
||||
mix ecto.create
|
||||
mix ecto.migrate
|
||||
|
||||
|
||||
- name: Run tests with coverage
|
||||
id: tests
|
||||
env:
|
||||
MIX_TEST_PARTITION: ${{ matrix.partition }}
|
||||
run: |
|
||||
# Run tests with coverage using partitioning
|
||||
output=$(mix test --cover --partitions 4 2>&1 || true)
|
||||
# Run tests with coverage
|
||||
output=$(mix test --cover 2>&1 || true)
|
||||
echo "$output" > test_output.txt
|
||||
|
||||
# Parse test results
|
||||
@@ -142,22 +136,22 @@ jobs:
|
||||
exit_code=$?
|
||||
echo "exit_code=$exit_code" >> $GITHUB_OUTPUT
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Generate coverage report
|
||||
id: coverage
|
||||
run: |
|
||||
# Generate coverage report with GitHub format
|
||||
output=$(mix coveralls.github 2>&1 || true)
|
||||
echo "$output" > coverage_output.txt
|
||||
|
||||
|
||||
# Extract coverage percentage
|
||||
coverage=$(echo "$output" | grep -o '[0-9]\+\.[0-9]\+%' | head -1 | sed 's/%//' || echo "0")
|
||||
if [ -z "$coverage" ]; then
|
||||
coverage="0"
|
||||
fi
|
||||
|
||||
|
||||
echo "percentage=$coverage" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Determine status
|
||||
if (( $(echo "$coverage >= 80" | bc -l) )); then
|
||||
echo "status=✅ Excellent" >> $GITHUB_OUTPUT
|
||||
@@ -167,14 +161,14 @@ jobs:
|
||||
echo "status=❌ Needs Improvement" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Run Credo analysis
|
||||
id: credo
|
||||
run: |
|
||||
# Run Credo and capture output
|
||||
output=$(mix credo --strict --format=json 2>&1 || true)
|
||||
echo "$output" > credo_output.txt
|
||||
|
||||
|
||||
# Try to parse JSON output
|
||||
if echo "$output" | jq . > /dev/null 2>&1; then
|
||||
issues=$(echo "$output" | jq '.issues | length' 2>/dev/null || echo "0")
|
||||
@@ -189,12 +183,12 @@ jobs:
|
||||
normal_issues="0"
|
||||
low_issues="0"
|
||||
fi
|
||||
|
||||
|
||||
echo "total_issues=$issues" >> $GITHUB_OUTPUT
|
||||
echo "high_issues=$high_issues" >> $GITHUB_OUTPUT
|
||||
echo "normal_issues=$normal_issues" >> $GITHUB_OUTPUT
|
||||
echo "low_issues=$low_issues" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Determine status
|
||||
if [ "$issues" -eq 0 ]; then
|
||||
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
||||
@@ -204,24 +198,24 @@ jobs:
|
||||
echo "status=❌ Needs Attention" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Run Dialyzer analysis
|
||||
id: dialyzer
|
||||
run: |
|
||||
# Ensure PLT is built
|
||||
mix dialyzer --plt
|
||||
|
||||
|
||||
# Run Dialyzer and capture output
|
||||
output=$(mix dialyzer --format=github 2>&1 || true)
|
||||
echo "$output" > dialyzer_output.txt
|
||||
|
||||
|
||||
# Count warnings and errors
|
||||
warnings=$(echo "$output" | grep -c "warning:" || echo "0")
|
||||
errors=$(echo "$output" | grep -c "error:" || echo "0")
|
||||
|
||||
|
||||
echo "warnings=$warnings" >> $GITHUB_OUTPUT
|
||||
echo "errors=$errors" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Determine status
|
||||
if [ "$errors" -eq 0 ] && [ "$warnings" -eq 0 ]; then
|
||||
echo "status=✅ Clean" >> $GITHUB_OUTPUT
|
||||
@@ -231,7 +225,7 @@ jobs:
|
||||
echo "status=❌ Has Errors" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Create test results summary
|
||||
id: summary
|
||||
run: |
|
||||
@@ -242,11 +236,11 @@ jobs:
|
||||
coverage_score=${{ steps.coverage.outputs.percentage }}
|
||||
credo_score=$(echo "scale=0; (100 - ${{ steps.credo.outputs.total_issues }} * 2)" | bc | sed 's/^-.*$/0/')
|
||||
dialyzer_score=$(echo "scale=0; (100 - ${{ steps.dialyzer.outputs.warnings }} * 2 - ${{ steps.dialyzer.outputs.errors }} * 10)" | bc | sed 's/^-.*$/0/')
|
||||
|
||||
|
||||
overall_score=$(echo "scale=1; ($format_score + $compile_score + $test_score + $coverage_score + $credo_score + $dialyzer_score) / 6" | bc)
|
||||
|
||||
|
||||
echo "overall_score=$overall_score" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
# Determine overall status
|
||||
if (( $(echo "$overall_score >= 90" | bc -l) )); then
|
||||
echo "overall_status=🌟 Excellent" >> $GITHUB_OUTPUT
|
||||
@@ -258,7 +252,7 @@ jobs:
|
||||
echo "overall_status=❌ Poor" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Find existing PR comment
|
||||
if: github.event_name == 'pull_request'
|
||||
id: find_comment
|
||||
@@ -267,7 +261,7 @@ jobs:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
comment-author: 'github-actions[bot]'
|
||||
body-includes: '## 🧪 Test Results Summary'
|
||||
|
||||
|
||||
- name: Create or update PR comment
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
@@ -277,11 +271,11 @@ jobs:
|
||||
edit-mode: replace
|
||||
body: |
|
||||
## 🧪 Test Results Summary
|
||||
|
||||
|
||||
**Overall Quality Score: ${{ steps.summary.outputs.overall_score }}%** ${{ steps.summary.outputs.overall_status }}
|
||||
|
||||
|
||||
### 📊 Metrics Dashboard
|
||||
|
||||
|
||||
| Category | Status | Count | Details |
|
||||
|----------|---------|-------|---------|
|
||||
| 📝 **Code Formatting** | ${{ steps.format.outputs.status }} | ${{ steps.format.outputs.count }} issues | `mix format --check-formatted` |
|
||||
@@ -290,50 +284,50 @@ jobs:
|
||||
| 📊 **Coverage** | ${{ steps.coverage.outputs.status }} | ${{ steps.coverage.outputs.percentage }}% | `mix coveralls` |
|
||||
| 🎯 **Credo** | ${{ steps.credo.outputs.status }} | ${{ steps.credo.outputs.total_issues }} issues | High: ${{ steps.credo.outputs.high_issues }}, Normal: ${{ steps.credo.outputs.normal_issues }}, Low: ${{ steps.credo.outputs.low_issues }} |
|
||||
| 🔍 **Dialyzer** | ${{ steps.dialyzer.outputs.status }} | ${{ steps.dialyzer.outputs.errors }} errors, ${{ steps.dialyzer.outputs.warnings }} warnings | `mix dialyzer` |
|
||||
|
||||
|
||||
### 🎯 Quality Gates
|
||||
|
||||
|
||||
Based on the project's quality thresholds:
|
||||
- **Compilation Warnings**: ${{ steps.compile.outputs.warnings }}/148 (limit: 148)
|
||||
- **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87)
|
||||
- **Credo Issues**: ${{ steps.credo.outputs.total_issues }}/87 (limit: 87)
|
||||
- **Dialyzer Warnings**: ${{ steps.dialyzer.outputs.warnings }}/161 (limit: 161)
|
||||
- **Test Coverage**: ${{ steps.coverage.outputs.percentage }}%/50% (minimum: 50%)
|
||||
- **Test Failures**: ${{ steps.tests.outputs.failures }}/0 (limit: 0)
|
||||
|
||||
|
||||
<details>
|
||||
<summary>📈 Progress Toward Goals</summary>
|
||||
|
||||
|
||||
Target goals for the project:
|
||||
- ✨ **Zero compilation warnings** (currently: ${{ steps.compile.outputs.warnings }})
|
||||
- ✨ **≤10 Credo issues** (currently: ${{ steps.credo.outputs.total_issues }})
|
||||
- ✨ **Zero Dialyzer warnings** (currently: ${{ steps.dialyzer.outputs.warnings }})
|
||||
- ✨ **≥85% test coverage** (currently: ${{ steps.coverage.outputs.percentage }}%)
|
||||
- ✅ **Zero test failures** (currently: ${{ steps.tests.outputs.failures }})
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<details>
|
||||
<summary>🔧 Quick Actions</summary>
|
||||
|
||||
|
||||
To improve code quality:
|
||||
```bash
|
||||
# Fix formatting issues
|
||||
mix format
|
||||
|
||||
|
||||
# View detailed Credo analysis
|
||||
mix credo --strict
|
||||
|
||||
|
||||
# Check Dialyzer warnings
|
||||
mix dialyzer
|
||||
|
||||
|
||||
# Generate detailed coverage report
|
||||
mix coveralls.html
|
||||
```
|
||||
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
🤖 *Auto-generated by GitHub Actions* • Updated: ${{ github.event.head_commit.timestamp }}
|
||||
|
||||
> **Note**: This comment will be updated automatically when new commits are pushed to this PR.
|
||||
|
||||
> **Note**: This comment will be updated automatically when new commits are pushed to this PR.
|
||||
|
||||
80
CHANGELOG.md
80
CHANGELOG.md
@@ -2,6 +2,86 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.89.5](https://github.com/wanderer-industries/wanderer/compare/v1.89.4...v1.89.5) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.89.4](https://github.com/wanderer-industries/wanderer/compare/v1.89.3...v1.89.4) (2025-12-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed acl character update issues
|
||||
|
||||
## [v1.89.3](https://github.com/wanderer-industries/wanderer/compare/v1.89.2...v1.89.3) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed tracking issues
|
||||
|
||||
## [v1.89.2](https://github.com/wanderer-industries/wanderer/compare/v1.89.1...v1.89.2) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.89.1](https://github.com/wanderer-industries/wanderer/compare/v1.89.0...v1.89.1) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed tracking issues
|
||||
|
||||
## [v1.89.0](https://github.com/wanderer-industries/wanderer/compare/v1.88.13...v1.89.0) (2025-11-30)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* removed unnecessary command
|
||||
|
||||
* rework wormholes reference
|
||||
|
||||
## [v1.88.13](https://github.com/wanderer-industries/wanderer/compare/v1.88.12...v1.88.13) (2025-11-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed tracking issues
|
||||
|
||||
## [v1.88.12](https://github.com/wanderer-industries/wanderer/compare/v1.88.11...v1.88.12) (2025-11-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed c4 -> ns connections auto size issues
|
||||
|
||||
## [v1.88.11](https://github.com/wanderer-industries/wanderer/compare/v1.88.10...v1.88.11) (2025-11-29)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.88.10](https://github.com/wanderer-industries/wanderer/compare/v1.88.9...v1.88.10) (2025-11-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed pings cleanup
|
||||
|
||||
## [v1.88.9](https://github.com/wanderer-industries/wanderer/compare/v1.88.8...v1.88.9) (2025-11-29)
|
||||
|
||||
|
||||
|
||||
50
Makefile
50
Makefile
@@ -32,6 +32,56 @@ format f:
|
||||
test t:
|
||||
MIX_ENV=test mix test
|
||||
|
||||
# Run tests in 4 parallel partitions (useful for CI or faster local runs)
|
||||
test-parallel tp:
|
||||
@echo "Running tests in 4 parallel partitions..."
|
||||
@mkdir -p /tmp/wanderer_test_results
|
||||
@rm -f /tmp/wanderer_test_results/partition_*.txt /tmp/wanderer_test_results/exit_*.txt
|
||||
@for i in 1 2 3 4; do \
|
||||
(MIX_TEST_PARTITION=$$i MIX_ENV=test mix test --partitions 4 2>&1; echo $$? > /tmp/wanderer_test_results/exit_$$i.txt) | \
|
||||
tee /tmp/wanderer_test_results/partition_$$i.txt | sed "s/^/[P$$i] /" & \
|
||||
done; \
|
||||
wait
|
||||
@echo ""
|
||||
@echo "========================================"
|
||||
@echo " TEST RESULTS SUMMARY"
|
||||
@echo "========================================"
|
||||
@total_tests=0; total_failures=0; total_excluded=0; all_passed=true; \
|
||||
for i in 1 2 3 4; do \
|
||||
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
|
||||
if [ "$$exit_code" != "0" ]; then all_passed=false; fi; \
|
||||
summary=$$(grep -E "^[0-9]+ (tests?|doctest)" /tmp/wanderer_test_results/partition_$$i.txt | tail -1 || echo "No results"); \
|
||||
tests=$$(echo "$$summary" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
failures=$$(echo "$$summary" | grep -oE "[0-9]+ failures?" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
excluded=$$(echo "$$summary" | grep -oE "[0-9]+ excluded" | grep -oE "^[0-9]+" || echo "0"); \
|
||||
total_tests=$$((total_tests + tests)); \
|
||||
total_failures=$$((total_failures + failures)); \
|
||||
total_excluded=$$((total_excluded + excluded)); \
|
||||
if [ "$$exit_code" = "0" ]; then \
|
||||
echo "Partition $$i: ✓ $$summary"; \
|
||||
else \
|
||||
echo "Partition $$i: ✗ $$summary (exit code: $$exit_code)"; \
|
||||
fi; \
|
||||
done; \
|
||||
echo "========================================"; \
|
||||
echo "TOTAL: $$total_tests tests, $$total_failures failures, $$total_excluded excluded"; \
|
||||
echo "========================================"; \
|
||||
if [ "$$all_passed" = "true" ]; then \
|
||||
echo "✓ All partitions passed!"; \
|
||||
else \
|
||||
echo "✗ Some partitions failed. Details below:"; \
|
||||
echo ""; \
|
||||
for i in 1 2 3 4; do \
|
||||
exit_code=$$(cat /tmp/wanderer_test_results/exit_$$i.txt 2>/dev/null || echo "1"); \
|
||||
if [ "$$exit_code" != "0" ]; then \
|
||||
echo "======== PARTITION $$i FAILURES ========"; \
|
||||
grep -A 50 "Failures:" /tmp/wanderer_test_results/partition_$$i.txt 2>/dev/null || cat /tmp/wanderer_test_results/partition_$$i.txt; \
|
||||
echo ""; \
|
||||
fi; \
|
||||
done; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
coverage cover co:
|
||||
MIX_ENV=test mix test --cover
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { MapContextMenu } from '@/hooks/Mapper/components/mapRootContent/compone
|
||||
import { useSkipContextMenu } from '@/hooks/Mapper/hooks/useSkipContextMenu';
|
||||
import { MapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings';
|
||||
import { CharacterActivity } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity';
|
||||
import { WormholeSignaturesDialog } from '@/hooks/Mapper/components/mapRootContent/components/WormholeSignaturesDialog';
|
||||
import { useCharacterActivityHandlers } from './hooks/useCharacterActivityHandlers';
|
||||
import { TrackingDialog } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
@@ -34,6 +35,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
const [showOnTheMap, setShowOnTheMap] = useState(false);
|
||||
const [showMapSettings, setShowMapSettings] = useState(false);
|
||||
const [showTrackingDialog, setShowTrackingDialog] = useState(false);
|
||||
const [showWormholeList, setShowWormholeList] = useState(false);
|
||||
|
||||
/* Important Notice - this solution needs for use one instance of MapInterface */
|
||||
const mapInterface = isReady ? <MapInterface /> : null;
|
||||
@@ -41,6 +43,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
const handleShowOnTheMap = useCallback(() => setShowOnTheMap(true), []);
|
||||
const handleShowMapSettings = useCallback(() => setShowMapSettings(true), []);
|
||||
const handleShowTrackingDialog = useCallback(() => setShowTrackingDialog(true), []);
|
||||
const handleShowWormholesReference = useCallback(() => setShowWormholeList(true), []);
|
||||
|
||||
useMapEventListener(event => {
|
||||
if (event.name === Commands.showTracking) {
|
||||
@@ -65,6 +68,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
onShowOnTheMap={handleShowOnTheMap}
|
||||
onShowMapSettings={handleShowMapSettings}
|
||||
onShowTrackingDialog={handleShowTrackingDialog}
|
||||
onShowWormholesReference={handleShowWormholesReference}
|
||||
additionalContent={<PingsInterface hasLeftOffset />}
|
||||
/>
|
||||
</div>
|
||||
@@ -79,6 +83,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
onShowOnTheMap={handleShowOnTheMap}
|
||||
onShowMapSettings={handleShowMapSettings}
|
||||
onShowTrackingDialog={handleShowTrackingDialog}
|
||||
onShowWormholesReference={handleShowWormholesReference}
|
||||
/>
|
||||
</div>
|
||||
</Topbar>
|
||||
@@ -93,6 +98,7 @@ export const MapRootContent = ({}: MapRootContentProps) => {
|
||||
{showTrackingDialog && (
|
||||
<TrackingDialog visible={showTrackingDialog} onHide={() => setShowTrackingDialog(false)} />
|
||||
)}
|
||||
<WormholeSignaturesDialog visible={showWormholeList} onHide={() => setShowWormholeList(false)} />
|
||||
|
||||
{hasOldSettings && <OldSettingsDialog />}
|
||||
</Layout>
|
||||
|
||||
@@ -12,9 +12,15 @@ export interface MapContextMenuProps {
|
||||
onShowOnTheMap?: () => void;
|
||||
onShowMapSettings?: () => void;
|
||||
onShowTrackingDialog?: () => void;
|
||||
onShowWormholesReference?: () => void;
|
||||
}
|
||||
|
||||
export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTrackingDialog }: MapContextMenuProps) => {
|
||||
export const MapContextMenu = ({
|
||||
onShowOnTheMap,
|
||||
onShowMapSettings,
|
||||
onShowTrackingDialog,
|
||||
onShowWormholesReference,
|
||||
}: MapContextMenuProps) => {
|
||||
const {
|
||||
outCommand,
|
||||
storedSettings: { setInterfaceSettings },
|
||||
@@ -52,6 +58,12 @@ export const MapContextMenu = ({ onShowOnTheMap, onShowMapSettings, onShowTracki
|
||||
command: onShowOnTheMap,
|
||||
visible: canTrackCharacters,
|
||||
},
|
||||
{
|
||||
label: 'Wormholes Ref.',
|
||||
icon: 'pi pi-bullseye',
|
||||
command: onShowWormholesReference,
|
||||
visible: canTrackCharacters,
|
||||
},
|
||||
{ separator: true, visible: true },
|
||||
{
|
||||
label: 'Settings',
|
||||
|
||||
@@ -14,6 +14,7 @@ interface RightBarProps {
|
||||
onShowOnTheMap?: () => void;
|
||||
onShowMapSettings?: () => void;
|
||||
onShowTrackingDialog?: () => void;
|
||||
onShowWormholesReference?: () => void;
|
||||
additionalContent?: ReactNode;
|
||||
}
|
||||
|
||||
@@ -21,6 +22,7 @@ export const RightBar = ({
|
||||
onShowOnTheMap,
|
||||
onShowMapSettings,
|
||||
onShowTrackingDialog,
|
||||
onShowWormholesReference,
|
||||
additionalContent,
|
||||
}: RightBarProps) => {
|
||||
const {
|
||||
@@ -90,6 +92,16 @@ export const RightBar = ({
|
||||
<i className="pi pi-hashtag"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
<WdTooltipWrapper content="Wormholes Reference" position={TooltipPosition.left}>
|
||||
<button
|
||||
className="btn bg-transparent text-gray-400 hover:text-white border-transparent hover:bg-transparent py-2 h-auto min-h-auto"
|
||||
type="button"
|
||||
onClick={onShowWormholesReference}
|
||||
>
|
||||
<i className="pi pi-bullseye"></i>
|
||||
</button>
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { createContext, useCallback, useContext, useRef, useState } from 'react';
|
||||
import { OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
|
||||
import { createContext, useCallback, useContext, useRef, useState, useEffect } from 'react';
|
||||
import { Commands, OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
import { CommandInCharactersTrackingInfo } from '@/hooks/Mapper/types/commandsIn.ts';
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
|
||||
type DiffTrackingInfo = { characterId: string; tracked: boolean };
|
||||
|
||||
@@ -122,6 +123,14 @@ export const TrackingProvider = ({ children }: WithChildren) => {
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
// Listen for refresh_tracking_data event (triggered when ACL members change)
|
||||
useMapEventListener(event => {
|
||||
if (event.name === Commands.refreshTrackingData) {
|
||||
loadTracking();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<TrackingContext.Provider
|
||||
value={{
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { DataTable } from 'primereact/datatable';
|
||||
import { Column } from 'primereact/column';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types';
|
||||
import { RespawnTag, WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
|
||||
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import clsx from 'clsx';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { InputIcon } from 'primereact/inputicon';
|
||||
|
||||
const renderSpawns = (w: WormholeDataRaw) => (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{w.src.map(s => {
|
||||
const group = s.split('-')[0];
|
||||
const info = WORMHOLES_ADDITIONAL_INFO[group];
|
||||
|
||||
if (!info) {
|
||||
return (
|
||||
<span
|
||||
key={s}
|
||||
className="px-[4px] py-[1px] rounded bg-stone-800 text-stone-300 text-xs border border-stone-700"
|
||||
>
|
||||
{s}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const cls = WORMHOLE_CLASS_STYLES[String(info.wormholeClassID)] || '';
|
||||
const label = `${info.shortName}`;
|
||||
return (
|
||||
<span
|
||||
key={s}
|
||||
className={clsx(cls, 'px-[4px] py-[1px] rounded text-xs border border-stone-700 bg-stone-900/40')}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderName = (w: WormholeDataRaw) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<WHClassView
|
||||
whClassName={w.name}
|
||||
noOffset
|
||||
useShortTitle
|
||||
classNameWh="overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderRespawn = (w: WormholeDataRaw) => (
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{w.respawn.map(r => (
|
||||
<RespawnTag key={r} value={r} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
export interface WormholeSignaturesDialogProps {
|
||||
visible: boolean;
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
export const WormholeSignaturesDialog = ({ visible, onHide }: WormholeSignaturesDialogProps) => {
|
||||
const {
|
||||
data: { wormholes },
|
||||
} = useMapRootState();
|
||||
|
||||
const [filter, setFilter] = useState('');
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = filter.trim().toLowerCase();
|
||||
|
||||
if (!q) return wormholes;
|
||||
|
||||
return wormholes.filter(w => {
|
||||
const destInfo = WORMHOLES_ADDITIONAL_INFO[w.dest];
|
||||
const spawnsLabels = w.src
|
||||
.map(s => {
|
||||
const group = s.split('-')[0];
|
||||
const info = WORMHOLES_ADDITIONAL_INFO[group];
|
||||
if (!info) return s;
|
||||
return `${info.title} ${info.shortName}`.trim();
|
||||
})
|
||||
.join(' ');
|
||||
|
||||
return [
|
||||
w.name,
|
||||
destInfo?.title,
|
||||
destInfo?.shortName,
|
||||
spawnsLabels,
|
||||
String(w.total_mass),
|
||||
String(w.max_mass_per_jump),
|
||||
w.lifetime,
|
||||
w.respawn.join(','),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')
|
||||
.toLowerCase()
|
||||
.includes(q);
|
||||
});
|
||||
}, [wormholes, filter]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header="Wormholes Reference"
|
||||
visible={visible}
|
||||
draggable={false}
|
||||
resizable={false}
|
||||
className="w-[950px] h-[600px]"
|
||||
onHide={onHide}
|
||||
contentClassName="!p-0 flex flex-col h-full"
|
||||
>
|
||||
<div className="p-3 flex items-center justify-between gap-2 border-b border-stone-800">
|
||||
<div className="font-semibold text-sm text-stone-200">Reference list of all wormhole types</div>
|
||||
<IconField iconPosition="right">
|
||||
<InputIcon
|
||||
className={clsx('pi pi-times', {
|
||||
['cursor-pointer text-stone-400 hover:text-stone-200']: filter,
|
||||
['text-stone-700 opacity-50 cursor-default']: !filter,
|
||||
})}
|
||||
onClick={() => filter && setFilter('')}
|
||||
role="button"
|
||||
aria-label="Clear search"
|
||||
aria-disabled={!filter}
|
||||
title={filter ? 'Clear' : 'Nothing to clear'}
|
||||
/>
|
||||
<InputText className="w-64" placeholder="Search" value={filter} onChange={e => setFilter(e.target.value)} />
|
||||
</IconField>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-3 overflow-x-hidden">
|
||||
<DataTable value={filtered} size="small" scrollable scrollHeight="flex" stripedRows>
|
||||
<Column header="Type" body={renderName} className="w-[160px]" bodyClassName="whitespace-normal break-words" />
|
||||
<Column header="Spawns In" body={renderSpawns} bodyClassName="whitespace-normal break-words text-[13px]" />
|
||||
<Column
|
||||
field="lifetime"
|
||||
header="Lifetime"
|
||||
className="w-[90px]"
|
||||
bodyClassName="whitespace-normal break-words text-[13px]"
|
||||
/>
|
||||
<Column
|
||||
header="Total Mass"
|
||||
className="w-[120px]"
|
||||
body={(w: WormholeDataRaw) => kgToTons(w.total_mass)}
|
||||
bodyClassName="whitespace-normal break-words text-[13px]"
|
||||
/>
|
||||
<Column
|
||||
header="Max/jump"
|
||||
className="w-[120px]"
|
||||
body={(w: WormholeDataRaw) => kgToTons(w.max_mass_per_jump)}
|
||||
bodyClassName="whitespace-normal break-words text-[13px]"
|
||||
/>
|
||||
<Column
|
||||
header="Respawn"
|
||||
className="w-[150px]"
|
||||
body={renderRespawn}
|
||||
bodyClassName="whitespace-normal break-words text-[13px]"
|
||||
/>
|
||||
</DataTable>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WormholeSignaturesDialog';
|
||||
20
assets/js/hooks/Mapper/components/ui-kit/RespawnTag.tsx
Normal file
20
assets/js/hooks/Mapper/components/ui-kit/RespawnTag.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Respawn } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const WORMHOLE_SPAWN_CLASSES_BG = {
|
||||
[Respawn.static]: 'bg-lime-400/80 text-stone-950',
|
||||
[Respawn.wandering]: 'bg-stone-800',
|
||||
[Respawn.reverse]: 'bg-blue-400 text-stone-950',
|
||||
};
|
||||
|
||||
type RespawnTagProps = { value: string };
|
||||
export const RespawnTag = ({ value }: RespawnTagProps) => (
|
||||
<span
|
||||
className={clsx(
|
||||
'px-[6px] py-[0px] rounded text-stone-300 text-[12px] font-[500] border border-stone-700',
|
||||
WORMHOLE_SPAWN_CLASSES_BG[value as Respawn],
|
||||
)}
|
||||
>
|
||||
{value}
|
||||
</span>
|
||||
);
|
||||
@@ -23,3 +23,4 @@ export * from './MenuItemWithInfo';
|
||||
export * from './MarkdownTextViewer.tsx';
|
||||
export * from './WdButton.tsx';
|
||||
export * from './constants.ts';
|
||||
export * from './RespawnTag';
|
||||
|
||||
@@ -63,127 +63,122 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const { pingAdded, pingCancelled } = useCommandPings();
|
||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
|
||||
case Commands.pingAdded:
|
||||
pingAdded(data as CommandPingAdded);
|
||||
break;
|
||||
case Commands.pingAdded:
|
||||
pingAdded(data as CommandPingAdded);
|
||||
break;
|
||||
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ export enum Commands {
|
||||
updateTracking = 'update_tracking',
|
||||
userSettingsUpdated = 'user_settings_updated',
|
||||
showTracking = 'show_tracking',
|
||||
refreshTrackingData = 'refresh_tracking_data',
|
||||
pingAdded = 'ping_added',
|
||||
pingCancelled = 'ping_cancelled',
|
||||
}
|
||||
@@ -74,6 +75,7 @@ export type Command =
|
||||
| Commands.updateActivity
|
||||
| Commands.updateTracking
|
||||
| Commands.showTracking
|
||||
| Commands.refreshTrackingData
|
||||
| Commands.pingAdded
|
||||
| Commands.pingCancelled;
|
||||
|
||||
@@ -145,6 +147,7 @@ export type CommandUserSettingsUpdated = {
|
||||
};
|
||||
|
||||
export type CommandShowTracking = null;
|
||||
export type CommandRefreshTrackingData = Record<string, never>;
|
||||
export type CommandUpdateActivity = {
|
||||
characterId: number;
|
||||
systemId: number;
|
||||
@@ -206,6 +209,7 @@ export interface CommandData {
|
||||
[Commands.systemCommentRemoved]: CommandCommentRemoved;
|
||||
[Commands.systemCommentsUpdated]: unknown;
|
||||
[Commands.showTracking]: CommandShowTracking;
|
||||
[Commands.refreshTrackingData]: CommandRefreshTrackingData;
|
||||
[Commands.pingAdded]: CommandPingAdded;
|
||||
[Commands.pingCancelled]: CommandPingCancelled;
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ config :logger,
|
||||
case config_env() do
|
||||
:prod -> "info"
|
||||
:dev -> "info"
|
||||
:test -> "debug"
|
||||
:test -> "warning"
|
||||
end
|
||||
)
|
||||
)
|
||||
|
||||
@@ -16,6 +16,11 @@ defmodule WandererApp.Api.AccessList do
|
||||
|
||||
includes([:owner, :members])
|
||||
|
||||
default_fields([
|
||||
:name,
|
||||
:description
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -79,12 +84,15 @@ defmodule WandererApp.Api.AccessList do
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :description, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
# Note: api_key intentionally not public for security
|
||||
attribute :api_key, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
@@ -16,6 +16,14 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
|
||||
includes([:access_list])
|
||||
|
||||
default_fields([
|
||||
:name,
|
||||
:eve_character_id,
|
||||
:eve_corporation_id,
|
||||
:eve_alliance_id,
|
||||
:role
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -89,22 +97,27 @@ defmodule WandererApp.Api.AccessListMember do
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_character_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_corporation_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :eve_alliance_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :role, :atom do
|
||||
default "viewer"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
|
||||
@@ -19,9 +19,10 @@ defmodule WandererApp.Api.Changes.InjectMapFromActor do
|
||||
_other ->
|
||||
# nil or unexpected return shape - check for direct map_id
|
||||
# Check params (input), arguments, and attributes (in that order)
|
||||
map_id = Map.get(changeset.params, :map_id) ||
|
||||
Ash.Changeset.get_argument(changeset, :map_id) ||
|
||||
Ash.Changeset.get_attribute(changeset, :map_id)
|
||||
map_id =
|
||||
Map.get(changeset.params, :map_id) ||
|
||||
Ash.Changeset.get_argument(changeset, :map_id) ||
|
||||
Ash.Changeset.get_attribute(changeset, :map_id)
|
||||
|
||||
case map_id do
|
||||
nil ->
|
||||
|
||||
@@ -13,6 +13,8 @@ defmodule WandererApp.Api.Map do
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("maps_v1")
|
||||
|
||||
migration_defaults scopes: "'{wormholes}'"
|
||||
end
|
||||
|
||||
json_api do
|
||||
@@ -111,6 +113,7 @@ defmodule WandererApp.Api.Map do
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:scopes,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
@@ -135,6 +138,7 @@ defmodule WandererApp.Api.Map do
|
||||
:slug,
|
||||
:description,
|
||||
:scope,
|
||||
:scopes,
|
||||
:only_tracked_characters,
|
||||
:owner_id,
|
||||
:sse_enabled
|
||||
@@ -209,7 +213,7 @@ defmodule WandererApp.Api.Map do
|
||||
end
|
||||
|
||||
create :duplicate do
|
||||
accept [:name, :description, :scope, :only_tracked_characters]
|
||||
accept [:name, :description, :scope, :scopes, :only_tracked_characters]
|
||||
argument :source_map_id, :uuid, allow_nil?: false
|
||||
argument :copy_acls, :boolean, default: true
|
||||
argument :copy_user_settings, :boolean, default: true
|
||||
@@ -225,9 +229,14 @@ defmodule WandererApp.Api.Map do
|
||||
description =
|
||||
Ash.Changeset.get_attribute(changeset, :description) || source_map.description
|
||||
|
||||
# Use provided scopes or fall back to source map scopes
|
||||
scopes =
|
||||
Ash.Changeset.get_attribute(changeset, :scopes) || source_map.scopes
|
||||
|
||||
changeset
|
||||
|> Ash.Changeset.change_attribute(:description, description)
|
||||
|> Ash.Changeset.change_attribute(:scope, source_map.scope)
|
||||
|> Ash.Changeset.change_attribute(:scopes, scopes)
|
||||
|> Ash.Changeset.change_attribute(
|
||||
:only_tracked_characters,
|
||||
source_map.only_tracked_characters
|
||||
@@ -359,6 +368,24 @@ defmodule WandererApp.Api.Map do
|
||||
public?(true)
|
||||
end
|
||||
|
||||
attribute :scopes, {:array, :atom} do
|
||||
default([:wormholes])
|
||||
allow_nil?(true)
|
||||
public?(true)
|
||||
|
||||
constraints(
|
||||
items: [
|
||||
one_of: [
|
||||
:wormholes,
|
||||
:hi,
|
||||
:low,
|
||||
:null,
|
||||
:pochven
|
||||
]
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
@@ -27,6 +27,11 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
includes([:map, :character])
|
||||
|
||||
default_fields([
|
||||
:tracked,
|
||||
:followed
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -219,14 +224,17 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
|
||||
attribute :tracked, :boolean do
|
||||
default false
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :followed, :boolean do
|
||||
default false
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
# Note: These attributes are encrypted (AshCloak) and intentionally not public
|
||||
attribute :solar_system_id, :integer
|
||||
attribute :structure_id, :integer
|
||||
attribute :station_id, :integer
|
||||
|
||||
@@ -22,6 +22,19 @@ defmodule WandererApp.Api.MapConnection do
|
||||
|
||||
includes([:map])
|
||||
|
||||
default_fields([
|
||||
:solar_system_source,
|
||||
:solar_system_target,
|
||||
:mass_status,
|
||||
:time_status,
|
||||
:ship_size_type,
|
||||
:type,
|
||||
:wormhole_type,
|
||||
:count_of_passage,
|
||||
:locked,
|
||||
:custom_info
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -197,15 +210,20 @@ defmodule WandererApp.Api.MapConnection do
|
||||
attributes do
|
||||
uuid_primary_key :id
|
||||
|
||||
attribute :solar_system_source, :integer
|
||||
attribute :solar_system_target, :integer
|
||||
attribute :solar_system_source, :integer do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_target, :integer do
|
||||
public? true
|
||||
end
|
||||
|
||||
# where 0 - greater than half
|
||||
# where 1 - less than half
|
||||
# where 2 - critical less than 10%
|
||||
attribute :mass_status, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -218,7 +236,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# 6 - EOL 48h
|
||||
attribute :time_status, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -229,7 +247,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# where 4 - Capital
|
||||
attribute :ship_size_type, :integer do
|
||||
default(2)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
@@ -238,21 +256,26 @@ defmodule WandererApp.Api.MapConnection do
|
||||
# where 2 - Bridge
|
||||
attribute :type, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :wormhole_type, :string
|
||||
attribute :wormhole_type, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :count_of_passage, :integer do
|
||||
default(0)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :locked, :boolean
|
||||
attribute :locked, :boolean do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :custom_info, :string do
|
||||
public? true
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
||||
:updated_by
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:settings
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_default_settings")
|
||||
|
||||
@@ -93,6 +97,7 @@ defmodule WandererApp.Api.MapDefaultSettings do
|
||||
|
||||
attribute :settings, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
constraints min_length: 2
|
||||
description "JSON string containing the default map settings"
|
||||
end
|
||||
|
||||
@@ -18,6 +18,15 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
:map
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:plan,
|
||||
:status,
|
||||
:characters_limit,
|
||||
:hubs_limit,
|
||||
:active_till,
|
||||
:auto_renew?
|
||||
])
|
||||
|
||||
# Enable automatic filtering and sorting
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
@@ -135,6 +144,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :plan, :atom do
|
||||
default "alpha"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -150,6 +160,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :status, :atom do
|
||||
default "active"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -164,22 +175,24 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
|
||||
attribute :characters_limit, :integer do
|
||||
default(100)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :hubs_limit, :integer do
|
||||
default(10)
|
||||
|
||||
public? true
|
||||
allow_nil?(true)
|
||||
end
|
||||
|
||||
attribute :active_till, :utc_datetime do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :auto_renew?, :boolean do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
|
||||
@@ -19,6 +19,10 @@ defmodule WandererApp.Api.MapSystemComment do
|
||||
:character
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:text
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_system_comments")
|
||||
|
||||
@@ -73,6 +77,7 @@ defmodule WandererApp.Api.MapSystemComment do
|
||||
|
||||
attribute :text, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
|
||||
@@ -16,6 +16,20 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
|
||||
includes([:system])
|
||||
|
||||
default_fields([
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:description,
|
||||
:temporary_name,
|
||||
:type,
|
||||
:linked_system_id,
|
||||
:kind,
|
||||
:group,
|
||||
:custom_info,
|
||||
:deleted
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -184,42 +198,56 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
|
||||
attribute :eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :description, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :temporary_name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :type, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :linked_system_id, :integer do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :kind, :string
|
||||
attribute :group, :string
|
||||
attribute :kind, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :group, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :custom_info, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :deleted, :boolean do
|
||||
allow_nil? false
|
||||
default false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :update_forced_at, :utc_datetime do
|
||||
|
||||
@@ -41,6 +41,21 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
:system
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:structure_type_id,
|
||||
:structure_type,
|
||||
:character_eve_id,
|
||||
:solar_system_name,
|
||||
:solar_system_id,
|
||||
:name,
|
||||
:notes,
|
||||
:owner_name,
|
||||
:owner_ticker,
|
||||
:owner_id,
|
||||
:status,
|
||||
:end_time
|
||||
])
|
||||
|
||||
# Enable automatic filtering and sorting
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
@@ -151,50 +166,62 @@ defmodule WandererApp.Api.MapSystemStructure do
|
||||
|
||||
attribute :structure_type_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :structure_type, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :character_eve_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :solar_system_id, :integer do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :name, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :notes, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_name, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_ticker, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :owner_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :status, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :end_time, :utc_datetime_usec do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp :inserted_at
|
||||
|
||||
@@ -24,6 +24,13 @@ defmodule WandererApp.Api.MapUserSettings do
|
||||
:user
|
||||
])
|
||||
|
||||
default_fields([
|
||||
:settings,
|
||||
:main_character_eve_id,
|
||||
:following_character_eve_id,
|
||||
:hubs
|
||||
])
|
||||
|
||||
routes do
|
||||
base("/map_user_settings")
|
||||
|
||||
@@ -85,19 +92,22 @@ defmodule WandererApp.Api.MapUserSettings do
|
||||
|
||||
attribute :settings, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :main_character_eve_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :following_character_eve_id, :string do
|
||||
allow_nil? true
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :hubs, {:array, :string} do
|
||||
allow_nil?(true)
|
||||
|
||||
public? true
|
||||
default([])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +31,13 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
includes([:character, :user])
|
||||
|
||||
default_fields([
|
||||
:entity_id,
|
||||
:entity_type,
|
||||
:event_type,
|
||||
:event_data
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -86,10 +93,12 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
attribute :entity_id, :string do
|
||||
allow_nil? false
|
||||
public? true
|
||||
end
|
||||
|
||||
attribute :entity_type, :atom do
|
||||
default "map"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -104,6 +113,7 @@ defmodule WandererApp.Api.UserActivity do
|
||||
|
||||
attribute :event_type, :atom do
|
||||
default "custom"
|
||||
public? true
|
||||
|
||||
constraints(
|
||||
one_of: [
|
||||
@@ -153,7 +163,9 @@ defmodule WandererApp.Api.UserActivity do
|
||||
allow_nil?(false)
|
||||
end
|
||||
|
||||
attribute :event_data, :string
|
||||
attribute :event_data, :string do
|
||||
public? true
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
|
||||
@@ -45,7 +45,7 @@ defmodule WandererApp.Cache do
|
||||
def insert({id, key}, value, opts) when is_binary(id) and (is_binary(key) or is_atom(key)),
|
||||
do: insert("#{id}:#{key}", value, opts)
|
||||
|
||||
def insert(key, nil, opts) when is_binary(key) or is_atom(key), do: delete(key)
|
||||
def insert(key, nil, _opts) when is_binary(key) or is_atom(key), do: delete(key)
|
||||
def insert(key, value, opts) when is_binary(key) or is_atom(key), do: put(key, value, opts)
|
||||
|
||||
def insert_or_update(key, value, update_fn, opts \\ [])
|
||||
|
||||
@@ -598,9 +598,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :skipped}
|
||||
end
|
||||
|
||||
_ ->
|
||||
@@ -799,7 +796,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
corporation_id
|
||||
|> WandererApp.Esi.get_corporation_info()
|
||||
|> case do
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker}} ->
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
@@ -1002,7 +999,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
defp maybe_update_active_maps(
|
||||
%{character_id: character_id, active_maps: active_maps} =
|
||||
state,
|
||||
%{map_id: map_id, track: true} = track_settings
|
||||
%{map_id: map_id, track: true}
|
||||
) do
|
||||
if not Enum.member?(active_maps, map_id) do
|
||||
WandererApp.Cache.put(
|
||||
|
||||
@@ -40,10 +40,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
||||
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
|
||||
|
||||
Logger.debug("[TrackerManager] Initialized with intervals: " <>
|
||||
"garbage_collection=#{div(@garbage_collection_interval, 60_000)}min, " <>
|
||||
"untrack=#{div(@untrack_characters_interval, 60_000)}min, " <>
|
||||
"inactive_timeout=#{div(@inactive_character_timeout, 60_000)}min")
|
||||
Logger.debug(
|
||||
"[TrackerManager] Initialized with intervals: " <>
|
||||
"garbage_collection=#{div(@garbage_collection_interval, 60_000)}min, " <>
|
||||
"untrack=#{div(@untrack_characters_interval, 60_000)}min, " <>
|
||||
"inactive_timeout=#{div(@inactive_character_timeout, 60_000)}min"
|
||||
)
|
||||
|
||||
%{
|
||||
characters: [],
|
||||
@@ -57,7 +59,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
WandererApp.Cache.insert("tracked_characters", [])
|
||||
|
||||
if length(tracked_characters) > 0 do
|
||||
Logger.debug("[TrackerManager] Restoring #{length(tracked_characters)} tracked characters from cache")
|
||||
Logger.debug(
|
||||
"[TrackerManager] Restoring #{length(tracked_characters)} tracked characters from cache"
|
||||
)
|
||||
end
|
||||
|
||||
tracked_characters
|
||||
@@ -151,10 +155,23 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
remove_from_untrack_queue(map_id, character_id)
|
||||
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_settings(character_id, track_settings)
|
||||
case WandererApp.Character.Tracker.update_settings(character_id, track_settings) do
|
||||
{:ok, character_state} ->
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
{:error, :not_found} ->
|
||||
# Tracker process not running yet - this is expected during initial tracking setup
|
||||
# The tracking_start_time cache key was already set by TrackingUtils.track_character
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Tracker not yet running for character #{character_id} - " <>
|
||||
"tracking will be active via cache key"
|
||||
end)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning(fn ->
|
||||
"[TrackerManager] Failed to update settings for character #{character_id}: #{inspect(reason)}"
|
||||
end)
|
||||
end
|
||||
else
|
||||
Logger.debug(fn ->
|
||||
"[TrackerManager] Queuing character #{character_id} for untracking from map #{map_id} - " <>
|
||||
@@ -184,6 +201,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
[],
|
||||
fn untrack_queue ->
|
||||
original_length = length(untrack_queue)
|
||||
|
||||
filtered =
|
||||
untrack_queue
|
||||
|> Enum.reject(fn {m_id, c_id} -> m_id == map_id and c_id == character_id end)
|
||||
|
||||
@@ -88,15 +88,4 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
|
||||
{:ok, pid}
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_child(uuid) do
|
||||
case Registry.lookup(@registry, uuid) do
|
||||
[{pid, _}] ->
|
||||
GenServer.cast(pid, :stop)
|
||||
|
||||
_ ->
|
||||
Logger.warn("Unable to locate pool assigned to #{inspect(uuid)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -38,7 +38,7 @@ defmodule WandererApp.Character.TrackingConfigUtils do
|
||||
%{id: "default", title: "Default", value: default_count}
|
||||
]
|
||||
|
||||
{:ok, pools_count} =
|
||||
{:ok, _pools_count} =
|
||||
Cachex.get(
|
||||
:esi_auth_cache,
|
||||
"configs_total_count"
|
||||
|
||||
@@ -53,24 +53,27 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
|
||||
@doc """
|
||||
Builds tracking data for all characters with access to a map.
|
||||
Only includes characters that have actual tracking permission.
|
||||
"""
|
||||
def build_tracking_data(map_id, current_user_id) do
|
||||
with {:ok, map} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
),
|
||||
with {:ok, map} <- WandererApp.MapRepo.get(map_id),
|
||||
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
||||
# Filter to only characters with actual tracking permission
|
||||
characters_with_tracking_permission =
|
||||
filter_characters_with_tracking_permission(characters_with_access, map)
|
||||
|
||||
# Map characters to tracking data
|
||||
{:ok, characters_data} =
|
||||
build_character_tracking_data(characters_with_access)
|
||||
build_character_tracking_data(characters_with_tracking_permission)
|
||||
|
||||
{:ok, main_character} =
|
||||
get_main_character(user_settings, characters_with_access, characters_with_access)
|
||||
get_main_character(
|
||||
user_settings,
|
||||
characters_with_tracking_permission,
|
||||
characters_with_tracking_permission
|
||||
)
|
||||
|
||||
following_character_eve_id =
|
||||
case user_settings do
|
||||
@@ -112,6 +115,70 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end)}
|
||||
end
|
||||
|
||||
# Filter characters to only include those with actual tracking permission
|
||||
# This prevents showing characters in the tracking dialog that will fail when toggled
|
||||
defp filter_characters_with_tracking_permission(characters, %{id: map_id, owner_id: owner_id}) do
|
||||
# Load ACLs with members properly (same approach as get_map_characters)
|
||||
acls = load_map_acls_with_members(map_id)
|
||||
|
||||
Enum.filter(characters, fn character ->
|
||||
has_tracking_permission?(character, owner_id, acls)
|
||||
end)
|
||||
end
|
||||
|
||||
# Load ACLs with members in the correct format for permission checking
|
||||
defp load_map_acls_with_members(map_id) do
|
||||
case WandererApp.Api.MapAccessList.read_by_map(%{map_id: map_id},
|
||||
load: [access_list: [:owner, :members]]
|
||||
) do
|
||||
{:ok, map_access_lists} ->
|
||||
map_access_lists
|
||||
|> Enum.map(fn mal -> mal.access_list end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Check if a character has tracking permission on a map
|
||||
# Returns true if the character can be tracked, false otherwise
|
||||
defp has_tracking_permission?(character, owner_id, acls) do
|
||||
cond do
|
||||
# Map owner always has tracking permission
|
||||
character.id == owner_id ->
|
||||
true
|
||||
|
||||
# Character belongs to same user as map owner
|
||||
# Note: character data from load_characters may not have user_id, so we need to load it
|
||||
check_same_user_as_owner_by_id(character.id, owner_id) ->
|
||||
true
|
||||
|
||||
# Check ACL-based permissions
|
||||
true ->
|
||||
case WandererApp.Permissions.check_characters_access([character], acls) do
|
||||
[character_permissions] ->
|
||||
map_permissions = WandererApp.Permissions.get_permissions(character_permissions)
|
||||
map_permissions.track_character and map_permissions.view_system
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Check if character belongs to the same user as the map owner (by character IDs)
|
||||
defp check_same_user_as_owner_by_id(_character_id, nil), do: false
|
||||
|
||||
defp check_same_user_as_owner_by_id(character_id, owner_id) do
|
||||
with {:ok, character} <- WandererApp.Character.get_character(character_id),
|
||||
{:ok, owner_character} <- WandererApp.Character.get_character(owner_id) do
|
||||
character.user_id != nil and character.user_id == owner_character.user_id
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
# Private implementation of update character tracking
|
||||
defp do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||
# First check current tracking state to avoid unnecessary permission checks
|
||||
@@ -126,7 +193,13 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
{true, settings_result} ->
|
||||
case check_character_tracking_permission(character, map_id) do
|
||||
{:ok, :allowed} ->
|
||||
do_update_character_tracking_impl(character, map_id, track, caller_pid, settings_result)
|
||||
do_update_character_tracking_impl(
|
||||
character,
|
||||
map_id,
|
||||
track,
|
||||
caller_pid,
|
||||
settings_result
|
||||
)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning(
|
||||
@@ -212,6 +285,9 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
{:ok, %{tracked: false} = existing_settings} ->
|
||||
if track do
|
||||
{:ok, updated_settings} = WandererApp.MapCharacterSettingsRepo.track(existing_settings)
|
||||
# Ensure character is in map state (fixes race condition where character
|
||||
# might not be synced yet from presence updates)
|
||||
:ok = WandererApp.Map.add_character(map_id, character)
|
||||
:ok = track([character], map_id, true, caller_pid)
|
||||
{:ok, updated_settings}
|
||||
else
|
||||
@@ -228,6 +304,9 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
tracked: true
|
||||
})
|
||||
|
||||
# Add character to map state immediately (fixes race condition where
|
||||
# character wouldn't appear on map until next update_presence cycle)
|
||||
:ok = WandererApp.Map.add_character(map_id, character)
|
||||
:ok = track([character], map_id, true, caller_pid)
|
||||
{:ok, settings}
|
||||
else
|
||||
@@ -290,6 +369,31 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
|
||||
if is_track_allowed do
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
|
||||
# Immediately set tracking_start_time cache key to enable map tracking
|
||||
# This ensures the character is tracked for updates even before the
|
||||
# Tracker process is fully started (avoids race condition)
|
||||
tracking_start_key = "character:#{character_id}:map:#{map_id}:tracking_start_time"
|
||||
|
||||
case WandererApp.Cache.lookup(tracking_start_key) do
|
||||
{:ok, nil} ->
|
||||
WandererApp.Cache.put(tracking_start_key, DateTime.utc_now())
|
||||
|
||||
# Clear stale location caches for fresh tracking
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
|
||||
|
||||
_ ->
|
||||
# Already tracking, no need to update
|
||||
:ok
|
||||
end
|
||||
|
||||
# Also call update_track_settings to update character state when tracker is ready
|
||||
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: true
|
||||
})
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
@@ -8,7 +8,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
@ttl :timer.hours(1)
|
||||
|
||||
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
|
||||
@req_esi_options [base_url: "https://esi.evetech.net", finch: WandererApp.Finch]
|
||||
|
||||
@cache_opts [cache: true]
|
||||
@retry_opts [retry: false, retry_log_level: :warning]
|
||||
@@ -74,7 +73,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
)
|
||||
|
||||
def get_routes_eve(hubs, origin, params, opts),
|
||||
def get_routes_eve(hubs, origin, _params, _opts),
|
||||
do:
|
||||
{:ok,
|
||||
hubs
|
||||
@@ -101,33 +100,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end)}
|
||||
|
||||
defp do_get_routes_eve(origin, destination, params, opts) do
|
||||
esi_params =
|
||||
Map.merge(params, %{
|
||||
connections: params.connections |> Enum.join(","),
|
||||
avoid: params.avoid |> Enum.join(",")
|
||||
})
|
||||
|
||||
do_get(
|
||||
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|> case do
|
||||
{:ok, result} ->
|
||||
%{
|
||||
"origin" => origin,
|
||||
"destination" => destination,
|
||||
"systems" => result,
|
||||
"success" => true
|
||||
}
|
||||
|
||||
error ->
|
||||
Logger.warning("Error getting routes: #{inspect(error)}")
|
||||
%{"origin" => origin, "destination" => destination, "systems" => [], "success" => false}
|
||||
end
|
||||
end
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "group-info-#{group_id}",
|
||||
@@ -273,6 +245,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
# Note: search_val and categories_val are used by the @decorate cacheable annotation above
|
||||
_unused = {search_val, categories_val}
|
||||
get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
end
|
||||
|
||||
@@ -348,7 +322,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
defp with_cache_opts(opts),
|
||||
do: opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
|
||||
|
||||
defp do_get(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
defp do_get(path, api_opts, opts, pool \\ @general_pool) do
|
||||
case Cachex.get(:api_cache, path) do
|
||||
{:ok, cached_data} when not is_nil(cached_data) ->
|
||||
{:ok, cached_data}
|
||||
@@ -358,7 +332,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_request(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
defp do_get_request(path, api_opts, opts, pool) do
|
||||
try do
|
||||
req_options_for_pool(pool)
|
||||
|> Req.new()
|
||||
@@ -448,7 +422,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, %{status: status} = _error} when status in [401, 403] ->
|
||||
do_get_retry(path, api_opts, opts)
|
||||
|
||||
{:ok, %{status: status, headers: headers}} ->
|
||||
{:ok, %{status: status}} ->
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
|
||||
{:error, %Mint.TransportError{reason: :timeout}} ->
|
||||
@@ -832,10 +806,10 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp handle_refresh_token_result(
|
||||
{:error, %OAuth2.Error{reason: :econnrefused} = error},
|
||||
character,
|
||||
_character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
_scopes
|
||||
) do
|
||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||
|
||||
@@ -393,9 +393,6 @@ defmodule WandererApp.EveDataService do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_solar_system_name(solar_system_name, wormhole_class) do
|
||||
end
|
||||
|
||||
defp get_triglavian_data(default_data, triglavian_systems, solar_system_id) do
|
||||
case Enum.find(triglavian_systems, fn system -> system.solar_system_id == solar_system_id end) do
|
||||
nil ->
|
||||
@@ -413,8 +410,12 @@ defmodule WandererApp.EveDataService do
|
||||
|
||||
defp get_security(security) do
|
||||
case security do
|
||||
nil -> {:ok, ""}
|
||||
_ -> {:ok, String.to_float(security) |> get_true_security() |> Float.to_string(decimals: 1)}
|
||||
nil ->
|
||||
{:ok, ""}
|
||||
|
||||
_ ->
|
||||
{:ok,
|
||||
String.to_float(security) |> get_true_security() |> :erlang.float_to_binary(decimals: 1)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -496,23 +497,23 @@ defmodule WandererApp.EveDataService do
|
||||
do: {:ok, 10_100}
|
||||
|
||||
defp get_wormhole_class_id(systems, region_id, constellation_id, solar_system_id) do
|
||||
with region <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||
end),
|
||||
constellation <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
||||
end),
|
||||
solar_system <-
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||
end),
|
||||
wormhole_class_id <- get_wormhole_class_id(region, constellation, solar_system) do
|
||||
{:ok, wormhole_class_id}
|
||||
else
|
||||
_ -> {:ok, -1}
|
||||
end
|
||||
region =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == region_id
|
||||
end)
|
||||
|
||||
constellation =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == constellation_id
|
||||
end)
|
||||
|
||||
solar_system =
|
||||
Enum.find(systems, fn system ->
|
||||
system.location_id |> Integer.parse() |> elem(0) == solar_system_id
|
||||
end)
|
||||
|
||||
wormhole_class_id = get_wormhole_class_id(region, constellation, solar_system)
|
||||
{:ok, wormhole_class_id}
|
||||
end
|
||||
|
||||
defp get_wormhole_class_id(_region, _constellation, solar_system)
|
||||
|
||||
@@ -178,6 +178,10 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_payload(payload, visited) when is_map(payload) do
|
||||
Map.new(payload, fn {k, v} -> {to_string(k), serialize_value(v, visited)} end)
|
||||
end
|
||||
|
||||
# Get allowed fields based on struct type
|
||||
defp get_allowed_fields(module) do
|
||||
module_name = module |> Module.split() |> List.last()
|
||||
@@ -192,10 +196,6 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
end
|
||||
end
|
||||
|
||||
defp serialize_payload(payload, visited) when is_map(payload) do
|
||||
Map.new(payload, fn {k, v} -> {to_string(k), serialize_value(v, visited)} end)
|
||||
end
|
||||
|
||||
defp serialize_fields(fields, visited) do
|
||||
Enum.reduce(fields, %{}, fn {k, v}, acc ->
|
||||
if is_nil(v) do
|
||||
|
||||
@@ -182,7 +182,7 @@ defmodule WandererApp.Kills.Client do
|
||||
end
|
||||
|
||||
# Guard against duplicate disconnection events
|
||||
def handle_info({:disconnected, reason}, %{connected: false, connecting: false} = state) do
|
||||
def handle_info({:disconnected, _reason}, %{connected: false, connecting: false} = state) do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -566,7 +566,7 @@ defmodule WandererApp.Kills.Client do
|
||||
end
|
||||
end
|
||||
|
||||
defp check_health(%{socket_pid: pid} = state) do
|
||||
defp check_health(%{socket_pid: pid}) do
|
||||
if socket_alive?(pid) do
|
||||
:healthy
|
||||
else
|
||||
@@ -590,22 +590,6 @@ defmodule WandererApp.Kills.Client do
|
||||
Process.send_after(self(), :health_check, @health_check_interval)
|
||||
end
|
||||
|
||||
defp handle_connection_lost(%{connected: false} = _state) do
|
||||
Logger.debug("[Client] Connection already lost, skipping cleanup")
|
||||
end
|
||||
|
||||
defp handle_connection_lost(state) do
|
||||
Logger.warning("[Client] Connection lost, cleaning up and reconnecting")
|
||||
|
||||
# Clean up existing socket
|
||||
if state.socket_pid do
|
||||
disconnect_socket(state.socket_pid)
|
||||
end
|
||||
|
||||
# Reset state and trigger reconnection
|
||||
send(self(), {:disconnected, :connection_lost})
|
||||
end
|
||||
|
||||
# Handler module for WebSocket events
|
||||
defmodule Handler do
|
||||
@moduledoc """
|
||||
@@ -640,7 +624,7 @@ defmodule WandererApp.Kills.Client do
|
||||
}
|
||||
|
||||
case GenSocketClient.join(transport, "killmails:lobby", join_params) do
|
||||
{:ok, response} ->
|
||||
{:ok, _response} ->
|
||||
send(state.parent, {:connected, self()})
|
||||
# Reset disconnected flag on successful connection
|
||||
{:ok, %{state | disconnected: false}}
|
||||
|
||||
@@ -46,7 +46,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(%{event: :map_server_started, payload: map_info}, state) do
|
||||
def handle_info(%{event: :map_server_started, payload: _map_info}, state) do
|
||||
{:noreply, schedule_subscription_update(state)}
|
||||
end
|
||||
|
||||
@@ -191,7 +191,7 @@ defmodule WandererApp.Kills.MapEventListener do
|
||||
# Client is not connected, retry with backoff
|
||||
schedule_retry_update(state)
|
||||
|
||||
error ->
|
||||
_error ->
|
||||
schedule_retry_update(state)
|
||||
end
|
||||
rescue
|
||||
|
||||
@@ -177,7 +177,7 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
|
||||
def list_hubs(map_id, hubs) do
|
||||
{:ok, map} = map_id |> get_map()
|
||||
{:ok, _map} = map_id |> get_map()
|
||||
|
||||
{:ok, hubs}
|
||||
end
|
||||
@@ -315,7 +315,7 @@ defmodule WandererApp.Map do
|
||||
end
|
||||
end
|
||||
|
||||
def update_subscription_settings!(%{map_id: map_id} = map, %{
|
||||
def update_subscription_settings!(%{map_id: map_id} = _map, %{
|
||||
characters_limit: characters_limit,
|
||||
hubs_limit: hubs_limit
|
||||
}) do
|
||||
@@ -326,7 +326,7 @@ defmodule WandererApp.Map do
|
||||
|> get_map!()
|
||||
end
|
||||
|
||||
def update_options!(%{map_id: map_id} = map, options) do
|
||||
def update_options!(%{map_id: map_id} = _map, options) do
|
||||
map_id
|
||||
|> update_map(%{options: options})
|
||||
|
||||
|
||||
@@ -115,11 +115,20 @@ defmodule WandererApp.Map.Manager do
|
||||
Enum.each(pings, fn %{id: ping_id, map_id: map_id, type: type} = ping ->
|
||||
{:ok, %{system: system}} = ping |> Ash.load([:system])
|
||||
|
||||
Server.Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: system.solar_system_id,
|
||||
type: type
|
||||
})
|
||||
# Handle case where parent system was already deleted
|
||||
case system do
|
||||
nil ->
|
||||
Logger.warning(
|
||||
"[cleanup_expired_pings] ping #{ping_id} destroyed (parent system already deleted)"
|
||||
)
|
||||
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Server.Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: solar_system_id,
|
||||
type: type
|
||||
})
|
||||
end
|
||||
|
||||
Ash.destroy!(ping)
|
||||
end)
|
||||
|
||||
@@ -76,11 +76,6 @@ defmodule WandererApp.Map.Operations do
|
||||
{:ok, map()} | {:skip, :exists} | {:error, String.t()}
|
||||
defdelegate create_connection(map_id, attrs, char_id), to: Connections
|
||||
|
||||
@doc "Create a connection from a Plug.Conn"
|
||||
@spec create_connection(Plug.Conn.t(), map()) ::
|
||||
{:ok, :created} | {:skip, :exists} | {:error, atom()}
|
||||
defdelegate create_connection(conn, attrs), to: Connections
|
||||
|
||||
@doc "Update a connection"
|
||||
@spec update_connection(String.t(), String.t(), map()) ::
|
||||
{:ok, map()} | {:error, String.t()}
|
||||
|
||||
@@ -329,6 +329,9 @@ defmodule WandererApp.Map.MapPool do
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||
|
||||
defp do_start_map(map_id, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||
if map_id in map_ids do
|
||||
# Map already started
|
||||
@@ -344,8 +347,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
[map_id | r_map_ids]
|
||||
end)
|
||||
|
||||
completed_operations = [:registry | completed_operations]
|
||||
|
||||
case registry_result do
|
||||
{new_value, _old_value} when is_list(new_value) ->
|
||||
:ok
|
||||
@@ -363,13 +364,9 @@ defmodule WandererApp.Map.MapPool do
|
||||
raise "Failed to add to cache: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
completed_operations = [:cache | completed_operations]
|
||||
|
||||
# Step 3: Start the map server using extracted helper
|
||||
do_initialize_map_server(map_id)
|
||||
|
||||
completed_operations = [:map_server | completed_operations]
|
||||
|
||||
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
||||
new_state = %{state | map_ids: [map_id | map_ids]}
|
||||
|
||||
@@ -445,8 +442,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
r_map_ids |> Enum.reject(fn id -> id == map_id end)
|
||||
end)
|
||||
|
||||
completed_operations = [:registry | completed_operations]
|
||||
|
||||
case registry_result do
|
||||
{new_value, _old_value} when is_list(new_value) ->
|
||||
:ok
|
||||
@@ -464,14 +459,10 @@ defmodule WandererApp.Map.MapPool do
|
||||
raise "Failed to delete from cache: #{inspect(reason)}"
|
||||
end
|
||||
|
||||
completed_operations = [:cache | completed_operations]
|
||||
|
||||
# Step 3: Stop the map server (clean up all map resources)
|
||||
map_id
|
||||
|> Server.Impl.stop_map()
|
||||
|
||||
completed_operations = [:map_server | completed_operations]
|
||||
|
||||
# Step 4: Update GenServer state (last, as this is in-memory and fast)
|
||||
new_state = %{state | map_ids: map_ids |> Enum.reject(fn id -> id == map_id end)}
|
||||
|
||||
@@ -560,9 +551,6 @@ defmodule WandererApp.Map.MapPool do
|
||||
# and the cleanup operations are safe to leave in a "stopped" state
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:error, _, state), do: {:stop, :error, :ok, state}
|
||||
|
||||
@impl true
|
||||
def handle_info(:backup_state, %{map_ids: map_ids, uuid: uuid} = state) do
|
||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||
|
||||
@@ -179,15 +179,4 @@ defmodule WandererApp.Map.MapPoolDynamicSupervisor do
|
||||
{:ok, pid}
|
||||
end
|
||||
end
|
||||
|
||||
defp stop_child(uuid) do
|
||||
case Registry.lookup(@registry, uuid) do
|
||||
[{pid, _}] ->
|
||||
GenServer.cast(pid, :stop)
|
||||
|
||||
_ ->
|
||||
Logger.warn("Unable to locate pool assigned to #{inspect(uuid)}")
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -77,7 +77,7 @@ defmodule WandererApp.Map.Routes do
|
||||
end
|
||||
end
|
||||
|
||||
def find(_map_id, hubs, origin, routes_settings, true) do
|
||||
def find(_map_id, hubs, origin, _routes_settings, true) do
|
||||
origin = origin |> String.to_integer()
|
||||
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
|
||||
|
||||
|
||||
@@ -93,10 +93,8 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Determines the ship size for a connection, applying wormhole‑specific rules
|
||||
for C1, C13, and C4⇄NS links, falling back to the caller’s provided size or Large.
|
||||
"""
|
||||
# Determines the ship size for a connection, applying wormhole-specific rules
|
||||
# for C1, C13, and C4⇄NS links, falling back to the caller's provided size or Large.
|
||||
defp resolve_ship_size(type_val, ship_size_val, src_info, tgt_info) do
|
||||
case parse_type(type_val) do
|
||||
@connection_type_wormhole ->
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererApp.Map.Operations.Duplication do
|
||||
"""
|
||||
|
||||
require Logger
|
||||
import Ash.Query, only: [filter: 2]
|
||||
|
||||
alias WandererApp.Api
|
||||
alias WandererApp.Api.{MapSystem, MapConnection, MapSystemSignature, MapCharacterSettings}
|
||||
|
||||
@@ -68,6 +68,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
WandererApp.Map.update_map_state(map_id, %{
|
||||
map: Map.merge(old_map, map_update)
|
||||
})
|
||||
|
||||
# Broadcast to map channel so all viewers can refresh their available characters
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :acl_members_changed, %{})
|
||||
end
|
||||
|
||||
def handle_acl_updated(map_id, acl_id) do
|
||||
@@ -87,6 +90,10 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
acl_id
|
||||
|> update_acl()
|
||||
|> broadcast_acl_updates(map_id)
|
||||
|
||||
# Broadcast to map channel so all viewers can refresh their available characters
|
||||
# This fixes the issue where users don't see newly added ACL members as available for tracking
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :acl_members_changed, %{acl_id: acl_id})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -108,6 +115,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
|> Map.get(:characters, [])
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids)
|
||||
|
||||
# Broadcast to map channel so all viewers can refresh their available characters
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :acl_members_changed, %{})
|
||||
end
|
||||
|
||||
def track_acls([]), do: :ok
|
||||
|
||||
@@ -814,14 +814,16 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
do: :ok
|
||||
|
||||
defp update_location(
|
||||
%{map: %{scope: scope}, map_id: map_id, map_opts: map_opts} =
|
||||
%{map: map, map_id: map_id, map_opts: map_opts} =
|
||||
_state,
|
||||
character_id,
|
||||
location,
|
||||
old_location
|
||||
) do
|
||||
scopes = get_effective_scopes(map)
|
||||
|
||||
ConnectionsImpl.is_connection_valid(
|
||||
scope,
|
||||
scopes,
|
||||
old_location.solar_system_id,
|
||||
location.solar_system_id
|
||||
)
|
||||
@@ -879,6 +881,21 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
defp is_character_in_space?(%{station_id: station_id, structure_id: structure_id} = _location),
|
||||
do: is_nil(structure_id) && is_nil(station_id)
|
||||
|
||||
# Get effective scopes from map, with fallback to legacy scope
|
||||
defp get_effective_scopes(%{scopes: scopes}) when is_list(scopes) and scopes != [], do: scopes
|
||||
|
||||
defp get_effective_scopes(%{scope: scope}) when is_atom(scope),
|
||||
do: legacy_scope_to_scopes(scope)
|
||||
|
||||
defp get_effective_scopes(_), do: [:wormholes]
|
||||
|
||||
# Legacy scope to new scopes array conversion
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
defp add_character(
|
||||
map_id,
|
||||
%{id: character_id} = map_character,
|
||||
|
||||
@@ -57,6 +57,12 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
@known_space [@hs, @ls, @ns, @pochven]
|
||||
|
||||
# Individual space type lists for granular scope matching
|
||||
@hi_space [@hs]
|
||||
@low_space [@ls]
|
||||
@null_space [@ns]
|
||||
@pochven_space [@pochven]
|
||||
|
||||
@prohibited_systems [@jita]
|
||||
@prohibited_system_classes [
|
||||
@a1,
|
||||
@@ -100,7 +106,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
@connection_type_wormhole 0
|
||||
@connection_type_stargate 1
|
||||
@connection_type_bridge 2
|
||||
# @connection_type_bridge 2 # reserved for future use
|
||||
@medium_ship_size 1
|
||||
|
||||
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
|
||||
@@ -403,7 +409,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
time_status: time_status,
|
||||
solar_system_source: solar_system_source,
|
||||
solar_system_target: solar_system_target
|
||||
} = updated_connection
|
||||
} = _updated_connection
|
||||
) do
|
||||
with source_system when not is_nil(source_system) <-
|
||||
WandererApp.Map.find_system_by_location(
|
||||
@@ -644,31 +650,49 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
start_time
|
||||
)
|
||||
|
||||
def can_add_location(_scope, nil), do: false
|
||||
def can_add_location(_scopes, nil), do: false
|
||||
|
||||
def can_add_location(:none, _solar_system_id), do: false
|
||||
def can_add_location([], _solar_system_id), do: false
|
||||
|
||||
def can_add_location(scope, solar_system_id) do
|
||||
def can_add_location(scopes, solar_system_id) when is_list(scopes) do
|
||||
{:ok, system_static_info} = get_system_static_info(solar_system_id)
|
||||
|
||||
case scope do
|
||||
:wormholes ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
|
||||
@wh_space |> Enum.member?(system_static_info.system_class)
|
||||
|
||||
:stargates ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
@known_space |> Enum.member?(system_static_info.system_class)
|
||||
|
||||
:all ->
|
||||
not is_prohibited_system_class?(system_static_info.system_class)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
not is_prohibited_system_class?(system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(solar_system_id)) and
|
||||
system_matches_any_scope?(system_static_info.system_class, scopes)
|
||||
end
|
||||
|
||||
# Legacy support for single scope atom
|
||||
def can_add_location(:none, _solar_system_id), do: false
|
||||
|
||||
def can_add_location(scope, solar_system_id) when is_atom(scope) do
|
||||
can_add_location(legacy_scope_to_scopes(scope), solar_system_id)
|
||||
end
|
||||
|
||||
# Helper function to check if a system class matches any of the selected scopes
|
||||
defp system_matches_any_scope?(_system_class, []), do: false
|
||||
|
||||
defp system_matches_any_scope?(system_class, scopes) do
|
||||
Enum.any?(scopes, fn scope ->
|
||||
system_matches_scope?(system_class, scope)
|
||||
end)
|
||||
end
|
||||
|
||||
# Individual scope matching functions
|
||||
defp system_matches_scope?(system_class, :wormholes), do: system_class in @wh_space
|
||||
defp system_matches_scope?(system_class, :hi), do: system_class in @hi_space
|
||||
defp system_matches_scope?(system_class, :low), do: system_class in @low_space
|
||||
defp system_matches_scope?(system_class, :null), do: system_class in @null_space
|
||||
defp system_matches_scope?(system_class, :pochven), do: system_class in @pochven_space
|
||||
defp system_matches_scope?(_system_class, _), do: false
|
||||
|
||||
# Legacy scope to new scopes array conversion
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
def is_prohibited_system_class?(system_class) do
|
||||
@prohibited_system_classes |> Enum.member?(system_class)
|
||||
end
|
||||
@@ -688,17 +712,41 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
)
|
||||
)
|
||||
|
||||
def is_connection_valid(_scope, from_solar_system_id, to_solar_system_id)
|
||||
def is_connection_valid(_scopes, from_solar_system_id, to_solar_system_id)
|
||||
when is_nil(from_solar_system_id) or is_nil(to_solar_system_id),
|
||||
do: false
|
||||
|
||||
def is_connection_valid([], _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
# New array-based scopes support
|
||||
def is_connection_valid(scopes, from_solar_system_id, to_solar_system_id)
|
||||
when is_list(scopes) and from_solar_system_id != to_solar_system_id do
|
||||
with {:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
|
||||
{:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do
|
||||
# Connection is valid if:
|
||||
# 1. Neither system is prohibited
|
||||
# 2. At least one system matches one of the selected scopes
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
|
||||
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
|
||||
(system_matches_any_scope?(from_system_static_info.system_class, scopes) or
|
||||
system_matches_any_scope?(to_system_static_info.system_class, scopes))
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy support: :all scope
|
||||
def is_connection_valid(:all, from_solar_system_id, to_solar_system_id),
|
||||
do: from_solar_system_id != to_solar_system_id
|
||||
|
||||
# Legacy support: :none scope
|
||||
def is_connection_valid(:none, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
# Legacy support: single atom scope (including :stargates which is used for connection type detection)
|
||||
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
|
||||
when from_solar_system_id != to_solar_system_id do
|
||||
when is_atom(scope) and from_solar_system_id != to_solar_system_id do
|
||||
with {:ok, known_jumps} <- find_solar_system_jump(from_solar_system_id, to_solar_system_id),
|
||||
{:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
|
||||
{:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do
|
||||
@@ -712,7 +760,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
:stargates ->
|
||||
# For stargates, we need to check:
|
||||
# 1. Both systems are in known space (HS, LS, NS)
|
||||
# 1. Both systems are in known space (HS, LS, NS, Pochven)
|
||||
# 2. There is a known jump between them
|
||||
# 3. Neither system is prohibited
|
||||
from_system_static_info.system_class in @known_space and
|
||||
@@ -720,13 +768,21 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (known_jumps |> Enum.empty?())
|
||||
|
||||
_ ->
|
||||
# For other legacy scopes, convert to array and use new logic
|
||||
is_connection_valid(
|
||||
legacy_scope_to_scopes(scope),
|
||||
from_solar_system_id,
|
||||
to_solar_system_id
|
||||
)
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
def is_connection_valid(_scope, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
def is_connection_valid(_scopes, _from_solar_system_id, _to_solar_system_id), do: false
|
||||
|
||||
def get_connection_mark_eol_time(map_id, connection_id, default \\ DateTime.utc_now()) do
|
||||
WandererApp.Cache.get("map_#{map_id}:conn_#{connection_id}:mark_eol_time")
|
||||
@@ -859,14 +915,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
source_system_info.system_class == @c13 or target_system_info.system_class == @c13 ->
|
||||
@frigate_ship_size
|
||||
|
||||
# C4 to null gets frigate (unless C4 is shattered)
|
||||
(source_system_info.system_class == @c4 and target_system_info.system_class == @ns and
|
||||
not source_system_info.is_shattered) or
|
||||
(target_system_info.system_class == @c4 and
|
||||
source_system_info.system_class == @ns and
|
||||
not target_system_info.is_shattered) ->
|
||||
@frigate_ship_size
|
||||
|
||||
true ->
|
||||
# Default to large for other wormhole connections
|
||||
@large_ship_size
|
||||
@@ -909,9 +957,6 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_time_status(_source_solar_system_id, _target_solar_system_id, _ship_size_type),
|
||||
do: @connection_time_status_default
|
||||
|
||||
defp get_new_time_status(_start_time, @connection_time_status_default),
|
||||
do: @connection_time_status_eol_24
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Logger.error("Cannot start map #{map_id}: map not loaded")
|
||||
{:error, :map_not_loaded}
|
||||
|
||||
map ->
|
||||
_map ->
|
||||
with :ok <- AclsImpl.track_acls(acls |> Enum.map(& &1.access_list_id)) do
|
||||
@pubsub_client.subscribe(
|
||||
WandererApp.PubSub,
|
||||
|
||||
@@ -5,7 +5,7 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
|
||||
alias WandererApp.Map.Server.Impl
|
||||
|
||||
@ping_auto_expire_timeout :timer.minutes(15)
|
||||
# @ping_auto_expire_timeout :timer.minutes(15) # reserved for future use
|
||||
|
||||
def add_ping(
|
||||
map_id,
|
||||
|
||||
@@ -129,8 +129,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
def remove_system_comment(
|
||||
map_id,
|
||||
comment_id,
|
||||
user_id,
|
||||
character_id
|
||||
_user_id,
|
||||
_character_id
|
||||
) do
|
||||
{:ok, %{system_id: system_id} = comment} =
|
||||
WandererApp.MapSystemCommentRepo.get_by_id(comment_id)
|
||||
@@ -309,7 +309,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
map_id
|
||||
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
|
||||
|> case do
|
||||
{:ok, result} ->
|
||||
{:ok, _result} ->
|
||||
:ok = WandererApp.Map.remove_system(map_id, solar_system_id)
|
||||
@ddrt.delete([solar_system_id], "rtree_#{map_id}")
|
||||
Impl.broadcast!(map_id, :systems_removed, [solar_system_id])
|
||||
@@ -403,21 +403,41 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|> Enum.each(fn s ->
|
||||
try do
|
||||
{:ok, %{eve_id: eve_id, system: system}} = s |> Ash.load([:system])
|
||||
:ok = Ash.destroy!(s)
|
||||
|
||||
# Handle case where parent system was already deleted
|
||||
case system do
|
||||
nil ->
|
||||
Logger.warning(
|
||||
"[cleanup_linked_signatures] signature #{eve_id} destroyed (parent system already deleted)"
|
||||
# Use Ash.destroy (not destroy!) to handle already-deleted signatures gracefully
|
||||
case Ash.destroy(s) do
|
||||
:ok ->
|
||||
# Handle case where parent system was already deleted
|
||||
case system do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] signature #{eve_id} destroyed (parent system already deleted)"
|
||||
end)
|
||||
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] for system #{solar_system_id}: #{inspect(eve_id)}"
|
||||
end)
|
||||
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
end
|
||||
|
||||
{:error, %Ash.Error.Invalid{errors: errors}} ->
|
||||
# Check if this is a StaleRecord error (signature already deleted)
|
||||
if Enum.any?(errors, &match?(%Ash.Error.Changes.StaleRecord{}, &1)) do
|
||||
Logger.debug(fn ->
|
||||
"[cleanup_linked_signatures] signature #{eve_id} already deleted (StaleRecord)"
|
||||
end)
|
||||
else
|
||||
Logger.error(
|
||||
"[cleanup_linked_signatures] Failed to destroy signature #{eve_id}: #{inspect(errors)}"
|
||||
)
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error(
|
||||
"[cleanup_linked_signatures] Failed to destroy signature: #{inspect(error)}"
|
||||
)
|
||||
|
||||
%{solar_system_id: solar_system_id} ->
|
||||
Logger.warning(
|
||||
"[cleanup_linked_signatures] for system #{solar_system_id}: #{inspect(eve_id)}"
|
||||
)
|
||||
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@@ -679,7 +699,11 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
|
||||
_ ->
|
||||
%{x: x, y: y} =
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(nil, rtree_name, map_opts)
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(
|
||||
nil,
|
||||
rtree_name,
|
||||
map_opts
|
||||
)
|
||||
|
||||
%{"x" => x, "y" => y}
|
||||
end
|
||||
@@ -742,7 +766,10 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
})
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to get system static info for #{solar_system_id}: #{inspect(reason)}")
|
||||
Logger.error(
|
||||
"Failed to get system static info for #{solar_system_id}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, :system_info_not_found}
|
||||
end
|
||||
end
|
||||
@@ -775,7 +802,10 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
:ok
|
||||
|
||||
{:error, reason} = error ->
|
||||
Logger.error("Failed to add system #{solar_system_id} to map #{map_id}: #{inspect(reason)}")
|
||||
Logger.error(
|
||||
"Failed to add system #{solar_system_id} to map #{map_id}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
error
|
||||
end
|
||||
else
|
||||
@@ -863,10 +893,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
updated_system
|
||||
end
|
||||
|
||||
defp maybe_update_labels(system, _labels), do: system
|
||||
|
||||
defp maybe_update_labels(
|
||||
%{name: old_labels} = system,
|
||||
%{labels: old_labels} = system,
|
||||
labels
|
||||
)
|
||||
when not is_nil(labels) and old_labels != labels do
|
||||
|
||||
@@ -128,7 +128,7 @@ defmodule WandererApp.Maps do
|
||||
tracked: tracked
|
||||
}
|
||||
|
||||
defp get_map_characters(%{id: map_id} = map) do
|
||||
defp get_map_characters(%{id: map_id} = _map) do
|
||||
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|
||||
|> case do
|
||||
nil ->
|
||||
@@ -174,9 +174,11 @@ defmodule WandererApp.Maps do
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}
|
||||
|
||||
# Cache with 5 minute TTL so ACL changes are picked up even when map server isn't running
|
||||
WandererApp.Cache.insert(
|
||||
"map_characters-#{map_id}",
|
||||
map_characters
|
||||
map_characters,
|
||||
ttl: :timer.minutes(5)
|
||||
)
|
||||
|
||||
{:ok, map_characters}
|
||||
|
||||
@@ -99,7 +99,7 @@ defmodule WandererApp.MapConnectionRepo do
|
||||
def get_by_id(map_id, id) do
|
||||
# Use read_by_map action which doesn't have the FilterConnectionsByActorMap preparation
|
||||
# that was causing "filter being false" errors in tests
|
||||
import Ash.Query
|
||||
require Ash.Query
|
||||
|
||||
WandererApp.Api.MapConnection
|
||||
|> Ash.Query.for_read(:read_by_map, %{map_id: map_id})
|
||||
|
||||
@@ -38,6 +38,4 @@ defmodule WandererApp.MapPingsRepo do
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def destroy(_ping_id), do: :ok
|
||||
end
|
||||
|
||||
@@ -84,7 +84,7 @@ defmodule WandererApp.MapRepo do
|
||||
end
|
||||
end
|
||||
|
||||
error in Ash.Error.Query.NotFound ->
|
||||
_error in Ash.Error.Query.NotFound ->
|
||||
Logger.debug("Map not found with slug: #{slug}")
|
||||
{:error, :not_found}
|
||||
|
||||
|
||||
@@ -487,15 +487,6 @@ defmodule WandererApp.SecurityAudit do
|
||||
|
||||
# Private functions
|
||||
|
||||
defp store_audit_entry(_audit_entry) do
|
||||
# Handle async processing if enabled
|
||||
# if async_enabled?() do
|
||||
# WandererApp.SecurityAudit.AsyncProcessor.log_event(audit_entry)
|
||||
# else
|
||||
# do_store_audit_entry(audit_entry)
|
||||
# end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def do_store_audit_entry(audit_entry) do
|
||||
# Ensure event_type is properly formatted
|
||||
@@ -631,11 +622,6 @@ defmodule WandererApp.SecurityAudit do
|
||||
end
|
||||
end
|
||||
|
||||
defp async_enabled? do
|
||||
Application.get_env(:wanderer_app, __MODULE__, [])
|
||||
|> Keyword.get(:async, false)
|
||||
end
|
||||
|
||||
defp emit_telemetry_event(audit_entry) do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :security_audit],
|
||||
|
||||
@@ -5,7 +5,11 @@ defmodule WandererApp.Test.Logger do
|
||||
"""
|
||||
|
||||
@callback info(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback info(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback error(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback error(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback warning(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback warning(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
@callback debug(message :: iodata() | (-> iodata())) :: :ok
|
||||
@callback debug(message :: iodata() | (-> iodata()), metadata :: keyword()) :: :ok
|
||||
end
|
||||
|
||||
@@ -9,12 +9,24 @@ defmodule WandererApp.Test.LoggerStub do
|
||||
@impl true
|
||||
def info(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def info(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def error(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def error(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def warning(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def warning(_message, _metadata), do: :ok
|
||||
|
||||
@impl true
|
||||
def debug(_message), do: :ok
|
||||
|
||||
@impl true
|
||||
def debug(_message, _metadata), do: :ok
|
||||
end
|
||||
|
||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Vault do
|
||||
end)
|
||||
end
|
||||
|
||||
defp find_fallback_module_to_decrypt(config, ciphertext) do
|
||||
defp find_fallback_module_to_decrypt(config, _ciphertext) do
|
||||
Enum.find(config[:ciphers], fn {label, _} ->
|
||||
label == :fallback
|
||||
end)
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererAppWeb.ApiRouter do
|
||||
"""
|
||||
|
||||
use Phoenix.Router
|
||||
import WandererAppWeb.ApiRouterHelpers
|
||||
alias WandererAppWeb.{ApiRoutes, ApiRouter.RouteSpec}
|
||||
require Logger
|
||||
|
||||
@@ -171,7 +170,7 @@ defmodule WandererAppWeb.ApiRouter do
|
||||
|> halt()
|
||||
end
|
||||
|
||||
defp find_similar_routes(path_info, version) do
|
||||
defp find_similar_routes(path_info, _version) do
|
||||
# Find routes with similar paths in current or other versions
|
||||
all_routes = ApiRoutes.table()
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
defmodule WandererAppWeb.ApiSpec do
|
||||
@behaviour OpenApiSpex.OpenApi
|
||||
|
||||
alias OpenApiSpex.{OpenApi, Info, Paths, Components, SecurityScheme, Server, Schema}
|
||||
alias OpenApiSpex.{OpenApi, Info, Paths, Components, SecurityScheme, Server}
|
||||
alias WandererAppWeb.{Endpoint, Router}
|
||||
alias WandererAppWeb.Schemas.ApiSchemas
|
||||
|
||||
|
||||
@@ -284,6 +284,7 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
"""
|
||||
attr(:type, :string, default: nil)
|
||||
attr(:class, :string, default: nil)
|
||||
attr(:data, :any, default: nil)
|
||||
attr(:rest, :global, include: ~w(disabled form name value))
|
||||
|
||||
slot(:inner_block, required: true)
|
||||
@@ -296,6 +297,7 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
"phx-submit-loading:opacity-75 p-button p-component p-button-outlined p-button-sm",
|
||||
@class
|
||||
]}
|
||||
data={@data}
|
||||
{@rest}
|
||||
>
|
||||
{render_slot(@inner_block)}
|
||||
@@ -614,7 +616,7 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
attr(:empty_label, :string, default: nil)
|
||||
attr(:rows, :list, required: true)
|
||||
attr(:row_id, :any, default: nil, doc: "the function for generating the row id")
|
||||
attr(:row_selected, :boolean, default: false, doc: "the function for generating the row id")
|
||||
attr(:row_selected, :any, default: false, doc: "the function for generating the row id")
|
||||
attr(:row_click, :any, default: nil, doc: "the function for handling phx-click on each row")
|
||||
|
||||
attr(:row_item, :any,
|
||||
@@ -703,13 +705,21 @@ defmodule WandererAppWeb.CoreComponents do
|
||||
"""
|
||||
end
|
||||
|
||||
attr(:field, :any, required: true)
|
||||
attr(:placeholder, :string, default: nil)
|
||||
attr(:label, :string, default: nil)
|
||||
attr(:label_class, :string, default: nil)
|
||||
attr(:input_class, :string, default: nil)
|
||||
attr(:dropdown_extra_class, :string, default: nil)
|
||||
attr(:option_extra_class, :string, default: nil)
|
||||
attr(:mode, :atom, default: :single)
|
||||
attr(:options, :list, default: [])
|
||||
attr(:debounce, :integer, default: nil)
|
||||
attr(:update_min_len, :integer, default: nil)
|
||||
attr(:available_option_class, :string, default: nil)
|
||||
attr(:value_mapper, :any, default: nil)
|
||||
slot(:inner_block)
|
||||
slot(:option)
|
||||
|
||||
def live_select(%{field: %Phoenix.HTML.FormField{} = field} = assigns) do
|
||||
assigns =
|
||||
|
||||
@@ -433,6 +433,10 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
# Invalidate map_characters cache for all maps using this ACL
|
||||
# This ensures the tracking page shows updated members even when map server isn't running
|
||||
invalidate_map_characters_cache(acl_id)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
@@ -440,6 +444,21 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
)
|
||||
end
|
||||
|
||||
defp invalidate_map_characters_cache(acl_id) do
|
||||
case Ash.read(
|
||||
WandererApp.Api.MapAccessList
|
||||
|> Ash.Query.for_read(:read_by_acl, %{acl_id: acl_id})
|
||||
) do
|
||||
{:ok, map_acls} ->
|
||||
Enum.each(map_acls, fn %{map_id: map_id} ->
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning("Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp member_to_json(member) do
|
||||
base = %{
|
||||
|
||||
@@ -123,12 +123,6 @@ defmodule WandererAppWeb.LicenseApiController do
|
||||
end
|
||||
end
|
||||
|
||||
def update_validity(conn, %{"id" => _license_id}) do
|
||||
conn
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Missing required parameter: is_valid"})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Updates a license's expiration date.
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
use WandererAppWeb, :controller
|
||||
use OpenApiSpex.ControllerSpecs
|
||||
|
||||
import Ash.Query, only: [filter: 2]
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Api.Character
|
||||
alias WandererApp.MapSystemRepo
|
||||
alias WandererApp.MapCharacterSettingsRepo
|
||||
alias WandererApp.MapConnectionRepo
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
alias WandererAppWeb.Schemas.{ApiSchemas, ResponseSchemas}
|
||||
@@ -16,7 +14,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
# V1 API Actions (for compatibility with versioned API router)
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
def index_v1(conn, params) do
|
||||
def index_v1(conn, _params) do
|
||||
# Delegate to the existing list implementation or create a basic one
|
||||
json(conn, %{
|
||||
data: [],
|
||||
@@ -43,7 +41,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def create_v1(conn, params) do
|
||||
def create_v1(conn, _params) do
|
||||
# Basic create implementation for testing
|
||||
json(conn, %{
|
||||
data: %{
|
||||
@@ -59,7 +57,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def update_v1(conn, %{"id" => id} = params) do
|
||||
def update_v1(conn, %{"id" => id} = _params) do
|
||||
# Basic update implementation for testing
|
||||
json(conn, %{
|
||||
data: %{
|
||||
@@ -82,7 +80,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
|> text("")
|
||||
end
|
||||
|
||||
def duplicate_v1(conn, %{"id" => id} = params) do
|
||||
def duplicate_v1(conn, %{"id" => id} = _params) do
|
||||
# Basic duplicate implementation for testing
|
||||
json(conn, %{
|
||||
data: %{
|
||||
@@ -99,7 +97,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def bulk_create_v1(conn, params) do
|
||||
def bulk_create_v1(conn, _params) do
|
||||
# Basic bulk create implementation for testing
|
||||
json(conn, %{
|
||||
data: [
|
||||
@@ -121,7 +119,7 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
})
|
||||
end
|
||||
|
||||
def bulk_update_v1(conn, params) do
|
||||
def bulk_update_v1(conn, _params) do
|
||||
# Basic bulk update implementation for testing
|
||||
json(conn, %{
|
||||
data: [
|
||||
@@ -325,13 +323,6 @@ defmodule WandererAppWeb.MapAPIController do
|
||||
# Helper functions for the API controller
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
defp get_map_id_by_slug(slug) do
|
||||
case WandererApp.Api.Map.get_map_by_slug(slug) do
|
||||
{:ok, map} -> {:ok, map.id}
|
||||
{:error, error} -> {:error, "Map not found for slug: #{slug}, error: #{inspect(error)}"}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_map_identifier(params) do
|
||||
case Map.get(params, "map_identifier") do
|
||||
nil ->
|
||||
|
||||
@@ -4,8 +4,6 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Api
|
||||
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@@ -65,24 +65,6 @@ defmodule WandererAppWeb.MapEventsAPIController do
|
||||
items: @event_schema
|
||||
})
|
||||
|
||||
@events_list_params %OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
since: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
format: :date_time,
|
||||
description: "Return events after this timestamp (ISO8601)"
|
||||
},
|
||||
limit: %OpenApiSpex.Schema{
|
||||
type: :integer,
|
||||
minimum: 1,
|
||||
maximum: 100,
|
||||
default: 100,
|
||||
description: "Maximum number of events to return"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# -----------------------------------------------------------------
|
||||
# OpenApiSpex Operations
|
||||
# -----------------------------------------------------------------
|
||||
@@ -173,7 +155,7 @@ defmodule WandererAppWeb.MapEventsAPIController do
|
||||
|> put_status(:bad_request)
|
||||
|> json(%{error: "Invalid 'limit' parameter. Must be between 1 and 100."})
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> put_status(:internal_server_error)
|
||||
|> json(%{error: "Internal server error"})
|
||||
@@ -184,7 +166,7 @@ defmodule WandererAppWeb.MapEventsAPIController do
|
||||
# Private Functions
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
defp get_map(conn, map_identifier) do
|
||||
defp get_map(conn, _map_identifier) do
|
||||
# The map should already be loaded by the CheckMapApiKey plug
|
||||
case conn.assigns[:map] do
|
||||
nil -> {:error, :map_not_found}
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule WandererAppWeb.Plugs.JsonApiPerformanceMonitor do
|
||||
conn
|
||||
|> register_before_send(fn conn ->
|
||||
end_time = System.monotonic_time(:millisecond)
|
||||
duration = end_time - start_time
|
||||
_duration = end_time - start_time
|
||||
|
||||
# Extract response metadata
|
||||
response_metadata = extract_response_metadata(conn, request_metadata)
|
||||
|
||||
@@ -12,7 +12,6 @@ defmodule WandererAppWeb.Plugs.LicenseAuth do
|
||||
require Logger
|
||||
|
||||
alias WandererApp.License.LicenseManager
|
||||
alias WandererApp.Helpers.Config
|
||||
|
||||
@doc """
|
||||
Authenticates requests using the LM_AUTH_KEY.
|
||||
@@ -21,7 +20,7 @@ defmodule WandererAppWeb.Plugs.LicenseAuth do
|
||||
"""
|
||||
def authenticate_lm(conn, _opts) do
|
||||
auth_header = get_req_header(conn, "authorization")
|
||||
lm_auth_key = Config.get_env(:wanderer_app, :lm_auth_key)
|
||||
lm_auth_key = Application.get_env(:wanderer_app, :lm_auth_key)
|
||||
|
||||
case auth_header do
|
||||
["Bearer " <> token] ->
|
||||
|
||||
@@ -37,7 +37,7 @@ defmodule WandererAppWeb.UserAuth do
|
||||
nil ->
|
||||
{:halt, redirect_require_login(socket)}
|
||||
|
||||
%User{characters: characters} ->
|
||||
%User{characters: _characters} ->
|
||||
{:cont, new_socket}
|
||||
end
|
||||
|
||||
@@ -112,13 +112,6 @@ defmodule WandererAppWeb.UserAuth do
|
||||
|> LiveView.redirect(to: ~p"/")
|
||||
end
|
||||
|
||||
defp track_characters([]), do: :ok
|
||||
|
||||
defp track_characters([%{id: character_id} | characters]) do
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
track_characters(characters)
|
||||
end
|
||||
|
||||
defp maybe_store_return_to(%{method: "GET"} = conn) do
|
||||
%{request_path: request_path, query_string: query_string} = conn
|
||||
return_to = if query_string == "", do: request_path, else: request_path <> "?" <> query_string
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
use WandererAppWeb, :live_view
|
||||
|
||||
alias WandererApp.ExternalEvents.AclEventBroadcaster
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
@@ -281,11 +282,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> Enum.find(&(&1.id == member_id))
|
||||
|> WandererApp.Api.AccessListMember.destroy!()
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{socket.assigns.selected_acl_id}",
|
||||
{:acl_updated, %{acl_id: socket.assigns.selected_acl_id}}
|
||||
)
|
||||
broadcast_acl_updated(socket.assigns.selected_acl_id)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
@@ -444,11 +441,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :update], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{socket.assigns.selected_acl_id}",
|
||||
{:acl_updated, %{acl_id: socket.assigns.selected_acl_id}}
|
||||
)
|
||||
broadcast_acl_updated(socket.assigns.selected_acl_id)
|
||||
|
||||
socket
|
||||
|> assign(
|
||||
@@ -574,11 +567,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{access_list_id}",
|
||||
{:acl_updated, %{acl_id: access_list_id}}
|
||||
)
|
||||
broadcast_acl_updated(access_list_id)
|
||||
|
||||
{:ok, member}
|
||||
|
||||
@@ -613,11 +602,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{access_list_id}",
|
||||
{:acl_updated, %{acl_id: access_list_id}}
|
||||
)
|
||||
broadcast_acl_updated(access_list_id)
|
||||
|
||||
{:ok, member}
|
||||
|
||||
@@ -653,11 +638,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|
||||
:telemetry.execute([:wanderer_app, :acl, :member, :add], %{count: 1})
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{access_list_id}",
|
||||
{:acl_updated, %{acl_id: access_list_id}}
|
||||
)
|
||||
broadcast_acl_updated(access_list_id)
|
||||
|
||||
{:ok, member}
|
||||
|
||||
@@ -688,7 +669,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
"""
|
||||
end
|
||||
|
||||
slot(:option)
|
||||
attr(:option, :any, required: true)
|
||||
|
||||
def search_member_item(assigns) do
|
||||
~H"""
|
||||
@@ -737,4 +718,31 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
defp map_ui_acl(acl, selected_id) do
|
||||
acl |> Map.put(:selected, acl.id == selected_id)
|
||||
end
|
||||
|
||||
# Broadcast ACL update and invalidate map_characters cache for all maps using this ACL
|
||||
# This ensures the tracking page shows updated members even when map server isn't running
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
invalidate_map_characters_cache(acl_id)
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
{:acl_updated, %{acl_id: acl_id}}
|
||||
)
|
||||
end
|
||||
|
||||
defp invalidate_map_characters_cache(acl_id) do
|
||||
case Ash.read(
|
||||
WandererApp.Api.MapAccessList
|
||||
|> Ash.Query.for_read(:read_by_acl, %{acl_id: acl_id})
|
||||
) do
|
||||
{:ok, map_acls} ->
|
||||
Enum.each(map_acls, fn %{map_id: map_id} ->
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
end)
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning("Failed to invalidate map_characters cache for ACL #{acl_id}: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,13 +115,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<.link
|
||||
disabled={@selected_acl_id == "" or not can_add_members?(@access_list, @current_user)}
|
||||
:if={@selected_acl_id != "" and can_add_members?(@access_list, @current_user)}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none"
|
||||
patch={~p"/access-lists/#{@selected_acl_id}/add-members"}
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</.link>
|
||||
<div
|
||||
:if={@selected_acl_id == "" or not can_add_members?(@access_list, @current_user)}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none btn-disabled"
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">Add Members</h3>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,6 @@ defmodule WandererAppWeb.AdminLive do
|
||||
require Logger
|
||||
alias BetterNumber, as: Number
|
||||
|
||||
@invite_link_ttl :timer.hours(24)
|
||||
|
||||
def mount(_params, %{"user_id" => user_id} = _session, socket)
|
||||
when not is_nil(user_id) do
|
||||
WandererApp.StartCorpWalletTrackerTask.maybe_start_corp_wallet_tracker(
|
||||
|
||||
@@ -209,7 +209,7 @@
|
||||
rows={@transactions}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={transaction}>
|
||||
<:col :let={_transaction}>
|
||||
<div class=" text-22">
|
||||
<.icon name="hero-credit-card-solid" class="h-5 w-5" />
|
||||
</div>
|
||||
@@ -267,7 +267,7 @@
|
||||
rows={@active_map_subscriptions}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={subscription}>
|
||||
<:col :let={_subscription}>
|
||||
<div class=" text-22">
|
||||
<.icon name="hero-check-badge-solid" class="w-5 h-5" />
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,8 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Character.TrackingUtils
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok, maps} = WandererApp.Maps.get_available_maps(socket.assigns.current_user)
|
||||
@@ -18,22 +20,19 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(characters: [], selected_map: nil, maps: [])}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_params(params, _url, socket) do
|
||||
{:noreply, apply_action(socket, socket.assigns.live_action, params)}
|
||||
end
|
||||
|
||||
defp apply_action(socket, :index, _params) do
|
||||
# Unsubscribe from previous map if any
|
||||
socket = maybe_unsubscribe_from_map(socket)
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :characters_tracking)
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
|> assign(selected_map: nil, selected_map_slug: nil)
|
||||
end
|
||||
|
||||
defp apply_action(
|
||||
@@ -43,6 +42,10 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
) do
|
||||
selected_map = maps |> Enum.find(&(&1.slug == map_slug))
|
||||
|
||||
# Unsubscribe from previous map and subscribe to new one
|
||||
socket = maybe_unsubscribe_from_map(socket)
|
||||
socket = maybe_subscribe_to_map(socket, selected_map)
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :characters_tracking)
|
||||
|> assign(:page_title, "Characters Tracking")
|
||||
@@ -55,6 +58,27 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
end)
|
||||
end
|
||||
|
||||
# Subscribe to map PubSub channel to receive ACL update notifications
|
||||
defp maybe_subscribe_to_map(socket, nil), do: socket
|
||||
|
||||
defp maybe_subscribe_to_map(socket, %{id: map_id}) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(WandererApp.PubSub, map_id)
|
||||
end
|
||||
|
||||
socket
|
||||
end
|
||||
|
||||
# Unsubscribe from previous map's PubSub channel
|
||||
defp maybe_unsubscribe_from_map(%{assigns: %{selected_map: nil}} = socket), do: socket
|
||||
|
||||
defp maybe_unsubscribe_from_map(%{assigns: %{selected_map: %{id: map_id}}} = socket) do
|
||||
Phoenix.PubSub.unsubscribe(WandererApp.PubSub, map_id)
|
||||
socket
|
||||
end
|
||||
|
||||
defp maybe_unsubscribe_from_map(socket), do: socket
|
||||
|
||||
@impl true
|
||||
def handle_event("select_map_" <> map_slug, _, socket) do
|
||||
{:noreply,
|
||||
@@ -77,21 +101,30 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
case characters |> Enum.find(&(&1.id == character_id)) do
|
||||
%{tracked: false} ->
|
||||
WandererApp.MapCharacterSettingsRepo.track(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
%{tracked: current_tracked, eve_id: eve_id} ->
|
||||
# Use TrackingUtils.update_tracking to properly set/unset the tracking_start_time
|
||||
# cache key, which is required for the character to appear in get_tracked_character_ids
|
||||
case TrackingUtils.update_tracking(
|
||||
selected_map.id,
|
||||
eve_id,
|
||||
current_user.id,
|
||||
not current_tracked,
|
||||
self(),
|
||||
false
|
||||
) do
|
||||
{:ok, _tracking_data, _event} ->
|
||||
:ok
|
||||
|
||||
%{tracked: true} ->
|
||||
WandererApp.MapCharacterSettingsRepo.untrack(%{
|
||||
character_id: character_id,
|
||||
map_id: selected_map.id
|
||||
})
|
||||
{:error, reason} ->
|
||||
Logger.error(
|
||||
"Failed to toggle tracking for character #{character_id} on map #{selected_map.id}: #{inspect(reason)}"
|
||||
)
|
||||
end
|
||||
|
||||
WandererApp.Map.Server.untrack_characters(selected_map.id, [
|
||||
character_id
|
||||
])
|
||||
nil ->
|
||||
Logger.warning(
|
||||
"Character #{character_id} not found in available characters for map #{selected_map.id}"
|
||||
)
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
@@ -111,6 +144,20 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Handle ACL members changed event - reload characters list
|
||||
@impl true
|
||||
def handle_info(
|
||||
%{event: :acl_members_changed},
|
||||
%{assigns: %{selected_map: selected_map, current_user: current_user}} = socket
|
||||
)
|
||||
when not is_nil(selected_map) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_async(:characters, fn ->
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
end)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_event, socket), do: {:noreply, socket}
|
||||
end
|
||||
|
||||
@@ -79,7 +79,7 @@ defmodule WandererAppWeb.MapSubscription do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
defp get_title(%{plan: plan, auto_renew?: auto_renew?, active_till: active_till} = subscription) do
|
||||
defp get_title(%{plan: plan, auto_renew?: auto_renew?, active_till: active_till}) do
|
||||
if plan != :alpha do
|
||||
"Active subscription: omega \nActive till: #{Calendar.strftime(active_till, "%m/%d/%Y")} \nAuto renew: #{auto_renew?}"
|
||||
else
|
||||
|
||||
@@ -300,13 +300,13 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
%{"character_eve_id" => character_eve_id},
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
current_user: %{id: current_user_id}
|
||||
map_id: _map_id,
|
||||
current_user: %{id: _current_user_id}
|
||||
}
|
||||
} = socket
|
||||
)
|
||||
when not is_nil(character_eve_id) do
|
||||
{:ok, character} = WandererApp.Character.get_by_eve_id("#{character_eve_id}")
|
||||
{:ok, _character} = WandererApp.Character.get_by_eve_id("#{character_eve_id}")
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
@@ -338,12 +338,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
station_id: character.station_id
|
||||
}
|
||||
|
||||
defp get_map_with_acls(map_id) do
|
||||
with {:ok, map} <- WandererApp.Api.Map.by_id(map_id) do
|
||||
{:ok, Ash.load!(map, :acls)}
|
||||
end
|
||||
end
|
||||
|
||||
def needs_tracking_setup?(
|
||||
only_tracked_characters,
|
||||
characters,
|
||||
|
||||
@@ -17,6 +17,13 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
socket
|
||||
end
|
||||
|
||||
def handle_server_event(%{event: :acl_members_changed, payload: _payload}, socket) do
|
||||
# ACL members have changed - notify frontend to refresh tracking data
|
||||
# This ensures users see newly added characters as available for tracking
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("refresh_tracking_data", %{})
|
||||
end
|
||||
|
||||
def handle_server_event(
|
||||
:refresh_permissions,
|
||||
%{assigns: %{current_user: current_user, map_slug: map_slug}} = socket
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule WandererAppWeb.MapKillsEventHandler do
|
||||
|
||||
def handle_server_event(
|
||||
%{event: :init_kills},
|
||||
%{assigns: %{map_id: map_id} = assigns} = socket
|
||||
%{assigns: %{map_id: map_id} = _assigns} = socket
|
||||
) do
|
||||
# Get kill counts from cache
|
||||
case WandererApp.Map.get_map(map_id) do
|
||||
|
||||
@@ -3,7 +3,7 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
use Phoenix.Component
|
||||
require Logger
|
||||
|
||||
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler, MapSystemsEventHandler}
|
||||
alias WandererAppWeb.{MapEventHandler, MapCoreEventHandler}
|
||||
|
||||
def handle_server_event(
|
||||
%{
|
||||
|
||||
@@ -168,7 +168,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
current_user: %{id: current_user_id},
|
||||
map_id: map_id,
|
||||
main_character_id: main_character_id,
|
||||
map_user_settings: map_user_settings,
|
||||
map_user_settings: _map_user_settings,
|
||||
user_permissions: %{update_system: true}
|
||||
} = assigns
|
||||
} = socket
|
||||
@@ -380,7 +380,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
|
||||
def handle_ui_event(
|
||||
"undo_delete_signatures",
|
||||
%{"system_id" => solar_system_id, "eve_ids" => eve_ids} = payload,
|
||||
%{"system_id" => solar_system_id, "eve_ids" => eve_ids} = _payload,
|
||||
%{
|
||||
assigns: %{
|
||||
map_id: map_id,
|
||||
|
||||
@@ -97,7 +97,7 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
%{"solarSystemId" => solar_system_id} = _event,
|
||||
%{
|
||||
assigns: %{
|
||||
current_user: current_user,
|
||||
current_user: _current_user,
|
||||
has_tracked_characters?: true,
|
||||
map_id: map_id,
|
||||
user_permissions: %{add_system: true}
|
||||
@@ -109,7 +109,7 @@ defmodule WandererAppWeb.MapSystemCommentsEventHandler do
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
|> case do
|
||||
%{id: system_id} = system when not is_nil(system_id) ->
|
||||
%{id: system_id} = _system when not is_nil(system_id) ->
|
||||
{:ok, comments} = WandererApp.MapSystemCommentRepo.get_by_system(system_id)
|
||||
|
||||
{:reply,
|
||||
|
||||
@@ -5,8 +5,6 @@ defmodule WandererAppWeb.MapAuditLive do
|
||||
|
||||
alias WandererAppWeb.UserActivity
|
||||
|
||||
@active_subscription_periods ["2M", "3M"]
|
||||
|
||||
def mount(
|
||||
%{"slug" => map_slug, "period" => period, "activity" => activity} = _params,
|
||||
_session,
|
||||
|
||||
@@ -157,7 +157,7 @@ defmodule WandererAppWeb.MapCharactersLive do
|
||||
|> assign(:groups, groups)
|
||||
end
|
||||
|
||||
defp map_ui_character(map_id, character) do
|
||||
defp map_ui_character(_map_id, character) do
|
||||
character
|
||||
|> Map.take([
|
||||
:id,
|
||||
|
||||
@@ -230,6 +230,7 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
def handle_event(socket, {:DOWN, ref, :process, _pid, reason}) when is_reference(ref) do
|
||||
# Task failed, log the error and update the client
|
||||
Logger.error("Task failed: #{inspect(reason)}")
|
||||
socket
|
||||
end
|
||||
|
||||
def handle_event(socket, event),
|
||||
|
||||
@@ -112,13 +112,6 @@ defmodule WandererAppWeb.MapLive do
|
||||
|> WandererAppWeb.MapEventHandler.handle_event(info)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(info, socket),
|
||||
do:
|
||||
{:noreply,
|
||||
socket
|
||||
|> WandererAppWeb.MapEventHandler.handle_event(info)}
|
||||
|
||||
@impl true
|
||||
def handle_event("change_subscription_tab", %{"tab" => tab}, socket),
|
||||
do: {:noreply, socket |> assign(active_subscription_tab: tab)}
|
||||
|
||||
@@ -5,7 +5,6 @@ defmodule WandererAppWeb.Maps.MapBalanceComponent do
|
||||
require Logger
|
||||
|
||||
alias BetterNumber, as: Number
|
||||
alias WandererApp.License.LicenseManager
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
@@ -99,7 +98,7 @@ defmodule WandererAppWeb.Maps.MapBalanceComponent do
|
||||
type: :in
|
||||
})
|
||||
|
||||
{:ok, user} =
|
||||
{:ok, _user} =
|
||||
user
|
||||
|> WandererApp.Api.User.update_balance(%{
|
||||
balance: (user_balance || 0.0) - amount
|
||||
|
||||
@@ -77,7 +77,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
|> assign(:active_page, :maps)
|
||||
|> assign(:uri, URI.parse(url) |> Map.put(:path, ~p"/"))
|
||||
|> assign(:page_title, "Maps - Create")
|
||||
|> assign(:scopes, ["wormholes", "stargates", "none", "all"])
|
||||
|> assign(:available_scopes, available_scopes())
|
||||
|> assign(
|
||||
:form,
|
||||
AshPhoenix.Form.for_create(WandererApp.Api.Map, :new,
|
||||
@@ -86,7 +86,8 @@ defmodule WandererAppWeb.MapsLive do
|
||||
],
|
||||
prepare_source: fn form ->
|
||||
form
|
||||
|> Map.put("scope", "wormholes")
|
||||
# Default to wormholes scope for new maps
|
||||
|> Map.put("scopes", [:wormholes])
|
||||
end
|
||||
)
|
||||
)
|
||||
@@ -115,6 +116,9 @@ defmodule WandererAppWeb.MapsLive do
|
||||
_ -> map |> map_map()
|
||||
end
|
||||
|
||||
# Auto-initialize scopes from legacy scope if scopes is empty/nil
|
||||
map = maybe_initialize_scopes_from_legacy(map)
|
||||
|
||||
# Add owner to characters list, filtering out nil values
|
||||
characters =
|
||||
[map.owner |> map_character() | socket.assigns.characters]
|
||||
@@ -125,7 +129,7 @@ defmodule WandererAppWeb.MapsLive do
|
||||
|> assign(:active_page, :maps)
|
||||
|> assign(:uri, URI.parse(url) |> Map.put(:path, ~p"/"))
|
||||
|> assign(:page_title, "Maps - Edit")
|
||||
|> assign(:scopes, ["wormholes", "stargates", "none", "all"])
|
||||
|> assign(:available_scopes, available_scopes())
|
||||
|> assign(:map_slug, map_slug)
|
||||
|> assign(:characters, characters)
|
||||
|> assign(
|
||||
@@ -215,13 +219,6 @@ defmodule WandererAppWeb.MapsLive do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("set-default-scope", %{"id" => id}, socket) do
|
||||
send_update(LiveSelect.Component, options: ["wormholes", "stargates", "none", "all"], id: id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("generate-map-api-key", _params, socket) do
|
||||
new_api_key = UUID.uuid4()
|
||||
|
||||
@@ -257,27 +254,25 @@ defmodule WandererAppWeb.MapsLive do
|
||||
@impl true
|
||||
def handle_event(
|
||||
"live_select_change",
|
||||
%{"id" => id, "text" => text} = _change_event,
|
||||
%{"id" => id, "text" => _text} = _change_event,
|
||||
socket
|
||||
) do
|
||||
options =
|
||||
if text == "" do
|
||||
socket.assigns.scopes
|
||||
else
|
||||
socket.assigns.scopes
|
||||
end
|
||||
|
||||
send_update(LiveSelect.Component, options: options, id: id)
|
||||
# This handler is for ACL live_select component
|
||||
send_update(LiveSelect.Component, options: socket.assigns.acls, id: id)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("validate", %{"form" => form} = _params, socket) do
|
||||
# Process scopes from checkbox form data
|
||||
scopes = parse_scopes_from_form(form)
|
||||
|
||||
form =
|
||||
AshPhoenix.Form.validate(
|
||||
socket.assigns.form,
|
||||
form
|
||||
|> Map.put("acls", form["acls"] || [])
|
||||
|> Map.put("scopes", scopes)
|
||||
|> Map.put(
|
||||
"only_tracked_characters",
|
||||
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
|
||||
@@ -293,15 +288,10 @@ defmodule WandererAppWeb.MapsLive do
|
||||
%{assigns: %{current_user: current_user}} = socket
|
||||
)
|
||||
when not is_nil(current_user) do
|
||||
scope =
|
||||
form
|
||||
|> Map.get("scope")
|
||||
|> case do
|
||||
"" -> "wormholes"
|
||||
scope -> scope
|
||||
end
|
||||
# Process scopes from checkbox form data
|
||||
scopes = parse_scopes_from_form(form)
|
||||
|
||||
form = form |> Map.put("scope", scope)
|
||||
form = form |> Map.put("scopes", scopes)
|
||||
|
||||
case WandererApp.Api.Map.new(form) do
|
||||
{:ok, new_map} ->
|
||||
@@ -426,18 +416,13 @@ defmodule WandererAppWeb.MapsLive do
|
||||
# Successfully found the map, proceed with loading and updating
|
||||
{:ok, map_with_acls} = Ash.load(map, :acls)
|
||||
|
||||
scope =
|
||||
form
|
||||
|> Map.get("scope")
|
||||
|> case do
|
||||
"" -> "wormholes"
|
||||
scope -> scope
|
||||
end
|
||||
# Process scopes from checkbox form data
|
||||
scopes = parse_scopes_from_form(form)
|
||||
|
||||
form =
|
||||
form
|
||||
|> Map.put("acls", form["acls"] || [])
|
||||
|> Map.put("scope", scope)
|
||||
|> Map.put("scopes", scopes)
|
||||
|> Map.put(
|
||||
"only_tracked_characters",
|
||||
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
|
||||
@@ -820,4 +805,74 @@ defmodule WandererAppWeb.MapsLive do
|
||||
map
|
||||
|> Map.put(:acls, acls |> Enum.map(&map_acl/1))
|
||||
end
|
||||
|
||||
defp available_scopes do
|
||||
[
|
||||
%{value: "wormholes", label: "Wormholes", description: "J-space systems"},
|
||||
%{value: "hi", label: "High-Sec", description: "Security 0.5 - 1.0"},
|
||||
%{value: "low", label: "Low-Sec", description: "Security 0.1 - 0.4"},
|
||||
%{value: "null", label: "Null-Sec", description: "Security 0.0 and below"},
|
||||
%{value: "pochven", label: "Pochven", description: "Triglavian space"}
|
||||
]
|
||||
end
|
||||
|
||||
# Auto-initialize scopes from legacy scope setting if scopes is empty/nil
|
||||
defp maybe_initialize_scopes_from_legacy(%{scopes: scopes} = map)
|
||||
when is_list(scopes) and scopes != [] do
|
||||
# Scopes already set, don't override
|
||||
map
|
||||
end
|
||||
|
||||
defp maybe_initialize_scopes_from_legacy(%{scope: scope} = map) do
|
||||
# Convert legacy scope to new scopes format
|
||||
scopes = legacy_scope_to_scopes(scope)
|
||||
Map.put(map, :scopes, scopes)
|
||||
end
|
||||
|
||||
defp maybe_initialize_scopes_from_legacy(map) do
|
||||
# No scope field, default to wormholes
|
||||
Map.put(map, :scopes, [:wormholes])
|
||||
end
|
||||
|
||||
# Convert legacy scope atom to new scopes list
|
||||
defp legacy_scope_to_scopes(:wormholes), do: [:wormholes]
|
||||
defp legacy_scope_to_scopes(:stargates), do: [:hi, :low, :null]
|
||||
defp legacy_scope_to_scopes(:none), do: []
|
||||
defp legacy_scope_to_scopes(:all), do: [:wormholes, :hi, :low, :null, :pochven]
|
||||
defp legacy_scope_to_scopes(_), do: [:wormholes]
|
||||
|
||||
defp parse_scopes_from_form(form) do
|
||||
# Extract selected scopes from form data
|
||||
# Form sends scopes as "scopes" => %{"wormholes" => "true", "hi" => "true", ...}
|
||||
form
|
||||
|> Map.get("scopes", %{})
|
||||
|> case do
|
||||
scopes when is_map(scopes) ->
|
||||
scopes
|
||||
|> Enum.filter(fn {_key, value} -> value == "true" end)
|
||||
|> Enum.map(fn {key, _value} -> String.to_existing_atom(key) end)
|
||||
|
||||
scopes when is_list(scopes) ->
|
||||
# Already a list of atoms/strings
|
||||
scopes
|
||||
|> Enum.map(fn
|
||||
scope when is_atom(scope) -> scope
|
||||
scope when is_binary(scope) -> String.to_existing_atom(scope)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
# Helper function to get current scopes from form for checkbox state
|
||||
def get_current_scopes(form) do
|
||||
scopes = Phoenix.HTML.Form.input_value(form, :scopes) || []
|
||||
|
||||
scopes
|
||||
|> Enum.map(fn
|
||||
scope when is_atom(scope) -> Atom.to_string(scope)
|
||||
scope when is_binary(scope) -> scope
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -151,15 +151,66 @@
|
||||
placeholder="Select a map owner"
|
||||
options={Enum.map(@characters, fn character -> {character.label, character.id} end)}
|
||||
/>
|
||||
<.input
|
||||
type="select"
|
||||
field={f[:scope]}
|
||||
class="select h-8 min-h-[10px] !pt-1 !pb-1 text-sm bg-neutral-900"
|
||||
wrapper_class="mt-2"
|
||||
label="Map scope"
|
||||
placeholder="Select a map scope"
|
||||
options={Enum.map(@scopes, fn scope -> {scope, scope} end)}
|
||||
/>
|
||||
<!-- Map Scopes Section -->
|
||||
<div class="mt-2 border border-dashed border-stone-600 rounded p-3">
|
||||
<p class="text-xs text-stone-400 mb-2">
|
||||
Select which space types to automatically track on the map
|
||||
</p>
|
||||
<div class="grid grid-cols-2 gap-1">
|
||||
<%= for scope_option <- @available_scopes do %>
|
||||
<% is_checked = scope_option.value in (get_current_scopes(f) || []) %>
|
||||
<label class="flex items-center gap-2 cursor-pointer py-1 px-2 rounded hover:bg-stone-800">
|
||||
<div class="flex items-center">
|
||||
<div
|
||||
class={[
|
||||
"checkboxRoot sizeM p-checkbox p-component",
|
||||
if(is_checked, do: "p-highlight", else: "")
|
||||
]}
|
||||
data-p-highlight={is_checked}
|
||||
data-p-disabled="false"
|
||||
data-pc-name="checkbox"
|
||||
data-pc-section="root"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={"form[scopes][#{scope_option.value}]"}
|
||||
value="true"
|
||||
checked={is_checked}
|
||||
class="p-checkbox-input"
|
||||
aria-invalid="false"
|
||||
data-pc-section="input"
|
||||
/>
|
||||
<div
|
||||
class="p-checkbox-box"
|
||||
data-p-highlight={is_checked}
|
||||
data-p-disabled="false"
|
||||
data-pc-section="box"
|
||||
>
|
||||
<svg
|
||||
:if={is_checked}
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="p-icon p-checkbox-icon"
|
||||
aria-hidden="true"
|
||||
data-pc-section="icon"
|
||||
>
|
||||
<path
|
||||
d="M4.86199 11.5948C4.78717 11.5923 4.71366 11.5745 4.64596 11.5426C4.57826 11.5107 4.51779 11.4652 4.46827 11.4091L0.753985 7.69483C0.683167 7.64891 0.623706 7.58751 0.580092 7.51525C0.536478 7.44299 0.509851 7.36177 0.502221 7.27771C0.49459 7.19366 0.506156 7.10897 0.536046 7.03004C0.565935 6.95111 0.613367 6.88 0.674759 6.82208C0.736151 6.76416 0.8099 6.72095 0.890436 6.69571C0.970973 6.67046 1.05619 6.66385 1.13966 6.67635C1.22313 6.68886 1.30266 6.72017 1.37226 6.76792C1.44186 6.81567 1.4997 6.8786 1.54141 6.95197L4.86199 10.2503L12.6397 2.49483C12.7444 2.42694 12.8689 2.39617 12.9932 2.40745C13.1174 2.41873 13.2343 2.47141 13.3251 2.55705C13.4159 2.64268 13.4753 2.75632 13.4938 2.87973C13.5123 3.00315 13.4888 3.1292 13.4271 3.23768L5.2557 11.4091C5.20618 11.4652 5.14571 11.5107 5.07801 11.5426C5.01031 11.5745 4.9368 11.5923 4.86199 11.5948Z"
|
||||
fill="currentColor"
|
||||
>
|
||||
</path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs select-none">{scope_option.label}</span>
|
||||
</label>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<.input
|
||||
type="checkbox"
|
||||
field={f[:only_tracked_characters]}
|
||||
@@ -170,7 +221,10 @@
|
||||
type="checkbox"
|
||||
field={f[:create_default_acl]}
|
||||
label="Create default access list"
|
||||
checked={Phoenix.HTML.Form.normalize_value("checkbox", f[:create_default_acl].value) == true or is_nil(f[:create_default_acl].value)}
|
||||
checked={
|
||||
Phoenix.HTML.Form.normalize_value("checkbox", f[:create_default_acl].value) == true or
|
||||
is_nil(f[:create_default_acl].value)
|
||||
}
|
||||
/>
|
||||
<.live_select
|
||||
field={f[:acls]}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
rows={@transactions}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={transaction}>
|
||||
<:col :let={_transaction}>
|
||||
<div class=" text-22">
|
||||
<.icon name="hero-credit-card-solid" class="h-5 w-5" />
|
||||
</div>
|
||||
@@ -145,7 +145,7 @@
|
||||
rows={@invoices}
|
||||
class="!max-h-[40vh] !overflow-y-auto"
|
||||
>
|
||||
<:col :let={invoice}>
|
||||
<:col :let={_invoice}>
|
||||
<div class=" text-22">
|
||||
Map subscription
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,6 @@ defmodule WandererAppWeb.OpenApiV1Spec do
|
||||
|
||||
@behaviour OpenApiSpex.OpenApi
|
||||
|
||||
alias OpenApiSpex.{OpenApi, Info, Server, Components}
|
||||
|
||||
@impl OpenApiSpex.OpenApi
|
||||
def spec do
|
||||
# This is called by the modify_open_api option in the router
|
||||
|
||||
@@ -220,7 +220,7 @@ defmodule WandererAppWeb.Plugs.RequestValidator do
|
||||
|
||||
defp validate_params(_params, _max_length, _max_depth, _current_depth), do: :ok
|
||||
|
||||
defp validate_param_value(key, value, max_length, max_depth, current_depth)
|
||||
defp validate_param_value(key, value, max_length, _max_depth, _current_depth)
|
||||
when is_binary(value) do
|
||||
cond do
|
||||
String.length(value) > max_length ->
|
||||
|
||||
@@ -94,7 +94,7 @@ defmodule WandererAppWeb.Plugs.ResponseSanitizer do
|
||||
case Application.get_env(:wanderer_app, :environment) do
|
||||
:dev ->
|
||||
nonce = generate_nonce()
|
||||
conn = put_private(conn, :csp_nonce, nonce)
|
||||
_conn = put_private(conn, :csp_nonce, nonce)
|
||||
|
||||
base_policy
|
||||
|> Enum.map(fn directive ->
|
||||
|
||||
@@ -116,7 +116,9 @@ defmodule WandererAppWeb.PresenceGracePeriodManager do
|
||||
|> Map.new()
|
||||
|
||||
if length(timers_to_cancel) > 0 do
|
||||
Logger.debug("[PresenceGracePeriod] Cleared state for map #{map_id} - cancelled #{length(timers_to_cancel)} timers")
|
||||
Logger.debug(
|
||||
"[PresenceGracePeriod] Cleared state for map #{map_id} - cancelled #{length(timers_to_cancel)} timers"
|
||||
)
|
||||
end
|
||||
|
||||
new_state = %{
|
||||
|
||||
@@ -82,7 +82,8 @@ defmodule WandererAppWeb.Router do
|
||||
"allow-modals",
|
||||
"allow-same-origin",
|
||||
"allow-downloads",
|
||||
"allow-popups"
|
||||
"allow-popups",
|
||||
"allow-popups-to-escape-sandbox"
|
||||
]
|
||||
|
||||
pipeline :admin_bauth do
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.88.9"
|
||||
@version "1.89.5"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapScopesExtensions1 do
|
||||
@moduledoc """
|
||||
Installs any extensions that are mentioned in the repo's `installed_extensions/0` callback
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
execute("ALTER FUNCTION ash_raise_error(jsonb) STABLE;")
|
||||
execute("ALTER FUNCTION ash_raise_error(jsonb, ANYCOMPATIBLE) STABLE")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION uuid_generate_v7()
|
||||
RETURNS UUID
|
||||
AS $$
|
||||
DECLARE
|
||||
timestamp TIMESTAMPTZ;
|
||||
microseconds INT;
|
||||
BEGIN
|
||||
timestamp = clock_timestamp();
|
||||
microseconds = (cast(extract(microseconds FROM timestamp)::INT - (floor(extract(milliseconds FROM timestamp))::INT * 1000) AS DOUBLE PRECISION) * 4.096)::INT;
|
||||
|
||||
RETURN encode(
|
||||
set_byte(
|
||||
set_byte(
|
||||
overlay(uuid_send(gen_random_uuid()) placing substring(int8send(floor(extract(epoch FROM timestamp) * 1000)::BIGINT) FROM 3) FROM 1 FOR 6
|
||||
),
|
||||
6, (b'0111' || (microseconds >> 8)::bit(4))::bit(8)::int
|
||||
),
|
||||
7, microseconds::bit(8)::int
|
||||
),
|
||||
'hex')::UUID;
|
||||
END
|
||||
$$
|
||||
LANGUAGE PLPGSQL
|
||||
SET search_path = ''
|
||||
VOLATILE;
|
||||
""")
|
||||
|
||||
execute("""
|
||||
CREATE OR REPLACE FUNCTION timestamp_from_uuid_v7(_uuid uuid)
|
||||
RETURNS TIMESTAMP WITHOUT TIME ZONE
|
||||
AS $$
|
||||
SELECT to_timestamp(('x0000' || substr(_uuid::TEXT, 1, 8) || substr(_uuid::TEXT, 10, 4))::BIT(64)::BIGINT::NUMERIC / 1000);
|
||||
$$
|
||||
LANGUAGE SQL
|
||||
SET search_path = ''
|
||||
IMMUTABLE PARALLEL SAFE STRICT;
|
||||
""")
|
||||
end
|
||||
|
||||
def down do
|
||||
# Uncomment this if you actually want to uninstall the extensions
|
||||
# when this migration is rolled back:
|
||||
execute("ALTER FUNCTION ash_raise_error(jsonb) VOLATILE;")
|
||||
execute("ALTER FUNCTION ash_raise_error(jsonb, ANYCOMPATIBLE) VOLATILE")
|
||||
end
|
||||
end
|
||||
36
priv/repo/migrations/20251129123128_add_map_scopes.exs
Normal file
36
priv/repo/migrations/20251129123128_add_map_scopes.exs
Normal file
@@ -0,0 +1,36 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapScopes do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:maps_v1) do
|
||||
add :scopes, {:array, :text}, default: fragment("'{wormholes}'")
|
||||
end
|
||||
|
||||
create_if_not_exists unique_index(:maps_v1, [:public_api_key],
|
||||
name: "maps_v1_unique_public_api_key_index"
|
||||
)
|
||||
|
||||
drop_if_exists index(:map_system_v1, [:map_id], name: "map_system_v1_map_id_visible_index")
|
||||
end
|
||||
|
||||
def down do
|
||||
create_if_not_exists index(:map_system_v1, [:map_id],
|
||||
name: "map_system_v1_map_id_visible_index",
|
||||
where: "visible = true"
|
||||
)
|
||||
|
||||
drop_if_exists unique_index(:maps_v1, [:public_api_key],
|
||||
name: "maps_v1_unique_public_api_key_index"
|
||||
)
|
||||
|
||||
alter table(:maps_v1) do
|
||||
remove :scopes
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMapScopesDefault do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:maps_v1) do
|
||||
modify :scopes, {:array, :text}, default: nil
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:maps_v1) do
|
||||
modify :scopes, {:array, :text}, default: []
|
||||
end
|
||||
end
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user