Compare commits

...

130 Commits

Author SHA1 Message Date
Dmitry Popov
46c1ccdfcc fix: Fixed character tracking settings 2025-08-29 02:31:00 +02:00
Dmitry Popov
8817536038 fix: Fixed character tracking settings 2025-08-29 02:30:19 +02:00
Dmitry Popov
c3bb23a6ee fix: Fixed character tracking settings 2025-08-29 01:41:08 +02:00
Dmitry Popov
7e9c4c575e fix: Fixed character tracking settings 2025-08-28 22:18:18 +02:00
CI
5a70eee91e chore: [skip ci] 2025-08-28 10:24:51 +00:00
CI
228f6990a1 chore: release version v1.77.2 2025-08-28 10:24:51 +00:00
Dmitry Popov
d80ed0e70e Merge pull request #504 from guarzo/guarzo/sigapi
fix: update system signature api to return correct system id
2025-08-28 14:24:26 +04:00
CI
4576c75737 chore: [skip ci] 2025-08-28 10:03:36 +00:00
CI
67764faaa7 chore: release version v1.77.1 2025-08-28 10:03:36 +00:00
Dmitry Popov
91dd0b27ae chore: Added support for limited telemetry (base only). 2025-08-28 12:03:01 +02:00
guarzo
99dcf49fbc Merge branch 'main' into guarzo/sigapi 2025-08-27 21:30:59 -04:00
guarzo
6fb3edbfd6 fix: update system signature api to return correct system id 2025-08-28 01:30:37 +00:00
CI
26f13ce857 chore: [skip ci] 2025-08-27 21:18:31 +00:00
CI
e9b475c0a8 chore: release version v1.77.0 2025-08-27 21:18:31 +00:00
Dmitry Popov
7752010092 feat(Core): Reduced DB calls to check existing system jumps 2025-08-27 23:17:58 +02:00
CI
d3705b3ed7 chore: [skip ci] 2025-08-27 20:46:18 +00:00
CI
1394e2897e chore: release version v1.76.13 2025-08-27 20:46:18 +00:00
Dmitry Popov
5117a1c5af fix(Core): Fixed maps start timeout 2025-08-27 22:42:29 +02:00
CI
3c62403f33 chore: [skip ci] 2025-08-20 14:37:18 +00:00
CI
a4760f5162 chore: release version v1.76.12 2025-08-20 14:37:18 +00:00
Dmitry Popov
b071070431 fix(Core): Reduced ESI api calls to update character corp/ally info 2025-08-20 16:36:46 +02:00
CI
3bcb9628e7 chore: [skip ci] 2025-08-20 07:53:27 +00:00
CI
e62c4cf5bf chore: release version v1.76.11 2025-08-20 07:53:27 +00:00
Dmitry Popov
af46962ce4 Merge pull request #503 from wanderer-industries/revert-501-guarzo/sigsfix
Revert "fix: default signature types not being shown"
2025-08-20 11:53:00 +04:00
Dmitry Popov
0b0967830b Revert "fix: default signature types not being shown" 2025-08-20 11:52:34 +04:00
CI
172251a208 chore: [skip ci] 2025-08-18 23:28:33 +00:00
CI
8a6fb63d55 chore: release version v1.76.10 2025-08-18 23:28:33 +00:00
Dmitry Popov
9652959e5e fix(Core): Added character trackers start queue 2025-08-19 01:27:58 +02:00
CI
825ef46d41 chore: [skip ci] 2025-08-18 11:42:47 +00:00
CI
ad9f7c6b95 chore: release version v1.76.9 2025-08-18 11:42:47 +00:00
Dmitry Popov
b960b5c149 Merge pull request #501 from guarzo/guarzo/sigsfix
fix: default signature types not being shown
2025-08-18 15:42:14 +04:00
CI
0f092d21f9 chore: [skip ci] 2025-08-17 21:28:20 +00:00
CI
031576caa6 chore: release version v1.76.8 2025-08-17 21:28:20 +00:00
Dmitry Popov
7a97a96c42 fix(Core): added DB connection default timeouts 2025-08-17 23:27:21 +02:00
CI
2efb2daba0 chore: [skip ci] 2025-08-16 22:17:44 +00:00
CI
4374c39924 chore: release version v1.76.7 2025-08-16 22:17:44 +00:00
Dmitry Popov
15711495c7 fix(Core): Fixed auth redirect URL 2025-08-17 00:17:17 +02:00
guarzo
236f803427 fix: default signature types not being shown 2025-08-15 23:03:22 +00:00
CI
6772130f2a chore: [skip ci] 2025-08-15 15:27:07 +00:00
CI
ddd72f3fac chore: release version v1.76.6 2025-08-15 15:27:07 +00:00
Dmitry Popov
6e262835ef Merge pull request #500 from guarzo/guarzo/moressefix
fix: empty subscriptions for sse
2025-08-15 19:26:34 +04:00
guarzo
2f3b8ddc5f fix: empty subscriptions for sse 2025-08-15 11:08:40 -04:00
CI
cea3a74b34 chore: [skip ci] 2025-08-15 10:29:11 +00:00
CI
867941a233 chore: release version v1.76.5 2025-08-15 10:29:11 +00:00
Dmitry Popov
3ff388a16d fix(Core): fixed tracking paused issues, fixed user activity data 2025-08-15 12:28:36 +02:00
CI
f4248e9ab9 chore: [skip ci] 2025-08-14 23:40:20 +00:00
CI
507b3289c7 chore: release version v1.76.4 2025-08-14 23:40:20 +00:00
Dmitry Popov
9e1dfc48d5 Merge pull request #499 from guarzo/guarzo/relfixes 2025-08-15 03:39:50 +04:00
guarzo
518cbc7b5d fix: timestamp errors for sse and tracking 2025-08-14 19:22:30 -04:00
CI
ccc8db0620 chore: [skip ci] 2025-08-14 21:41:56 +00:00
CI
7cfb663efd chore: release version v1.76.3 2025-08-14 21:41:56 +00:00
Dmitry Popov
e5103cc925 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-14 23:41:30 +02:00
Dmitry Popov
26458f5a19 chore: Get rid of tracking pauses 2025-08-14 23:41:26 +02:00
CI
79d5ec6caf chore: [skip ci] 2025-08-14 20:56:39 +00:00
CI
034d461ab6 chore: release version v1.76.2 2025-08-14 20:56:39 +00:00
Dmitry Popov
2e9c1c170c chore: Get rid of tracking pauses 2025-08-14 22:56:08 +02:00
Dmitry Popov
24ad3b2c61 chore: [skip ci] 2025-08-14 11:28:56 +02:00
CI
288f55dc2f chore: [skip ci] 2025-08-13 16:15:29 +00:00
CI
78dbea6267 chore: release version v1.76.1 2025-08-13 16:15:29 +00:00
Dmitry Popov
6a9e53141d Merge pull request #498 from wanderer-industries/reselect-systems-after-init
fix(Map): Fix problem when systems was deselected after change tab
2025-08-13 20:15:05 +04:00
DanSylvest
05e6994520 fix(Map): Fix problem when systems was deselected after change tab 2025-08-13 18:58:24 +03:00
CI
1a4dc67eb9 chore: [skip ci] 2025-08-12 11:39:44 +00:00
CI
31d87a116b chore: release version v1.76.0 2025-08-12 11:39:44 +00:00
Dmitry Popov
c47796d590 Merge pull request #497 from wanderer-industries/sig-temp-names
Sig temp names
2025-08-12 15:39:07 +04:00
Dmitry Popov
c7138a41ee feat(Signatures): Sync signature temporary name with system on link signature to system 2025-08-12 13:20:03 +02:00
Dmitry Popov
96f04c70a9 Merge branch 'main' into sig-temp-names 2025-08-11 19:20:46 +02:00
Dmitry Popov
87a8bc09ab chore: [skip ci] 2025-08-11 19:20:37 +02:00
Dmitry Popov
5f5661d559 chore: [skip ci] 2025-08-11 19:20:13 +02:00
CI
35ca87790e chore: [skip ci] 2025-08-11 15:57:51 +00:00
CI
ae43e4a57c chore: release version v1.75.23 2025-08-11 15:57:51 +00:00
Dmitry Popov
b91712a01a chore: updated deps 2025-08-11 17:54:23 +02:00
CI
b20007b341 chore: [skip ci] 2025-08-11 14:47:44 +00:00
CI
6a24e1188b chore: release version v1.75.22 2025-08-11 14:47:44 +00:00
Dmitry Popov
5894efc1aa chore: release version v1.75.21 2025-08-11 16:47:11 +02:00
CI
a05612d243 chore: [skip ci] 2025-08-11 12:02:06 +00:00
CI
48de874d6b chore: release version v1.75.21 2025-08-11 12:02:06 +00:00
Dmitry Popov
91e6da316f Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 14:01:41 +02:00
Dmitry Popov
fa60bd81a1 chore: release version v1.75.19 2025-08-11 14:01:33 +02:00
CI
a08a69c5be chore: [skip ci] 2025-08-11 11:55:22 +00:00
CI
18d450a41a chore: release version v1.75.20 2025-08-11 11:55:22 +00:00
Dmitry Popov
36cdee61c0 chore: release version v1.75.19 2025-08-11 13:54:51 +02:00
Dmitry Popov
797e188259 fix: Fixed docs 2025-08-11 13:49:22 +02:00
Dmitry Popov
91b581668a Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 13:44:17 +02:00
Dmitry Popov
ad01fec28f Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 13:44:13 +02:00
CI
357d3a0df6 chore: release version v1.75.19 2025-08-11 11:25:52 +00:00
CI
5ce6022761 chore: release version v1.75.18 2025-08-11 11:25:17 +00:00
CI
235a0c5aea chore: release version v1.75.17 2025-08-11 11:24:44 +00:00
CI
9b81fa6ebb chore: release version v1.75.16 2025-08-11 11:24:16 +00:00
Dmitry Popov
8792d5ab0e Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 13:23:40 +02:00
Dmitry Popov
d46ed0c078 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 13:23:36 +02:00
CI
73c433fcd2 chore: release version v1.75.15 2025-08-11 11:23:28 +00:00
CI
02b5239220 chore: release version v1.75.14 2025-08-11 11:22:57 +00:00
CI
0ed3bdfcb0 chore: release version v1.75.13 2025-08-11 11:22:29 +00:00
CI
bdeb89011f chore: release version v1.75.12 2025-08-11 11:22:00 +00:00
CI
1523b625bc chore: release version v1.75.11 2025-08-11 11:21:27 +00:00
CI
fb91eeb692 chore: release version v1.75.10 2025-08-11 11:20:57 +00:00
CI
601d2e02cb chore: release version v1.75.9 2025-08-11 11:20:24 +00:00
Dmitry Popov
0a662d34eb Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 13:19:52 +02:00
Dmitry Popov
5cd4693e9d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 13:19:49 +02:00
CI
f3f0f860e3 chore: release version v1.75.8 2025-08-11 11:04:18 +00:00
Dmitry Popov
93a5cf8a79 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 13:03:42 +02:00
Dmitry Popov
7cf15cbc21 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 13:03:39 +02:00
CI
30bc6d20b2 chore: release version v1.75.7 2025-08-11 10:55:27 +00:00
Dmitry Popov
b39f99fde4 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-08-11 12:55:02 +02:00
Dmitry Popov
0e8aa9efa4 chore: release version v1.75.5 2025-08-11 12:54:58 +02:00
CI
e1fcde36e3 chore: release version v1.75.6 2025-08-11 10:51:27 +00:00
Dmitry Popov
7aafe077d3 chore: release version v1.75.5 2025-08-11 12:50:59 +02:00
CI
5b8cab5e76 chore: release version v1.75.5 2025-08-11 10:48:40 +00:00
Dmitry Popov
4ab56af40a chore: release version v1.75.4 2025-08-11 12:48:09 +02:00
CI
e8cea86a76 chore: release version v1.75.4 2025-08-11 07:52:21 +00:00
Dmitry Popov
d0a6e0b358 Merge pull request #496 from guarzo/guarzo/secaudit 2025-08-11 11:51:26 +04:00
guarzo
8831b3e970 fix: restore security audit 2025-08-11 03:37:33 +00:00
CI
f6db6f0914 chore: release version v1.75.3
Some checks failed
Flaky Test Detection / 🔍 Detect Flaky Tests (push) Has been cancelled
Flaky Test Detection / 📊 Analyze Test History (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
🧪 Test Suite / Test Suite (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-08-10 22:17:25 +00:00
Dmitry Popov
ab8baeedd1 fix(core): Fixed character tracking issues 2025-08-11 00:16:52 +02:00
CI
eccee5e72e chore: release version v1.75.2 2025-08-10 16:00:40 +00:00
Dmitry Popov
4d93055bda Merge pull request #480 from wanderer-industries/default-settings
Default settings
2025-08-10 19:56:58 +04:00
Dmitry Popov
c60c16e56a chore: Fix issues with ash resources 2025-08-10 17:45:29 +02:00
Dmitry Popov
99b1de5647 chore: Fix issues with ash resources 2025-08-10 17:44:56 +02:00
Dmitry Popov
7efe11a421 chore: Fix issues with ash resources 2025-08-10 17:21:36 +02:00
Dmitry Popov
954108856a chore: Fix issues with ash resources 2025-08-10 16:55:45 +02:00
Dmitry Popov
cbca745ec4 Merge branch 'main' into default-settings 2025-08-10 16:19:59 +02:00
DanSylvest
e15e7c8f8d fix(Map): Fix indents for ally logos in list "On the map" 2025-08-10 12:51:15 +03:00
DanSylvest
65e8a520e5 fix(Map): Fix cancelling ping from system context menu 2025-08-10 12:00:05 +03:00
DanSylvest
3926af5a6d fix(Map): Hide admin settings tab 2025-08-10 10:02:29 +03:00
DanSylvest
556fb33223 fix(Map): Remote map setting refactoring 2025-08-10 09:57:50 +03:00
Dmitry Popov
82295adeab Merge pull request #477 from guarzo/guarzo/settings
feature: provide default settings interface
2025-07-31 12:21:06 +04:00
guarzo
9d7d4fad2e feature: provide default settings interface 2025-07-27 14:27:45 -04:00
Dmitry Popov
74f7ad155d Merge branch 'develop' into sig-temp-names 2025-07-18 13:52:07 +02:00
DanSylvest
f58ebad0ec fix(Map): Add Temp name field 2025-07-08 18:43:49 +03:00
Dmitry Popov
7ca4eb3b8f feat(Signatures): add support for signature temp names 2025-07-08 14:03:22 +02:00
208 changed files with 9615 additions and 15196 deletions

View File

@@ -5,7 +5,7 @@ on:
branches:
- main
- develop
- "releases/*"
env:
MIX_ENV: prod
GH_TOKEN: ${{ github.token }}
@@ -53,6 +53,7 @@ jobs:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ssh-key: "${{ secrets.COMMIT_KEY }}"
fetch-depth: 0
- name: 😅 Cache deps
id: cache-deps
@@ -95,9 +96,10 @@ jobs:
git config --global user.name 'CI'
git config --global user.email 'ci@users.noreply.github.com'
mix git_ops.release --force-patch --yes
git commit --allow-empty -m 'chore: [skip ci]'
git push --follow-tags
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
- name: Set commit hash for develop
id: set-commit-develop
if: github.ref == 'refs/heads/develop'
@@ -106,11 +108,9 @@ jobs:
docker:
name: 🛠 Build Docker Images
if: github.ref == 'refs/heads/develop'
needs: build
runs-on: ubuntu-22.04
outputs:
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
release-notes: ${{ steps.get-content.outputs.string }}
permissions:
checks: write
contents: write
@@ -137,19 +137,6 @@ jobs:
ref: ${{ needs.build.outputs.commit_hash }}
fetch-depth: 0
- name: Prepare Changelog
if: github.ref == 'refs/heads/main'
run: |
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
- name: Get Release Tag
id: get-latest-tag
if: github.ref == 'refs/heads/main'
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
@@ -198,26 +185,6 @@ jobs:
if-no-files-found: error
retention-days: 1
- uses: markpatterson27/markdown-to-output@v1
id: extract-changelog
if: github.ref == 'refs/heads/main'
with:
filepath: CHANGELOG.md
- name: Get content
uses: 2428392/gh-truncate-string-action@v1.3.0
id: get-content
if: github.ref == 'refs/heads/main'
with:
stringToTruncate: |
📣 Wanderer new release available 🎉
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
${{ steps.extract-changelog.outputs.body }}
maxLength: 500
truncationSymbol: "…"
merge:
runs-on: ubuntu-latest
needs:
@@ -248,9 +215,6 @@ jobs:
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}},enable=${{ github.ref == 'refs/heads/main' }}
type=semver,pattern={{major}}.{{minor}},enable=${{ github.ref == 'refs/heads/main' }}
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }},enable=${{ github.ref == 'refs/heads/main' }}
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
type=raw,value=develop-{{sha}},enable=${{ github.ref == 'refs/heads/develop' }}
@@ -267,19 +231,25 @@ jobs:
create-release:
name: 🏷 Create Release
runs-on: ubuntu-22.04
needs: [docker, merge]
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
needs: build
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get Release Tag
id: get-latest-tag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
- name: 🏷 Create Draft Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.docker.outputs.release-tag }}
name: Release ${{ needs.docker.outputs.release-tag }}
tag_name: ${{ steps.get-latest-tag.outputs.tag }}
name: Release ${{ steps.get-latest-tag.outputs.tag }}
body: |
## Info
Commit ${{ github.sha }} was deployed to `staging`. [See code diff](${{ github.event.compare }}).
@@ -289,10 +259,3 @@ jobs:
## How to Promote?
In order to promote this to prod, edit the draft and press **"Publish release"**.
draft: true
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
if: github.ref == 'refs/heads/main'
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ needs.docker.outputs.release-notes }}

187
.github/workflows/docker-arm.yml vendored Normal file
View File

@@ -0,0 +1,187 @@
name: Build Docker ARM Image
on:
push:
tags:
- '**'
env:
MIX_ENV: prod
GH_TOKEN: ${{ github.token }}
REGISTRY_IMAGE: wandererltd/community-edition-arm
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
docker:
name: 🛠 Build Docker Images
runs-on: ubuntu-22.04
outputs:
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
release-notes: ${{ steps.get-content.outputs.string }}
permissions:
checks: write
contents: write
packages: write
attestations: write
id-token: write
pull-requests: write
repository-projects: write
strategy:
fail-fast: false
matrix:
platform:
- linux/arm64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get Release Tag
id: get-latest-tag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ref: ${{ steps.get-latest-tag.outputs.tag }}
fetch-depth: 0
- name: Prepare Changelog
run: |
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.WANDERER_DOCKER_USER }}
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
push: true
context: .
file: ./Dockerfile
cache-from: type=gha
cache-to: type=gha,mode=max
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platform }}
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
build-args: |
MIX_ENV=prod
BUILD_METADATA=${{ steps.meta.outputs.json }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
- uses: markpatterson27/markdown-to-output@v1
id: extract-changelog
with:
filepath: CHANGELOG.md
- name: Get content
uses: 2428392/gh-truncate-string-action@v1.3.0
id: get-content
with:
stringToTruncate: |
📣 Wanderer **ARM** release available 🎉
**Version**: :${{ steps.get-latest-tag.outputs.tag }}
${{ steps.extract-changelog.outputs.body }}
maxLength: 500
truncationSymbol: "…"
merge:
runs-on: ubuntu-latest
needs:
- docker
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.WANDERER_DOCKER_USER }}
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY_IMAGE }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
notify:
name: 🏷 Notify about release
runs-on: ubuntu-22.04
needs: [docker, merge]
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ needs.docker.outputs.release-notes }}

187
.github/workflows/docker.yml vendored Normal file
View File

@@ -0,0 +1,187 @@
name: Build Docker Image
on:
push:
tags:
- '**'
env:
MIX_ENV: prod
GH_TOKEN: ${{ github.token }}
REGISTRY_IMAGE: wandererltd/community-edition
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
docker:
name: 🛠 Build Docker Images
runs-on: ubuntu-22.04
outputs:
release-tag: ${{ steps.get-latest-tag.outputs.tag }}
release-notes: ${{ steps.get-content.outputs.string }}
permissions:
checks: write
contents: write
packages: write
attestations: write
id-token: write
pull-requests: write
repository-projects: write
strategy:
fail-fast: false
matrix:
platform:
- linux/amd64
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get Release Tag
id: get-latest-tag
uses: "WyriHaximus/github-action-get-previous-tag@v1"
with:
fallback: 1.0.0
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
ref: ${{ steps.get-latest-tag.outputs.tag }}
fetch-depth: 0
- name: Prepare Changelog
run: |
yes | cp -rf CHANGELOG.md priv/changelog/CHANGELOG.md
sed -i '1i%{title: "Change Log"}\n\n---\n' priv/changelog/CHANGELOG.md
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY_IMAGE }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.WANDERER_DOCKER_USER }}
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
push: true
context: .
file: ./Dockerfile
cache-from: type=gha
cache-to: type=gha,mode=max
labels: ${{ steps.meta.outputs.labels }}
platforms: ${{ matrix.platform }}
outputs: type=image,"name=${{ env.REGISTRY_IMAGE }}",push-by-digest=true,name-canonical=true,push=true
build-args: |
MIX_ENV=prod
BUILD_METADATA=${{ steps.meta.outputs.json }}
- name: Export digest
run: |
mkdir -p /tmp/digests
digest="${{ steps.build.outputs.digest }}"
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
if-no-files-found: error
retention-days: 1
- uses: markpatterson27/markdown-to-output@v1
id: extract-changelog
with:
filepath: CHANGELOG.md
- name: Get content
uses: 2428392/gh-truncate-string-action@v1.3.0
id: get-content
with:
stringToTruncate: |
📣 Wanderer new release available 🎉
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
${{ steps.extract-changelog.outputs.body }}
maxLength: 500
truncationSymbol: "…"
merge:
runs-on: ubuntu-latest
needs:
- docker
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.WANDERER_DOCKER_USER }}
password: ${{ secrets.WANDERER_DOCKER_PASSWORD }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
${{ env.REGISTRY_IMAGE }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{version}},value=${{ needs.docker.outputs.release-tag }}
- name: Create manifest list and push
working-directory: /tmp/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.REGISTRY_IMAGE }}@sha256:%s ' *)
- name: Inspect image
run: |
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
notify:
name: 🏷 Notify about release
runs-on: ubuntu-22.04
needs: [docker, merge]
steps:
- name: Discord Webhook Action
uses: tsickert/discord-webhook@v5.3.0
with:
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ needs.docker.outputs.release-notes }}

File diff suppressed because it is too large Load Diff

View File

@@ -21,21 +21,17 @@ RUN mkdir config
# to ensure any relevant config change will trigger the dependencies
# to be re-compiled.
COPY config/config.exs config/${MIX_ENV}.exs config/
COPY priv priv
COPY lib lib
COPY assets assets
RUN mix compile
RUN mix assets.deploy
RUN mix compile
# Changes to config/runtime.exs don't require recompiling the code
COPY config/runtime.exs config/
COPY rel rel
RUN mix release
# start a new build stage so that the final image will only contain

View File

@@ -1,18 +1,13 @@
// import './tailwind.css';
//@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css';
//@import 'primereact/resources/themes/lara-dark-purple/theme.css';
//@import "prime-fixes";
@import 'primereact/resources/primereact.min.css';
//@import 'primeflex/primeflex.css';
@import 'primeicons/primeicons.css';
//@import 'primereact/resources/primereact.css';
@use 'primereact/resources/primereact.min.css';
@use 'primeicons/primeicons.css';
@import "fixes";
@import "prime-fixes";
@import "custom-scrollbar";
@import "tooltip";
@import "context-menu";
@use "fixes";
@use "prime-fixes";
@use "custom-scrollbar";
@use "tooltip";
@use "context-menu";
.fixedImportant {

View File

@@ -1,7 +1,7 @@
.vertical-tabs-container {
display: flex;
width: 100%;
min-height: 300px;
min-height: 400px;
.p-tabview {
width: 100%;
@@ -68,6 +68,28 @@
}
}
&.color-warn {
@apply bg-yellow-600/5 border-r-yellow-600/20;
&:hover {
@apply bg-yellow-600/10 border-r-yellow-600/40;
}
&.p-tabview-selected {
@apply bg-yellow-600/10 border-r-yellow-600;
.p-tabview-nav-link {
@apply text-yellow-600;
}
&:hover {
@apply bg-yellow-600/10 border-r-yellow-600;
}
}
}
}
}

View File

@@ -1,6 +1,3 @@
@import "fix-dialog";
@import "fix-popup";
@import "fix-tabs";
//@import "fix-input";
//@import "theme";
@use "fix-dialog";
@use "fix-popup";
@use "fix-tabs";

View File

@@ -19,7 +19,7 @@ export interface ContextMenuSystemProps {
onSystemStatus(val: number): void;
onSystemLabels(val: string): void;
onCustomLabelDialog(): void;
onTogglePing(type: PingType, solar_system_id: string, hasPing: boolean): void;
onTogglePing(type: PingType, solar_system_id: string, ping_id: string | undefined, hasPing: boolean): void;
onWaypointSet: WaypointSetContextHandler;
}

View File

@@ -109,7 +109,7 @@ export const useContextMenuSystemItems = ({
{ separator: true },
{
command: () => onTogglePing(PingType.Rally, systemId, hasPing),
command: () => onTogglePing(PingType.Rally, systemId, ping?.id, hasPing),
disabled: !isShowPingBtn,
template: () => {
const iconClasses = clsx({

View File

@@ -1,17 +1,24 @@
import { Node } from 'reactflow';
import { useCallback, useRef, useState } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { ContextMenu } from 'primereact/contextmenu';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export const useContextMenuSystemMultipleHandlers = () => {
const {
data: { pings },
} = useMapRootState();
const contextMenuRef = useRef<ContextMenu | null>(null);
const [systems, setSystems] = useState<Node<SolarSystemRawType>[]>();
const { deleteSystems } = useDeleteSystems();
const ping = useMemo(() => (pings.length === 1 ? pings[0] : undefined), [pings]);
const handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => {
setSystems(systems_);
ev.preventDefault();
@@ -24,13 +31,17 @@ export const useContextMenuSystemMultipleHandlers = () => {
return;
}
const sysToDel = systems.filter(x => !x.data.locked).map(x => x.id);
const sysToDel = systems
.filter(x => !x.data.locked)
.filter(x => x.id !== ping?.solar_system_id)
.map(x => x.id);
if (sysToDel.length === 0) {
return;
}
deleteSystems(sysToDel);
}, [deleteSystems, systems]);
}, [deleteSystems, systems, ping]);
return {
handleSystemMultipleContext,

View File

@@ -1,6 +1,6 @@
import { MapUserSettings, SettingsWithVersion } from '@/hooks/Mapper/mapRootProvider/types.ts';
const REQUIRED_KEYS = [
export const REQUIRED_KEYS = [
'widgets',
'interface',
'onTheMap',

View File

@@ -1,3 +1,4 @@
export * from './useSystemInfo';
export * from './useGetOwnOnlineCharacters';
export * from './useElementWidth';
export * from './useDetectSettingsChanged';

View File

@@ -0,0 +1,23 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useEffect, useState } from 'react';
export const useDetectSettingsChanged = () => {
const {
storedSettings: {
interfaceSettings,
settingsRoutes,
settingsLocal,
settingsSignatures,
settingsOnTheMap,
settingsKills,
},
} = useMapRootState();
const [counter, setCounter] = useState(0);
useEffect(
() => setCounter(x => x + 1),
[interfaceSettings, settingsRoutes, settingsLocal, settingsSignatures, settingsOnTheMap, settingsKills],
);
return counter;
};

View File

@@ -1,4 +1,4 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
.ConnectionTimeEOL {
background-image: linear-gradient(207deg, transparent, var(--conn-time-eol));

View File

@@ -1,4 +1,4 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
.EdgePathBack {
fill: none;

View File

@@ -1,4 +1,5 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
$pastel-blue: #5a7d9a;
$pastel-pink: rgb(30, 161, 255);
@@ -34,7 +35,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
color: var(--rf-text-color, #ffffff);
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
border: 1px solid darken($pastel-blue, 10%);
border: 1px solid color.adjust($pastel-blue, $lightness: -10%);
border-radius: 5px;
position: relative;
z-index: 3;

View File

@@ -1,4 +1,4 @@
@import './SolarSystemNodeDefault.module.scss';
@use './SolarSystemNodeDefault.module.scss';
/* ---------------------------------------------
Only override what's different from the base

View File

@@ -21,7 +21,9 @@ import { KillsCounter } from '@/hooks/Mapper/components/map/components/KillsCoun
export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>) => {
const nodeVars = useSolarSystemNode(props);
const { localCounterCharacters } = useLocalCounter(nodeVars);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(nodeVars.solarSystemId);
const { killsCount: localKillsCount, killsActivityType: localKillsActivityType } = useNodeKillsCount(
nodeVars.solarSystemId,
);
// console.log('JOipP', `render ${nodeVars.id}`, render++);

View File

@@ -1,4 +1,4 @@
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
.Signature {
position: relative;

View File

@@ -6,5 +6,5 @@ export * from './useCommandsCharacters';
export * from './useCommandsConnections';
export * from './useCommandsConnections';
export * from './useCenterSystem';
export * from './useSelectSystem';
export * from './useSelectSystems';
export * from './useMapCommands';

View File

@@ -1,21 +0,0 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandSelectSystem } from '@/hooks/Mapper/types';
export const useSelectSystem = () => {
const rf = useReactFlow();
const ref = useRef({ rf });
ref.current = { rf };
return useCallback((systemId: CommandSelectSystem) => {
ref.current.rf.setNodes(nds =>
nds.map(node => {
return {
...node,
selected: node.id === systemId,
};
}),
);
}, []);
};

View File

@@ -0,0 +1,31 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandSelectSystems } from '@/hooks/Mapper/types';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
const rf = useReactFlow();
const ref = useRef({ rf, onSelectionChange });
ref.current = { rf, onSelectionChange };
return useCallback(({ systems, delay }: CommandSelectSystems) => {
const run = () => {
ref.current.rf.setNodes(nds =>
nds.map(node => {
return {
...node,
selected: systems.includes(node.id),
};
}),
);
};
if (delay == null || delay === 0) {
run();
return;
}
setTimeout(run, delay);
}, []);
};

View File

@@ -14,6 +14,7 @@ import {
CommandRemoveSystems,
Commands,
CommandSelectSystem,
CommandSelectSystems,
CommandUpdateConnection,
CommandUpdateSystems,
MapHandlers,
@@ -28,7 +29,7 @@ import {
useMapRemoveSystems,
useMapUpdateSystems,
useCenterSystem,
useSelectSystem,
useSelectSystems,
} from './api';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
@@ -38,7 +39,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
const mapUpdateSystems = useMapUpdateSystems();
const removeSystems = useMapRemoveSystems(onSelectionChange);
const centerSystem = useCenterSystem();
const selectSystem = useSelectSystem();
const selectSystems = useSelectSystems(onSelectionChange);
const selectRef = useRef({ onSelectionChange });
selectRef.current = { onSelectionChange };
@@ -105,14 +106,11 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
break;
case Commands.selectSystem:
setTimeout(() => {
const systemId = `${data}`;
selectRef.current.onSelectionChange({
systems: [systemId],
connections: [],
});
selectSystem(systemId as CommandSelectSystem);
}, 500);
selectSystems({ systems: [data as string], delay: 500 });
break;
case Commands.selectSystems:
selectSystems(data as CommandSelectSystems);
break;
case Commands.pingAdded:

View File

@@ -1,5 +1,5 @@
@import './eve-common-variables';
@import './eve-common';
@use './eve-common-variables';
@use './eve-common';
.default-theme {
--rf-bg-color: #0C0A09;

View File

@@ -1,18 +1,19 @@
@use "sass:color";
$friendlyBase: #3bbd39;
$friendlyAlpha: #3bbd3952;
$friendlyDark20: darken($friendlyBase, 20%);
$friendlyDark30: darken($friendlyBase, 30%);
$friendlyDark5: darken($friendlyBase, 5%);
$friendlyDark20: color.adjust($friendlyBase, $lightness: -20%);
$friendlyDark30: color.adjust($friendlyBase, $lightness: -30%);
$friendlyDark5: color.adjust($friendlyBase, $lightness: -5%);
$lookingForBase: #43c2fd;
$lookingForAlpha: rgba(67, 176, 253, 0.48);
$lookingForDark15: darken($lookingForBase, 15%);
$lookingForDark15: color.adjust($lookingForBase, $lightness: -15%);
$homeBase: rgb(179, 253, 67);
$homeAlpha: rgba(186, 248, 48, 0.32);
$homeBackground: #a0fa5636;
$homeDark30: darken($homeBase, 30%);
$homeDark30: color.adjust($homeBase, $lightness: -30%);
:root {
--pastel-blue: #5a7d9a;

View File

@@ -1,4 +1,4 @@
@import './eve-common-variables';
@use './eve-common-variables';
.eve-wh-effect-color-pulsar {

View File

@@ -1,2 +1,2 @@
@import './default-theme.scss';
@import './pathfinder-theme.scss';
@use './default-theme.scss';
@use './pathfinder-theme.scss';

View File

@@ -1,10 +1,11 @@
@import './eve-common-variables';
@import './eve-common';
@use "sass:color";
@use './eve-common-variables';
@use './eve-common';
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
$homeBase: rgb(197, 253, 67);
$homeAlpha: rgba(197, 253, 67, 0.32);
$homeDark30: darken($homeBase, 30%);
$homeDark30: color.adjust($homeBase, $lightness: -30%);
.pathfinder-theme {
/* -- Override values from the default theme -- */

View File

@@ -14,6 +14,7 @@ import { PrimeIcons } from 'primereact/api';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
const TOOLTIP_PROPS = { content: 'Remove comment', position: TooltipPosition.top };
@@ -28,8 +29,7 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
const char = useGetCacheCharacter(characterEveId);
const [hovered, setHovered] = useState(false);
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const { outCommand } = useMapRootState();
const ref = useRef({ outCommand, id });
@@ -45,9 +45,6 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
const handleMouseEnter = useCallback(() => setHovered(true), []);
const handleMouseLeave = useCallback(() => setHovered(false), []);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
return (
<>
<InfoDrawer
@@ -68,11 +65,11 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
{!hovered && <TimeAgo timestamp={time} />}
{hovered && (
// @ts-ignore
<div ref={cpRemoveBtnRef}>
<div ref={cfRef}>
<WdImgButton
className={clsx(PrimeIcons.TRASH, 'hover:text-red-400')}
tooltip={TOOLTIP_PROPS}
onClick={handleShowCP}
onClick={cfShow}
/>
</div>
)}
@@ -85,9 +82,9 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
</InfoDrawer>
<ConfirmPopup
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="Are you sure you want to delete?"
icon="pi pi-exclamation-triangle"
accept={handleDelete}

View File

@@ -16,8 +16,9 @@ import { PrimeIcons } from 'primereact/api';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { Toast } from 'primereact/toast';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import useRefState from 'react-usestateref';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
const PING_PLACEMENT_MAP = {
[PingsPlacement.rightTop]: 'top-right',
@@ -78,9 +79,7 @@ export interface PingsInterfaceProps {
export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
const toast = useRef<Toast>(null);
const [isShow, setIsShow, isShowRef] = useRefState(false);
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const {
storedSettings: { interfaceSettings },
@@ -98,9 +97,6 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
const ping = useMemo(() => (pings.length === 1 ? pings[0] : null), [pings]);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
const navigateTo = useCallback(() => {
if (!ping) {
return;
@@ -242,11 +238,11 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
/>
{/*@ts-ignore*/}
<div ref={cpRemoveBtnRef}>
<div ref={cfRef}>
<WdImgButton
className={clsx('pi-trash', 'text-red-400 hover:text-red-300')}
tooltip={DELETE_TOOLTIP_PROPS}
onClick={handleShowCP}
onClick={cfShow}
/>
</div>
{/* TODO ADD solar system menu*/}
@@ -272,9 +268,9 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
/>
<ConfirmPopup
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="Are you sure you want to delete ping?"
icon="pi pi-exclamation-triangle text-orange-400"
accept={removePing}

View File

@@ -28,12 +28,12 @@ import {
renderInfoColumn,
renderUpdatedTimeLeft,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { getSignatureRowClass } from '../helpers/rowStyles';
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
@@ -157,9 +157,18 @@ export const SystemSignaturesContent = ({
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
);
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
const {
showGroupColumn,
showDescriptionColumn,
showAddedColumn,
showUpdatedColumn,
showCharacterColumn,
showCharacterPortrait,
} = useMemo(
() => ({
showGroupColumn: settings[SETTINGS_KEYS.SHOW_GROUP_COLUMN] as boolean,
showDescriptionColumn: settings[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN] as boolean,
showAddedColumn: settings[SETTINGS_KEYS.SHOW_ADDED_COLUMN] as boolean,
showUpdatedColumn: settings[SETTINGS_KEYS.SHOW_UPDATED_COLUMN] as boolean,
showCharacterColumn: settings[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN] as boolean,
showCharacterPortrait: settings[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT] as boolean,
@@ -309,15 +318,17 @@ export const SystemSignaturesContent = ({
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
sortable
/>
<Column
field="group"
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
body={sig => sig.group ?? ''}
hidden={isCompact}
sortable
/>
{showGroupColumn && (
<Column
field="group"
header="Group"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
body={sig => sig.group ?? ''}
hidden={isCompact}
sortable
/>
)}
<Column
field="info"
header="Info"
@@ -336,15 +347,17 @@ export const SystemSignaturesContent = ({
sortable
/>
)}
<Column
field="inserted_at"
header="Added"
dataType="date"
body={renderAddedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
{showAddedColumn && (
<Column
field="inserted_at"
header="Added"
dataType="date"
body={renderAddedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
)}
{showUpdatedColumn && (
<Column
field="updated_at"

View File

@@ -1,3 +1,4 @@
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
import {
GroupType,
SignatureGroup,
@@ -11,7 +12,6 @@ import {
SignatureKindFR,
SignatureKindRU,
} from '@/hooks/Mapper/types';
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
export const TIME_ONE_MINUTE = 1000 * 60;
export const TIME_TEN_MINUTES = TIME_ONE_MINUTE * 10;
@@ -130,6 +130,8 @@ export const SIGNATURE_SETTINGS = {
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.COMBAT_SITE, name: 'Show Combat Sites' },
],
uiFlags: [
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_GROUP_COLUMN, name: 'Show Group Column' },
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_ADDED_COLUMN, name: 'Show Added Column' },
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_UPDATED_COLUMN, name: 'Show Updated Column' },
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN, name: 'Show Description Column' },
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_CHARACTER_COLUMN, name: 'Show Character Column' },

View File

@@ -3,7 +3,7 @@ import { Dialog } from 'primereact/dialog';
import { useCallback, useRef, useState } from 'react';
import { TabPanel, TabView } from 'primereact/tabview';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OutCommand } from '@/hooks/Mapper/types';
import { OutCommand, UserPermission } from '@/hooks/Mapper/types';
import { CONNECTIONS_CHECKBOXES_PROPS, SIGNATURES_CHECKBOXES_PROPS, SYSTEMS_CHECKBOXES_PROPS } from './constants.ts';
import {
MapSettingsProvider,
@@ -12,7 +12,10 @@ import {
import { WidgetsSettings } from './components/WidgetsSettings';
import { CommonSettings } from './components/CommonSettings';
import { SettingsListItem } from './types.ts';
import { ImportExport } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/components/ImportExport.tsx';
import { ImportExport } from './components/ImportExport.tsx';
import { ServerSettings } from './components/ServerSettings.tsx';
import { AdminSettings } from './components/AdminSettings.tsx';
import { useMapCheckPermissions } from '@/hooks/Mapper/mapRootProvider/hooks/api';
export interface MapSettingsProps {
visible: boolean;
@@ -24,6 +27,7 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
const { outCommand } = useMapRootState();
const { renderSettingItem, setUserRemoteSettings } = useMapSettings();
const isAdmin = useMapCheckPermissions([UserPermission.ADMIN_MAP]);
const refVars = useRef({ outCommand, onHide, visible });
refVars.current = { outCommand, onHide, visible };
@@ -58,7 +62,7 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
header="Map user settings"
visible
draggable={false}
style={{ width: '550px' }}
style={{ width: '600px' }}
onShow={handleShow}
onHide={handleHide}
>
@@ -92,6 +96,16 @@ export const MapSettingsComp = ({ visible, onHide }: MapSettingsProps) => {
<TabPanel header="Import/Export" className="h-full" headerClassName={styles.verticalTabHeader}>
<ImportExport />
</TabPanel>
<TabPanel header="Server Settings" className="h-full" headerClassName="color-warn">
<ServerSettings />
</TabPanel>
{isAdmin && (
<TabPanel header="Admin Settings" className="h-full" headerClassName="color-warn">
<AdminSettings />
</TabPanel>
)}
</TabView>
</div>
</div>

View File

@@ -0,0 +1,128 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import { Button } from 'primereact/button';
import { callToastError, callToastSuccess, callToastWarn } from '@/hooks/Mapper/helpers';
import { OutCommand } from '@/hooks/Mapper/types';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { MapUserSettings, RemoteAdminSettingsResponse } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import fastDeepEqual from 'fast-deep-equal';
import { useDetectSettingsChanged } from '@/hooks/Mapper/components/hooks';
export const AdminSettings = () => {
const {
storedSettings: { getSettingsForExport },
outCommand,
} = useMapRootState();
const settingsChanged = useDetectSettingsChanged();
const [currentRemoteSettings, setCurrentRemoteSettings] = useState<MapUserSettings | null>(null);
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const toast = useRef<Toast | null>(null);
const hasSettingsForExport = useMemo(() => !!getSettingsForExport(), [getSettingsForExport]);
const refVars = useRef({ currentRemoteSettings, getSettingsForExport });
refVars.current = { currentRemoteSettings, getSettingsForExport };
useEffect(() => {
const load = async () => {
let res: RemoteAdminSettingsResponse | undefined;
try {
res = await outCommand({ type: OutCommand.getDefaultSettings, data: null });
} catch (error) {
// do nothing
}
if (!res || res.default_settings == null) {
return;
}
setCurrentRemoteSettings(parseMapUserSettings(res.default_settings));
};
load();
}, [outCommand]);
const isDirty = useMemo(() => {
const { currentRemoteSettings, getSettingsForExport } = refVars.current;
const localCurrent = parseMapUserSettings(getSettingsForExport());
return !fastDeepEqual(currentRemoteSettings, localCurrent);
// eslint-disable-next-line
}, [settingsChanged, currentRemoteSettings]);
const handleSync = useCallback(async () => {
const settings = getSettingsForExport();
if (!settings) {
callToastWarn(toast.current, 'No settings to save');
return;
}
let response: { success: boolean } | undefined;
try {
response = await outCommand({
type: OutCommand.saveDefaultSettings,
data: { settings },
});
} catch (err) {
callToastError(toast.current, 'Something went wrong while saving settings');
console.error('ERROR: ', err);
return;
}
if (!response || !response.success) {
callToastError(toast.current, 'Settings not saved - dont not why it');
return;
}
setCurrentRemoteSettings(parseMapUserSettings(settings));
callToastSuccess(toast.current, 'Settings saved successfully');
}, [getSettingsForExport, outCommand]);
return (
<div className="w-full h-full flex flex-col gap-5">
<div className="flex flex-col gap-1">
<div>
<Button
// @ts-ignore
ref={cfRef}
onClick={cfShow}
icon="pi pi-save"
size="small"
severity="danger"
label="Save as Map Default"
className="py-[4px]"
disabled={!hasSettingsForExport || !isDirty}
/>
</div>
{!isDirty && <span className="text-red-500/70 text-[12px]">*Local and remote are identical.</span>}
<span className="text-stone-500 text-[12px]">
*Will save your current settings as the default for all new users of this map. This action will overwrite any
existing default settings.
</span>
</div>
<Toast ref={toast} />
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="Your settings will overwrite default. Sure?."
icon="pi pi-exclamation-triangle"
accept={handleSync}
/>
</div>
);
};

View File

@@ -7,9 +7,14 @@ import {
import { useMapSettings } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/MapSettingsProvider.tsx';
import { SettingsListItem } from '@/hooks/Mapper/components/mapRootContent/components/MapSettings/types.ts';
import { useCallback } from 'react';
import { Button } from 'primereact/button';
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
export const CommonSettings = () => {
const { renderSettingItem } = useMapSettings();
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const renderSettingsList = useCallback(
(list: SettingsListItem[]) => {
@@ -18,6 +23,8 @@ export const CommonSettings = () => {
[renderSettingItem],
);
const handleResetSettings = () => {};
return (
<div className="flex flex-col h-full gap-1">
<div>
@@ -29,6 +36,33 @@ export const CommonSettings = () => {
<div className="grid grid-cols-[1fr_auto]">{renderSettingItem(MINI_MAP_PLACEMENT)}</div>
<div className="grid grid-cols-[1fr_auto]">{renderSettingItem(PINGS_PLACEMENT)}</div>
<div className="grid grid-cols-[1fr_auto]">{renderSettingItem(THEME_SETTING)}</div>
<div className="border-b-2 border-dotted border-stone-700/50 h-px my-3" />
<div className="grid grid-cols-[1fr_auto]">
<div />
<WdTooltipWrapper content="This dangerous action. And can not be undone" position={TooltipPosition.top}>
<Button
// @ts-ignore
ref={cfRef}
className="py-[4px]"
onClick={cfShow}
outlined
size="small"
severity="danger"
label="Reset Settings"
/>
</WdTooltipWrapper>
</div>
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="All settings for this map will be reset to default."
icon="pi pi-exclamation-triangle"
accept={handleResetSettings}
/>
</div>
);
};

View File

@@ -0,0 +1,90 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { Divider } from 'primereact/divider';
import { callToastError, callToastSuccess, callToastWarn } from '@/hooks/Mapper/helpers';
type SaveDefaultSettingsReturn = { success: boolean; error: string };
export const DefaultSettings = () => {
const {
outCommand,
storedSettings: { getSettingsForExport },
data: { userPermissions },
} = useMapRootState();
const [loading, setLoading] = useState(false);
const toast = useRef<Toast | null>(null);
const refVars = useRef({ getSettingsForExport, outCommand });
refVars.current = { getSettingsForExport, outCommand };
const handleSaveAsDefault = useCallback(async () => {
const settings = refVars.current.getSettingsForExport();
if (!settings) {
callToastWarn(toast.current, 'No settings to save');
return;
}
setLoading(true);
let response: SaveDefaultSettingsReturn;
try {
response = await refVars.current.outCommand({
type: OutCommand.saveDefaultSettings,
data: { settings },
});
} catch (error) {
console.error('Save default settings error:', error);
callToastError(toast.current, 'Failed to save default settings');
setLoading(false);
return;
}
if (response.success) {
callToastSuccess(toast.current, 'Default settings saved successfully');
setLoading(false);
return;
}
callToastError(toast.current, response.error || 'Failed to save default settings');
setLoading(false);
}, []);
if (!userPermissions?.admin_map) {
return null;
}
return (
<>
<Divider />
<div className="w-full h-full flex flex-col gap-5">
<h3 className="text-lg font-semibold">Default Settings (Admin Only)</h3>
<div className="flex flex-col gap-1">
<div>
<Button
onClick={handleSaveAsDefault}
icon="pi pi-save"
size="small"
severity="danger"
label="Save as Map Default"
className="py-[4px]"
loading={loading}
disabled={loading}
/>
</div>
<span className="text-stone-500 text-[12px]">
*Will save your current settings as the default for all new users of this map. This action will overwrite
any existing default settings.
</span>
</div>
<Toast ref={toast} />
</div>
</>
);
};

View File

@@ -0,0 +1,97 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Toast } from 'primereact/toast';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { callToastSuccess } from '@/hooks/Mapper/helpers';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { RemoteAdminSettingsResponse } from '@/hooks/Mapper/mapRootProvider/types.ts';
export const ServerSettings = () => {
const {
storedSettings: { applySettings },
outCommand,
} = useMapRootState();
const [hasSettings, setHasSettings] = useState(false);
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const toast = useRef<Toast | null>(null);
const handleSync = useCallback(async () => {
let res: RemoteAdminSettingsResponse | undefined;
try {
res = await outCommand({ type: OutCommand.getDefaultSettings, data: null });
} catch (error) {
// do nothing
}
if (res?.default_settings == null) {
applySettings(createDefaultWidgetSettings());
return;
}
try {
applySettings(parseMapUserSettings(res.default_settings));
callToastSuccess(toast.current, 'Settings synchronized successfully');
} catch (error) {
applySettings(createDefaultWidgetSettings());
}
}, [applySettings, outCommand]);
useEffect(() => {
const load = async () => {
let res: RemoteAdminSettingsResponse | undefined;
try {
res = await outCommand({ type: OutCommand.getDefaultSettings, data: null });
} catch (error) {
// do nothing
}
if (res?.default_settings == null) {
return;
}
setHasSettings(true);
};
load();
}, [outCommand]);
return (
<div className="w-full h-full flex flex-col gap-5">
<div className="flex flex-col gap-1">
<div>
<Button
// @ts-ignore
ref={cfRef}
onClick={cfShow}
icon="pi pi-file-import"
size="small"
severity="warning"
label="Sync with Default Settings"
className="py-[4px]"
disabled={!hasSettings}
/>
</div>
{!hasSettings && (
<span className="text-red-500/70 text-[12px]">*Default settings was not set by map administrator.</span>
)}
<span className="text-stone-500 text-[12px]">*Will apply admin settings which set as Default for map.</span>
</div>
<Toast ref={toast} />
<ConfirmPopup
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="You lost your current settings. Sure?."
icon="pi pi-exclamation-triangle"
accept={handleSync}
/>
</div>
);
};

View File

@@ -28,6 +28,9 @@ export const WidgetsSettings = ({}: WidgetsSettingsProps) => {
/>
))}
</div>
<div className="border-b-2 border-dotted border-stone-700/50 h-px my-3" />
<div className="grid grid-cols-[1fr_auto]">
<div />
<Button className="py-[4px]" onClick={resetWidgets} outlined size="small" label="Reset Widgets"></Button>

View File

@@ -1,8 +1,6 @@
import { Dialog } from 'primereact/dialog';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { useCallback, useRef, useState } from 'react';
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
@@ -11,10 +9,13 @@ import {
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
import { Toast } from 'primereact/toast';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import { saveTextFile } from '@/hooks/Mapper/utils';
import { Button } from 'primereact/button';
import { ConfirmPopup } from 'primereact/confirmpopup';
import { Dialog } from 'primereact/dialog';
import { Toast } from 'primereact/toast';
import { useCallback, useRef } from 'react';
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
return {
@@ -24,10 +25,7 @@ const createSettings = function <T>(lsSettings: string | null, defaultValues: T)
};
export const OldSettingsDialog = () => {
const cpRemoveBtnRef = useRef<HTMLElement>();
const [cpRemoveVisible, setCpRemoveVisible] = useState(false);
const handleShowCP = useCallback(() => setCpRemoveVisible(true), []);
const handleHideCP = useCallback(() => setCpRemoveVisible(false), []);
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
const toast = useRef<Toast | null>(null);
const {
@@ -43,7 +41,7 @@ export const OldSettingsDialog = () => {
const widgetKills = localStorage.getItem('kills:widget:settings');
const onTheMapOld = localStorage.getItem('window:onTheMap:settings');
const widgetsOld = localStorage.getItem('windows:settings:v2');
const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_5');
const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_6');
const out: MapUserSettings = {
killsWidget: createSettings(widgetKills, DEFAULT_KILLS_WIDGET_SETTINGS),
@@ -120,7 +118,7 @@ export const OldSettingsDialog = () => {
localStorage.removeItem('kills:widget:settings');
localStorage.removeItem('window:onTheMap:settings');
localStorage.removeItem('windows:settings:v2');
localStorage.removeItem('wanderer_system_signature_settings_v6_5');
localStorage.removeItem('wanderer_system_signature_settings_v6_6');
checkOldSettings();
}, [checkOldSettings]);
@@ -143,8 +141,8 @@ export const OldSettingsDialog = () => {
<div className="flex items-center justify-end">
<Button
// @ts-ignore
ref={cpRemoveBtnRef}
onClick={handleShowCP}
ref={cfRef}
onClick={cfShow}
icon="pi pi-exclamation-triangle"
size="small"
severity="warning"
@@ -192,9 +190,9 @@ export const OldSettingsDialog = () => {
</Dialog>
<ConfirmPopup
target={cpRemoveBtnRef.current}
visible={cpRemoveVisible}
onHide={handleHideCP}
target={cfRef.current}
visible={cfVisible}
onHide={cfHide}
message="After click dialog will disappear. Ready?"
icon="pi pi-exclamation-triangle"
accept={handleProceed}

View File

@@ -13,6 +13,8 @@ import { InputText } from 'primereact/inputtext';
import { IconField } from 'primereact/iconfield';
const itemTemplate = (item: CharacterTypeRaw & WithIsOwnCharacter, options: VirtualScrollerTemplateOptions) => {
const showAllyLogoPlaceholder = options.props.items?.some(x => x.alliance_id != null);
return (
<div
className={clsx(classes.CharacterRow, 'w-full box-border px-2 py-1', {
@@ -22,7 +24,15 @@ const itemTemplate = (item: CharacterTypeRaw & WithIsOwnCharacter, options: Virt
})}
style={{ height: options.props.itemSize + 'px' }}
>
<CharacterCard showCorporationLogo showAllyLogo showSystem showTicker showShip {...item} />
<CharacterCard
showCorporationLogo
showAllyLogo
showAllyLogoPlaceholder={showAllyLogoPlaceholder}
showSystem
showTicker
showShip
{...item}
/>
</div>
);
};

View File

@@ -94,6 +94,10 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
out = { ...out, type: values.type };
}
if (values.temporary_name != null) {
out = { ...out, temporary_name: values.temporary_name };
}
if (signatureData.group !== SignatureGroup.Wormhole) {
out = { ...out, name: '' };
}

View File

@@ -4,6 +4,7 @@ import { SignatureWormholeTypeSelect } from '@/hooks/Mapper/components/mapRootCo
import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
import { SignatureEOLCheckbox } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureEOLCheckbox';
import { SignatureTempName } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureTempName.tsx';
export const SignatureGroupContentWormholes = () => {
const { watch } = useFormContext<SystemSignature>();
@@ -32,6 +33,11 @@ export const SignatureGroupContentWormholes = () => {
<span>EOL:</span>
<SignatureEOLCheckbox name="isEOL" />
</label>
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
<span>Temp. Name:</span>
<SignatureTempName />
</label>
</>
);
};

View File

@@ -0,0 +1,15 @@
import { Controller, useFormContext } from 'react-hook-form';
import { InputText } from 'primereact/inputtext';
import { SystemSignature } from '@/hooks/Mapper/types';
export const SignatureTempName = () => {
const { control } = useFormContext<SystemSignature>();
return (
<Controller
name="temporary_name"
control={control}
render={({ field }) => <InputText placeholder="Temporary Name" value={field.value} onChange={field.onChange} />}
/>
);
};

View File

@@ -1,6 +1,6 @@
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { CommandSelectSystems, OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
import isEqual from 'lodash.isequal';
@@ -88,6 +88,18 @@ export const MapWrapper = () => {
useMapEventListener(event => {
runCommand(event);
if (event.name === Commands.init) {
const { selectedSystems } = ref.current;
if (selectedSystems.length === 0) {
return;
}
runCommand({
name: Commands.selectSystems,
data: { systems: selectedSystems } as CommandSelectSystems,
});
}
});
const onSelectionChange: OnMapSelectionChange = useCallback(
@@ -181,24 +193,20 @@ export const MapWrapper = () => {
ref.current.systemContextProps.systemId && setOpenSettings(ref.current.systemContextProps.systemId);
}, []);
const handleTogglePing = useCallback(async (type: PingType, solar_system_id: string, hasPing: boolean) => {
if (hasPing) {
// Find the ping for this solar system to get its ID
const ping = pings.find(p => p.solar_system_id === solar_system_id);
if (!ping) {
console.error('Cannot find ping for solar system:', solar_system_id);
const handleTogglePing = useCallback(
async (type: PingType, solar_system_id: string, ping_id: string | undefined, hasPing: boolean) => {
if (hasPing) {
await outCommand({
type: OutCommand.cancelPing,
data: { type, id: ping_id },
});
return;
}
await outCommand({
type: OutCommand.cancelPing,
data: { type, id: ping.id },
});
return;
}
setOpenPing({ type, solar_system_id });
}, [pings, outCommand]);
setOpenPing({ type, solar_system_id });
},
[],
);
const handleCustomLabelDialog = useCallback(() => {
const { systemContextProps } = ref.current;

View File

@@ -24,6 +24,7 @@ export type CharacterCardProps = {
useSystemsCache?: boolean;
showCorporationLogo?: boolean;
showAllyLogo?: boolean;
showAllyLogoPlaceholder?: boolean;
simpleMode?: boolean;
} & WithIsOwnCharacter &
WithClassName;
@@ -47,6 +48,7 @@ export const CharacterCard = ({
showShipName,
showCorporationLogo,
showAllyLogo,
showAllyLogoPlaceholder,
showTicker,
useSystemsCache,
className,
@@ -217,6 +219,18 @@ export const CharacterCard = ({
/>
</WdTooltipWrapper>
)}
{showAllyLogo && showAllyLogoPlaceholder && !char.alliance_id && (
<WdTooltipWrapper position={TooltipPosition.top} content="No alliance">
<span
className={clsx(
'min-w-[33px] min-h-[33px] w-[33px] h-[33px]',
'flex transition-[border-color,opacity] duration-250 rounded-none',
'wd-bg-default',
)}
/>
</WdTooltipWrapper>
)}
</div>
<div className="flex flex-col flex-grow overflow-hidden w-[50px]">

View File

@@ -12,14 +12,16 @@ export enum SETTINGS_KEYS {
SORT_FIELD = 'sortField',
SORT_ORDER = 'sortOrder',
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
SHOW_UPDATED_COLUMN = 'show_updated_column',
SHOW_ADDED_COLUMN = 'show_added_column',
SHOW_CHARACTER_COLUMN = 'show_character_column',
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
SHOW_GROUP_COLUMN = 'show_group_column',
SHOW_UPDATED_COLUMN = 'show_updated_column',
LAZY_DELETE_SIGNATURES = 'lazy_delete_signatures',
KEEP_LAZY_DELETE = 'keep_lazy_delete_enabled',
DELETION_TIMING = 'deletion_timing',
COLOR_BY_TYPE = 'color_by_type',
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
// From SignatureKind
COSMIC_ANOMALY = SignatureKind.CosmicAnomaly,
@@ -45,6 +47,8 @@ export const DEFAULT_SIGNATURE_SETTINGS: SignatureSettingsType = {
[SETTINGS_KEYS.SORT_FIELD]: 'inserted_at',
[SETTINGS_KEYS.SORT_ORDER]: -1,
[SETTINGS_KEYS.SHOW_GROUP_COLUMN]: true,
[SETTINGS_KEYS.SHOW_ADDED_COLUMN]: true,
[SETTINGS_KEYS.SHOW_UPDATED_COLUMN]: true,
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,

View File

@@ -2,3 +2,4 @@ export * from './sortWHClasses';
export * from './parseSignatures';
export * from './getSystemById';
export * from './getEveImageUrl';
export * from './toastHelpers';

View File

@@ -0,0 +1,28 @@
import { Toast } from 'primereact/toast';
export const callToastWarn = (toast: Toast | null, msg: string, life = 3000) => {
toast?.show({
severity: 'warn',
summary: 'Warning',
detail: msg,
life,
});
};
export const callToastError = (toast: Toast | null, msg: string, life = 3000) => {
toast?.show({
severity: 'error',
summary: 'Error',
detail: msg,
life,
});
};
export const callToastSuccess = (toast: Toast | null, msg: string, life = 3000) => {
toast?.show({
severity: 'success',
summary: 'Success',
detail: msg,
life,
});
};

View File

@@ -3,3 +3,4 @@ export * from './useHotkey';
export * from './usePageVisibility';
export * from './useSkipContextMenu';
export * from './useThrottle';
export * from './useConfirmPopup';

View File

@@ -0,0 +1,10 @@
import { useCallback, useRef, useState } from 'react';
export const useConfirmPopup = () => {
const cfRef = useRef<HTMLElement>();
const [cfVisible, setCfVisible] = useState(false);
const cfShow = useCallback(() => setCfVisible(true), []);
const cfHide = useCallback(() => setCfVisible(false), []);
return { cfRef, cfVisible, cfShow, cfHide };
};

View File

@@ -131,6 +131,7 @@ export interface MapRootContextProps {
hasOldSettings: boolean;
getSettingsForExport(): string | undefined;
applySettings(settings: MapUserSettings): boolean;
resetSettings(settings: MapUserSettings): void;
checkOldSettings(): void;
};
}
@@ -175,6 +176,7 @@ const MapRootContext = createContext<MapRootContextProps>({
hasOldSettings: false,
getSettingsForExport: () => '',
applySettings: () => false,
resetSettings: () => null,
checkOldSettings: () => null,
},
});
@@ -196,7 +198,7 @@ const MapRootHandlers = forwardRef(({ children }: WithChildren, fwdRef: Forwarde
export const MapRootProvider = ({ children, fwdRef, outCommand }: MapRootProviderProps) => {
const { update, ref } = useContextStore<MapRootData>({ ...INITIAL_DATA });
const storedSettings = useMapUserSettings(ref);
const storedSettings = useMapUserSettings(ref, outCommand);
const { windowsSettings, toggleWidgetVisibility, updateWidgetSettings, resetWidgets } =
useStoreWidgets(storedSettings);

View File

@@ -0,0 +1,30 @@
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
// TODO - we need provide and compare version
const createWidgetSettingsWithVersion = <T>(settings: T) => {
return {
version: 0,
settings,
};
};
export const createDefaultWidgetSettings = (): MapUserSettings => {
return {
killsWidget: createWidgetSettingsWithVersion(DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createWidgetSettingsWithVersion(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettingsWithVersion(getDefaultWidgetProps()),
routes: createWidgetSettingsWithVersion(DEFAULT_ROUTES_SETTINGS),
onTheMap: createWidgetSettingsWithVersion(DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettingsWithVersion(DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettingsWithVersion(STORED_INTERFACE_DEFAULT_VALUES),
};
};

View File

@@ -0,0 +1,66 @@
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types';
import { Dispatch, SetStateAction, useCallback, useEffect, useRef } from 'react';
import {
MapUserSettings,
MapUserSettingsStructure,
RemoteAdminSettingsResponse,
} from '@/hooks/Mapper/mapRootProvider/types.ts';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
import { parseMapUserSettings } from '@/hooks/Mapper/components/helpers';
interface UseActualizeRemoteMapSettingsProps {
outCommand: OutCommandHandler;
mapUserSettings: MapUserSettingsStructure;
applySettings: (val: MapUserSettings) => void;
setMapUserSettings: Dispatch<SetStateAction<MapUserSettingsStructure>>;
map_slug: string | null;
}
export const useActualizeRemoteMapSettings = ({
outCommand,
mapUserSettings,
setMapUserSettings,
applySettings,
map_slug,
}: UseActualizeRemoteMapSettingsProps) => {
const refVars = useRef({ applySettings, mapUserSettings, setMapUserSettings, map_slug });
refVars.current = { applySettings, mapUserSettings, setMapUserSettings, map_slug };
const actualizeRemoteMapSettings = useCallback(async () => {
const { applySettings } = refVars.current;
let res: RemoteAdminSettingsResponse | undefined;
try {
res = await outCommand({ type: OutCommand.getDefaultSettings, data: null });
} catch (error) {
// do nothing
}
if (res?.default_settings == null) {
applySettings(createDefaultWidgetSettings());
return;
}
try {
applySettings(parseMapUserSettings(res.default_settings));
} catch (error) {
applySettings(createDefaultWidgetSettings());
}
}, [outCommand]);
useEffect(() => {
const { mapUserSettings } = refVars.current;
// INFO: Do nothing if slug is not set
if (map_slug == null) {
return;
}
// INFO: Do nothing if user have already data
if (map_slug in mapUserSettings) {
return;
}
actualizeRemoteMapSettings();
}, [actualizeRemoteMapSettings, map_slug]);
};

View File

@@ -1,44 +1,16 @@
import useLocalStorageState from 'use-local-storage-state';
import { MapUserSettings, MapUserSettingsStructure } from '@/hooks/Mapper/mapRootProvider/types.ts';
import {
DEFAULT_KILLS_WIDGET_SETTINGS,
DEFAULT_ON_THE_MAP_SETTINGS,
DEFAULT_ROUTES_SETTINGS,
DEFAULT_WIDGET_LOCAL_SETTINGS,
getDefaultWidgetProps,
STORED_INTERFACE_DEFAULT_VALUES,
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
import { useCallback, useEffect, useRef, useState } from 'react';
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures';
import { MapRootData } from '@/hooks/Mapper/mapRootProvider';
import { useSettingsValueAndSetter } from '@/hooks/Mapper/mapRootProvider/hooks/useSettingsValueAndSetter.ts';
import fastDeepEqual from 'fast-deep-equal';
// import { actualizeSettings } from '@/hooks/Mapper/mapRootProvider/helpers';
// TODO - we need provide and compare version
const createWidgetSettingsWithVersion = <T>(settings: T) => {
return {
version: 0,
settings,
};
};
const createDefaultWidgetSettings = (): MapUserSettings => {
return {
killsWidget: createWidgetSettingsWithVersion(DEFAULT_KILLS_WIDGET_SETTINGS),
localWidget: createWidgetSettingsWithVersion(DEFAULT_WIDGET_LOCAL_SETTINGS),
widgets: createWidgetSettingsWithVersion(getDefaultWidgetProps()),
routes: createWidgetSettingsWithVersion(DEFAULT_ROUTES_SETTINGS),
onTheMap: createWidgetSettingsWithVersion(DEFAULT_ON_THE_MAP_SETTINGS),
signaturesWidget: createWidgetSettingsWithVersion(DEFAULT_SIGNATURE_SETTINGS),
interface: createWidgetSettingsWithVersion(STORED_INTERFACE_DEFAULT_VALUES),
};
};
import { OutCommandHandler } from '@/hooks/Mapper/types';
import { useActualizeRemoteMapSettings } from '@/hooks/Mapper/mapRootProvider/hooks/useActualizeRemoteMapSettings.ts';
import { createDefaultWidgetSettings } from '@/hooks/Mapper/mapRootProvider/helpers/createDefaultWidgetSettings.ts';
const EMPTY_OBJ = {};
export const useMapUserSettings = ({ map_slug }: MapRootData) => {
export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCommandHandler) => {
const [isReady, setIsReady] = useState(false);
const [hasOldSettings, setHasOldSettings] = useState(false);
@@ -49,19 +21,25 @@ export const useMapUserSettings = ({ map_slug }: MapRootData) => {
const ref = useRef({ mapUserSettings, setMapUserSettings, map_slug });
ref.current = { mapUserSettings, setMapUserSettings, map_slug };
useEffect(() => {
const { mapUserSettings, setMapUserSettings } = ref.current;
if (map_slug === null) {
return;
const applySettings = useCallback((settings: MapUserSettings) => {
const { map_slug, mapUserSettings, setMapUserSettings } = ref.current;
if (map_slug == null) {
return false;
}
if (!(map_slug in mapUserSettings)) {
setMapUserSettings({
...mapUserSettings,
[map_slug]: createDefaultWidgetSettings(),
});
if (fastDeepEqual(settings, mapUserSettings[map_slug])) {
return false;
}
}, [map_slug]);
setMapUserSettings(old => ({
...old,
[map_slug]: settings,
}));
return true;
}, []);
useActualizeRemoteMapSettings({ outCommand, applySettings, mapUserSettings, setMapUserSettings, map_slug });
const [interfaceSettings, setInterfaceSettings] = useSettingsValueAndSetter(
mapUserSettings,
@@ -178,23 +156,9 @@ export const useMapUserSettings = ({ map_slug }: MapRootData) => {
return JSON.stringify(ref.current.mapUserSettings[map_slug]);
}, []);
const applySettings = useCallback((settings: MapUserSettings) => {
const { map_slug, mapUserSettings, setMapUserSettings } = ref.current;
if (map_slug == null) {
return false;
}
if (fastDeepEqual(settings, mapUserSettings[map_slug])) {
return false;
}
setMapUserSettings(old => ({
...old,
[map_slug]: settings,
}));
return true;
}, []);
const resetSettings = useCallback(() => {
applySettings(createDefaultWidgetSettings());
}, [applySettings]);
return {
isReady,
@@ -217,6 +181,7 @@ export const useMapUserSettings = ({ map_slug }: MapRootData) => {
getSettingsForExport,
applySettings,
resetSettings,
checkOldSettings,
};
};

View File

@@ -85,3 +85,7 @@ export type MapUserSettings = {
export type MapUserSettingsStructure = {
[mapId: string]: MapUserSettings;
};
export type WdResponse<T> = T;
export type RemoteAdminSettingsResponse = { default_settings?: string };

View File

@@ -27,6 +27,7 @@ export enum Commands {
userRoutes = 'user_routes',
centerSystem = 'center_system',
selectSystem = 'select_system',
selectSystems = 'select_systems',
linkSignatureToSystem = 'link_signature_to_system',
signaturesUpdated = 'signatures_updated',
systemCommentAdded = 'system_comment_added',
@@ -60,6 +61,7 @@ export type Command =
| Commands.routes
| Commands.userRoutes
| Commands.selectSystem
| Commands.selectSystems
| Commands.centerSystem
| Commands.linkSignatureToSystem
| Commands.signaturesUpdated
@@ -118,6 +120,10 @@ export type CommandUserRoutes = RoutesList;
export type CommandKillsUpdated = Kill[];
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
export type CommandSelectSystem = string | undefined;
export type CommandSelectSystems = {
systems: string[];
delay?: number;
};
export type CommandCenterSystem = string | undefined;
export type CommandLinkSignatureToSystem = {
solar_system_source: number;
@@ -187,6 +193,7 @@ export interface CommandData {
[Commands.killsUpdated]: CommandKillsUpdated;
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
[Commands.selectSystem]: CommandSelectSystem;
[Commands.selectSystems]: CommandSelectSystems;
[Commands.centerSystem]: CommandCenterSystem;
[Commands.linkSignatureToSystem]: CommandLinkSignatureToSystem;
[Commands.signaturesUpdated]: CommandLinkSignaturesUpdated;
@@ -269,6 +276,8 @@ export enum OutCommand {
showTracking = 'show_tracking',
getUserSettings = 'get_user_settings',
updateUserSettings = 'update_user_settings',
saveDefaultSettings = 'save_default_settings',
getDefaultSettings = 'get_default_settings',
unlinkSignature = 'unlink_signature',
searchSystems = 'search_systems',
undoDeleteSignatures = 'undo_delete_signatures',

View File

@@ -48,6 +48,7 @@ export type SystemSignature = {
inserted_at?: string;
updated_at?: string;
deleted?: boolean;
temporary_name?: string;
};
export interface ExtendedSystemSignature extends SystemSignature {

View File

@@ -79,7 +79,7 @@
"sass-loader": "^14.2.1",
"ts-jest": "^29.1.2",
"typescript": "^5.2.2",
"vite": "^5.0.5",
"vite": "^6.3.5",
"vite-plugin-cdn-import": "^1.0.1"
},
"peerDependencies": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

File diff suppressed because it is too large Load Diff

82
clean_changelog.py Normal file
View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""
Script to clean up CHANGELOG.md by removing empty version entries.
An empty version entry has only a version header followed by empty lines,
without any actual content (### Bug Fixes: or ### Features: sections).
"""
import re
def clean_changelog():
with open('./CHANGELOG.md', 'r') as f:
content = f.read()
# Split content into sections based on version headers
version_pattern = r'^## \[v\d+\.\d+\.\d+\].*?\([^)]+\)$'
# Find all version headers with their positions
matches = list(re.finditer(version_pattern, content, re.MULTILINE))
# Build new content by keeping only non-empty versions
new_content = ""
# Keep the header (everything before first version)
if matches:
new_content += content[:matches[0].start()]
else:
# No versions found, keep original
return content
for i, match in enumerate(matches):
version_start = match.start()
# Find the end of this version section (start of next version or end of file)
if i + 1 < len(matches):
version_end = matches[i + 1].start()
else:
version_end = len(content)
version_section = content[version_start:version_end]
# Check if this version has actual content
# Look for ### Bug Fixes: or ### Features: followed by actual content
has_content = False
# Split the section into lines
lines = version_section.split('\n')
# Look for content sections
in_content_section = False
for line in lines:
line_stripped = line.strip()
# Check if we're entering a content section
if line_stripped.startswith('### Bug Fixes:') or line_stripped.startswith('### Features:'):
in_content_section = True
continue
# If we're in a content section and find non-empty content
if in_content_section:
if line_stripped and not line_stripped.startswith('###') and not line_stripped.startswith('##'):
# This is actual content (not just another header)
if line_stripped.startswith('*') or len(line_stripped) > 0:
has_content = True
break
elif line_stripped.startswith('##'):
# We've reached the next version, stop looking
break
# Only keep versions with actual content
if has_content:
new_content += version_section
return new_content
if __name__ == "__main__":
cleaned_content = clean_changelog()
# Write the cleaned content back to the file
with open('./CHANGELOG.md', 'w') as f:
f.write(cleaned_content)
print("CHANGELOG.md has been cleaned up successfully!")

View File

@@ -102,6 +102,23 @@ config :error_tracker,
repo: WandererApp.Repo,
otp_app: :wanderer_app
# Security Audit Configuration
config :wanderer_app, WandererApp.SecurityAudit,
enabled: true,
# Set to true in production for better performance
async: false,
batch_size: 100,
flush_interval: 5000,
log_level: :info,
threat_detection: %{
enabled: true,
max_failed_attempts: 5,
max_permission_denials: 10,
window_seconds: 300,
bulk_operation_threshold: 10000
},
retention_days: 90
config :git_ops,
mix_project: Mix.Project.get!(),
changelog_file: "CHANGELOG.md",

View File

@@ -11,11 +11,13 @@ config :wanderer_app, WandererAppWeb.Endpoint,
config :wanderer_app, WandererApp.Repo,
ssl: false,
stacktrace: true,
show_sensitive_data_on_connection_error: true,
show_sensitive_data_on_connection_error: false,
pool_size: 15,
migration_timestamps: [type: :utc_datetime_usec],
migration_lock: nil,
queue_target: 5000
queue_target: 5000,
queue_interval: 1000,
checkout_timeout: 15000
# Configures Swoosh API Client
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: WandererApp.Finch
@@ -27,5 +29,8 @@ config :swoosh, local: false
config :logger,
level: :info
# Enable async security audit processing in production
config :wanderer_app, WandererApp.SecurityAudit, async: true
# Runtime production configuration, including reading
# of environment variables, is done on config/runtime.exs.

View File

@@ -129,6 +129,8 @@ config :wanderer_app,
admin_username: System.get_env("WANDERER_ADMIN_USERNAME", "admin"),
admin_password: System.get_env("WANDERER_ADMIN_PASSWORD"),
admins: admins,
base_metrics_only:
System.get_env("WANDERER_BASE_METRICS_ONLY", "false") |> String.to_existing_atom(),
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1"),

View File

@@ -28,6 +28,7 @@ defmodule WandererApp.Api do
resource WandererApp.Api.MapSubscription
resource WandererApp.Api.MapTransaction
resource WandererApp.Api.MapUserSettings
resource WandererApp.Api.MapDefaultSettings
resource WandererApp.Api.User
resource WandererApp.Api.ShipTypeInfo
resource WandererApp.Api.UserActivity

View File

@@ -124,7 +124,7 @@ defmodule WandererApp.Api.Character do
update :update_corporation do
require_atomic? false
accept([:corporation_id, :corporation_name, :corporation_ticker, :alliance_id])
accept([:corporation_id, :corporation_name, :corporation_ticker])
end
update :update_alliance do

View File

@@ -79,8 +79,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
accept [
:map_id,
:character_id,
:tracked,
:followed
:tracked
]
argument :map_id, :uuid, allow_nil?: false

View File

@@ -0,0 +1,145 @@
defmodule WandererApp.Api.MapDefaultSettings do
@moduledoc """
Resource for storing default map settings that admins can configure.
These settings will be applied to new users when they first access the map.
"""
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer,
extensions: [AshJsonApi.Resource]
postgres do
repo(WandererApp.Repo)
table("map_default_settings")
end
json_api do
type "map_default_settings"
includes([
:map,
:created_by,
:updated_by
])
routes do
base("/map_default_settings")
get(:read)
index(:read)
post(:create)
patch(:update)
delete(:destroy)
end
end
code_interface do
define(:create, action: :create)
define(:update, action: :update)
define(:destroy, action: :destroy)
define(:get_by_map_id, action: :get_by_map_id)
end
actions do
default_accept [
:map_id,
:settings
]
defaults [:read, :destroy]
create :create do
primary?(true)
accept [:map_id, :settings]
change relate_actor(:created_by)
change relate_actor(:updated_by)
change fn changeset, _context ->
changeset
|> validate_json_settings()
end
end
update :update do
primary?(true)
accept [:settings]
# Required for managing relationships
require_atomic? false
change relate_actor(:updated_by)
change fn changeset, _context ->
changeset
|> validate_json_settings()
end
end
read :get_by_map_id do
argument :map_id, :uuid, allow_nil?: false
filter expr(map_id == ^arg(:map_id))
prepare fn query, _context ->
Ash.Query.limit(query, 1)
end
end
end
attributes do
uuid_primary_key :id
attribute :settings, :string do
allow_nil? false
constraints min_length: 2
description "JSON string containing the default map settings"
end
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end
relationships do
belongs_to :map, WandererApp.Api.Map do
primary_key? false
allow_nil? false
public? true
end
belongs_to :created_by, WandererApp.Api.Character do
allow_nil? true
public? true
end
belongs_to :updated_by, WandererApp.Api.Character do
allow_nil? true
public? true
end
end
identities do
identity :unique_map_settings, [:map_id]
end
defp validate_json_settings(changeset) do
case Ash.Changeset.get_attribute(changeset, :settings) do
nil ->
changeset
settings ->
case Jason.decode(settings) do
{:ok, _} ->
changeset
{:error, _} ->
Ash.Changeset.add_error(
changeset,
field: :settings,
message: "must be valid JSON"
)
end
end
end
end

View File

@@ -31,6 +31,8 @@ defmodule WandererApp.Api.MapSubscription do
end
code_interface do
define(:create, action: :create)
define(:by_id,
get_by: [:id],
action: :read
@@ -39,6 +41,15 @@ defmodule WandererApp.Api.MapSubscription do
define(:all_active, action: :all_active)
define(:all_by_map, action: :all_by_map)
define(:active_by_map, action: :active_by_map)
define(:destroy, action: :destroy)
define(:cancel, action: :cancel)
define(:expire, action: :expire)
define(:update_plan, action: :update_plan)
define(:update_characters_limit, action: :update_characters_limit)
define(:update_hubs_limit, action: :update_hubs_limit)
define(:update_active_till, action: :update_active_till)
define(:update_auto_renew, action: :update_auto_renew)
end
actions do
@@ -51,7 +62,7 @@ defmodule WandererApp.Api.MapSubscription do
:auto_renew?
]
defaults [:read]
defaults [:create, :read, :update, :destroy]
read :all_active do
prepare build(sort: [updated_at: :asc])

View File

@@ -31,6 +31,9 @@ defmodule WandererApp.Api.MapSystemComment do
end
code_interface do
define(:create, action: :create)
define(:destroy, action: :destroy)
define(:by_id,
get_by: [:id],
action: :read
@@ -46,7 +49,7 @@ defmodule WandererApp.Api.MapSystemComment do
:text
]
defaults [:read]
defaults [:read, :destroy]
create :create do
primary? true

View File

@@ -30,6 +30,7 @@ defmodule WandererApp.Api.MapSystemSignature do
code_interface do
define(:all_active, action: :all_active)
define(:create, action: :create)
define(:destroy, action: :destroy)
define(:update, action: :update)
define(:update_linked_system, action: :update_linked_system)
define(:update_type, action: :update_type)
@@ -62,6 +63,7 @@ defmodule WandererApp.Api.MapSystemSignature do
:eve_id,
:character_eve_id,
:name,
:temporary_name,
:description,
:kind,
:group,
@@ -101,6 +103,7 @@ defmodule WandererApp.Api.MapSystemSignature do
:eve_id,
:character_eve_id,
:name,
:temporary_name,
:description,
:kind,
:group,
@@ -120,6 +123,7 @@ defmodule WandererApp.Api.MapSystemSignature do
:eve_id,
:character_eve_id,
:name,
:temporary_name,
:description,
:kind,
:group,
@@ -195,6 +199,10 @@ defmodule WandererApp.Api.MapSystemSignature do
allow_nil? true
end
attribute :temporary_name, :string do
allow_nil? true
end
attribute :type, :string do
allow_nil? true
end
@@ -241,6 +249,7 @@ defmodule WandererApp.Api.MapSystemSignature do
:eve_id,
:character_eve_id,
:name,
:temporary_name,
:description,
:type,
:linked_system_id,

View File

@@ -29,19 +29,7 @@ defmodule WandererApp.Api.MapTransaction do
:amount
]
defaults [:create]
read :read do
primary?(true)
pagination offset?: true,
default_limit: 25,
max_page_size: 100,
countable: true,
required?: false
prepare build(sort: [inserted_at: :desc])
end
defaults [:create, :read, :update, :destroy]
read :by_map do
argument(:map_id, :string, allow_nil?: false)

View File

@@ -40,6 +40,7 @@ defmodule WandererApp.Api.MapUserSettings do
action: :read
)
define(:update_hubs, action: :update_hubs)
define(:update_settings, action: :update_settings)
define(:update_following_character, action: :update_following_character)
define(:update_main_character, action: :update_main_character)
@@ -52,7 +53,7 @@ defmodule WandererApp.Api.MapUserSettings do
:settings
]
defaults [:create, :read]
defaults [:create, :read, :update, :destroy]
update :update_settings do
accept [:settings]

View File

@@ -145,7 +145,12 @@ defmodule WandererApp.Api.UserActivity do
:admin_action,
:config_change,
:bulk_operation,
:security_alert
:security_alert,
# Subscription events
:subscription_created,
:subscription_updated,
:subscription_deleted,
:subscription_unknown
]
)

View File

@@ -45,6 +45,9 @@ defmodule WandererApp.Application do
Supervisor.child_spec({Cachex, name: :tracked_characters},
id: :tracked_characters_cache_worker
),
Supervisor.child_spec({Cachex, name: :wanderer_app_cache},
id: :wanderer_app_cache_worker
),
{Registry, keys: :unique, name: WandererApp.MapRegistry},
{Registry, keys: :unique, name: WandererApp.Character.TrackerRegistry},
{PartitionSupervisor,
@@ -60,6 +63,14 @@ defmodule WandererApp.Application do
if Application.get_env(:wanderer_app, :environment) == :test do
[]
else
security_audit_children =
if Application.get_env(:wanderer_app, WandererApp.SecurityAudit, [])
|> Keyword.get(:async, false) do
[WandererApp.SecurityAudit.AsyncProcessor]
else
[]
end
[
WandererApp.Esi.InitClientsTask,
WandererApp.Scheduler,
@@ -68,7 +79,7 @@ defmodule WandererApp.Application do
{WandererApp.Character.TrackerPoolSupervisor, []},
WandererApp.Character.TrackerManager,
WandererApp.Map.Manager
]
] ++ security_audit_children
end
children =

View File

@@ -0,0 +1,150 @@
defmodule WandererApp.Audit.RequestContext do
@moduledoc """
Provides utilities for extracting request context information
for audit logging purposes.
"""
require Logger
@doc """
Extract the client's IP address from the connection.
Simply returns the remote_ip from the connection.
"""
def get_ip_address(conn) do
conn.remote_ip
|> :inet.ntoa()
|> to_string()
rescue
error ->
Logger.warning("Failed to get IP address: #{inspect(error)}",
error: error,
stacktrace: __STACKTRACE__
)
"unknown"
end
@doc """
Extract the user agent from the request headers.
"""
def get_user_agent(conn) do
get_header(conn, "user-agent") || "unknown"
end
@doc """
Extract or generate a session ID for the request.
"""
def get_session_id(conn) do
# Try to get from session
session_id = get_session(conn, :session_id)
# Fall back to request ID
session_id || get_request_id(conn)
end
@doc """
Extract or generate a request ID for correlation.
"""
def get_request_id(conn) do
# Try standard request ID headers
get_header(conn, "x-request-id") ||
get_header(conn, "x-correlation-id") ||
Logger.metadata()[:request_id] ||
generate_request_id()
end
@doc """
Build a complete request metadata map for audit logging.
"""
def build_request_metadata(conn) do
%{
ip_address: get_ip_address(conn),
user_agent: get_user_agent(conn),
session_id: get_session_id(conn),
request_id: get_request_id(conn),
request_path: conn.request_path,
method: conn.method |> to_string() |> String.upcase(),
host: conn.host,
port: conn.port,
scheme: conn.scheme |> to_string()
}
end
@doc """
Extract user information from the connection.
Returns a map with user_id and any additional user context.
"""
def get_user_info(conn) do
case conn.assigns[:current_user] do
%{id: user_id} = user ->
%{
user_id: user_id,
username: Map.get(user, :username),
email: Map.get(user, :email)
}
nil ->
%{user_id: nil}
end
end
@doc """
Build a minimal request details map for audit events.
This is used by existing audit calls that expect specific fields.
"""
def build_request_details(conn) do
metadata = build_request_metadata(conn)
%{
ip_address: metadata.ip_address,
user_agent: metadata.user_agent,
session_id: metadata.session_id,
request_path: metadata.request_path,
method: metadata.method
}
end
@doc """
Set request context in the process dictionary for async logging.
"""
def set_request_context(conn) do
context = %{
metadata: build_request_metadata(conn),
user_info: get_user_info(conn),
timestamp: DateTime.utc_now()
}
Process.put(:audit_request_context, context)
conn
end
@doc """
Get request context from the process dictionary.
"""
def get_request_context do
Process.get(:audit_request_context)
end
# Private functions
defp get_header(conn, header) do
case Plug.Conn.get_req_header(conn, header) do
[value | _] -> value
[] -> nil
end
end
defp get_session(conn, key) do
conn
|> Plug.Conn.get_session(key)
rescue
_ -> nil
end
defp generate_request_id do
"req_#{:crypto.strong_rand_bytes(16) |> Base.url_encode64(padding: false)}"
end
end

View File

@@ -113,6 +113,63 @@ defmodule WandererApp.CachedInfo do
end
end
def get_solar_system_jumps() do
case WandererApp.Cache.lookup(:solar_system_jumps) do
{:ok, nil} ->
data = WandererApp.EveDataService.get_solar_system_jumps_data()
cache_items(data, :solar_system_jumps)
{:ok, data}
{:ok, data} ->
{:ok, data}
end
end
def get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
# Create normalized cache key (smaller ID first for bidirectional lookup)
{id1, id2} =
if from_solar_system_id < to_solar_system_id do
{from_solar_system_id, to_solar_system_id}
else
{to_solar_system_id, from_solar_system_id}
end
cache_key = "jump_#{id1}_#{id2}"
case WandererApp.Cache.lookup(cache_key) do
{:ok, nil} ->
# Build jump index if not exists
build_jump_index()
WandererApp.Cache.lookup(cache_key)
result ->
result
end
end
defp build_jump_index() do
case get_solar_system_jumps() do
{:ok, jumps} ->
jumps
|> Enum.each(fn jump ->
{id1, id2} =
if jump.from_solar_system_id < jump.to_solar_system_id do
{jump.from_solar_system_id, jump.to_solar_system_id}
else
{jump.to_solar_system_id, jump.from_solar_system_id}
end
cache_key = "jump_#{id1}_#{id2}"
WandererApp.Cache.put(cache_key, jump)
end)
_ ->
:error
end
end
def get_wormhole_types!() do
case get_wormhole_types() do
{:ok, wormhole_types} ->

View File

@@ -28,7 +28,7 @@ defmodule WandererApp.Character do
Cachex.put(:character_cache, character_id, character)
{:ok, character}
_ ->
error ->
{:error, :not_found}
end
@@ -263,7 +263,7 @@ defmodule WandererApp.Character do
end
end
defp maybe_merge_map_character_settings(%{id: character_id} = character, map_id, true) do
defp maybe_merge_map_character_settings(%{id: character_id} = character, _map_id, true) do
{:ok, tracking_paused} =
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
@@ -283,39 +283,44 @@ defmodule WandererApp.Character do
|> case do
{:ok, settings} when not is_nil(settings) ->
character
|> Map.put(:online, false)
|> Map.merge(settings)
|> Map.merge(%{
solar_system_id: settings.solar_system_id,
structure_id: settings.structure_id,
station_id: settings.station_id,
ship: settings.ship,
ship_name: settings.ship_name,
ship_item_id: settings.ship_item_id
})
_ ->
character
|> Map.put(:online, false)
|> Map.merge(@default_character_tracking_data)
end
|> Map.merge(%{tracking_paused: tracking_paused})
|> Map.merge(%{online: false, tracking_paused: tracking_paused})
end
defp prepare_search_results(result) do
{:ok, characters} =
_load_eve_info(Map.get(result, "character"), :get_character_info, &_map_character_info/1)
load_eve_info(Map.get(result, "character"), :get_character_info, &map_character_info/1)
{:ok, corporations} =
_load_eve_info(
load_eve_info(
Map.get(result, "corporation"),
:get_corporation_info,
&_map_corporation_info/1
&map_corporation_info/1
)
{:ok, alliances} =
_load_eve_info(Map.get(result, "alliance"), :get_alliance_info, &_map_alliance_info/1)
load_eve_info(Map.get(result, "alliance"), :get_alliance_info, &map_alliance_info/1)
[[characters | corporations] | alliances] |> List.flatten()
end
defp _load_eve_info(nil, _, _), do: {:ok, []}
defp load_eve_info(nil, _, _), do: {:ok, []}
defp _load_eve_info([], _, _), do: {:ok, []}
defp load_eve_info([], _, _), do: {:ok, []}
defp _load_eve_info(eve_ids, method, map_function),
defp load_eve_info(eve_ids, method, map_function),
do:
{:ok,
Enum.map(eve_ids, fn eve_id ->
@@ -331,7 +336,7 @@ defmodule WandererApp.Character do
end)
|> Enum.filter(fn result -> not is_nil(result) end)}
defp _map_alliance_info(info) do
defp map_alliance_info(info) do
%{
label: info["name"],
value: info["eve_id"] |> to_string(),
@@ -339,7 +344,7 @@ defmodule WandererApp.Character do
}
end
defp _map_character_info(info) do
defp map_character_info(info) do
%{
label: info["name"],
value: info["eve_id"] |> to_string(),
@@ -347,7 +352,7 @@ defmodule WandererApp.Character do
}
end
defp _map_corporation_info(info) do
defp map_corporation_info(info) do
%{
label: info["name"],
value: info["eve_id"] |> to_string(),

View File

@@ -49,11 +49,13 @@ defmodule WandererApp.Character.Activity do
"""
def process_character_activity(map_id, current_user) do
with {:ok, map_user_settings} <- get_map_user_settings(map_id, current_user.id),
raw_activity <- WandererApp.Map.get_character_activity(map_id),
{:ok, raw_activity} <- WandererApp.Map.get_character_activity(map_id),
{:ok, user_characters} <-
WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do
result = process_activity_data(raw_activity, map_user_settings, user_characters)
result
process_activity_data(raw_activity, map_user_settings, user_characters)
else
_ ->
[]
end
end

View File

@@ -7,6 +7,7 @@ defmodule WandererApp.Character.Tracker do
defstruct [
:character_id,
:alliance_id,
:corporation_id,
:opts,
server_online: true,
start_time: nil,
@@ -21,6 +22,8 @@ defmodule WandererApp.Character.Tracker do
@type t :: %__MODULE__{
character_id: integer,
alliance_id: integer,
corporation_id: integer,
opts: map,
server_online: boolean,
start_time: DateTime.t(),
@@ -34,10 +37,10 @@ defmodule WandererApp.Character.Tracker do
}
@pause_tracking_timeout :timer.minutes(60 * 10)
@offline_timeout :timer.minutes(5)
@online_error_timeout :timer.minutes(2)
@ship_error_timeout :timer.minutes(2)
@location_error_timeout :timer.minutes(2)
@offline_timeout :timer.minutes(10)
@online_error_timeout :timer.minutes(10)
@ship_error_timeout :timer.minutes(10)
@location_error_timeout :timer.minutes(10)
@online_forbidden_ttl :timer.seconds(7)
@online_limit_ttl :timer.seconds(7)
@forbidden_ttl :timer.seconds(5)
@@ -49,8 +52,15 @@ defmodule WandererApp.Character.Tracker do
def new(args), do: __struct__(args)
def init(args) do
character_id = args[:character_id]
{:ok, %{corporation_id: corporation_id, alliance_id: alliance_id}} =
WandererApp.Character.get_character(character_id)
%{
character_id: args[:character_id],
character_id: character_id,
corporation_id: corporation_id,
alliance_id: alliance_id,
start_time: DateTime.utc_now(),
opts: args
}
@@ -101,6 +111,7 @@ defmodule WandererApp.Character.Tracker do
if duration >= timeout do
pause_tracking(character_id)
WandererApp.Cache.delete("character:#{character_id}:#{type}_error_time")
:ok
else
@@ -113,15 +124,14 @@ defmodule WandererApp.Character.Tracker do
if WandererApp.Character.can_pause_tracking?(character_id) &&
not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
# Log character tracking statistics before pausing
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
Logger.debug(fn ->
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
Logger.warning(
"CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors",
character_id: character_id,
"CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors: #{inspect(character_id: character_id,
active_maps: length(character_state.active_maps),
is_online: character_state.is_online,
tracking_duration_minutes: get_tracking_duration_minutes(character_id)
)
tracking_duration_minutes: get_tracking_duration_minutes(character_id))}"
end)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
@@ -193,7 +203,7 @@ defmodule WandererApp.Character.Tracker do
access_token: access_token,
character_id: character_id
) do
{:ok, online} ->
{:ok, online} when is_map(online) ->
online = get_online(online)
if online.online == true do
@@ -258,7 +268,7 @@ defmodule WandererApp.Character.Tracker do
character_id: character_id
})
Logger.warning("ESI_ERROR: Character online tracking failed",
Logger.warning("ESI_ERROR: Character online tracking failed #{inspect(error)}",
character_id: character_id,
tracking_pool: tracking_pool,
error_type: error,
@@ -388,12 +398,21 @@ defmodule WandererApp.Character.Tracker do
{:ok, %{eve_id: eve_id, tracking_pool: tracking_pool}} =
WandererApp.Character.get_character(character_id)
case WandererApp.Esi.get_character_info(eve_id) do
{:ok, _info} ->
character_eve_id = eve_id |> String.to_integer()
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
update = maybe_update_corporation(character_state, eve_id |> String.to_integer())
WandererApp.Character.update_character_state(character_id, update)
alliance_id = character_aff_info |> Map.get("alliance_id")
corporation_id = character_aff_info |> Map.get("corporation_id")
updated_state =
character_state
|> maybe_update_corporation(corporation_id)
|> maybe_update_alliance(alliance_id)
WandererApp.Character.update_character_state(character_id, updated_state)
:ok
@@ -975,7 +994,38 @@ defmodule WandererApp.Character.Tracker do
end
end
defp update_alliance(%{character_id: character_id} = state, alliance_id) do
defp maybe_update_alliance(
%{character_id: character_id, alliance_id: old_alliance_id} = state,
alliance_id
)
when old_alliance_id != alliance_id and is_nil(alliance_id) do
{:ok, character} = WandererApp.Character.get_character(character_id)
character_update = %{
alliance_id: nil,
alliance_name: nil,
alliance_ticker: nil
}
{:ok, _character} =
Character.update_alliance(character, character_update)
WandererApp.Character.update_character(character_id, character_update)
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}}
)
state
end
defp maybe_update_alliance(
%{character_id: character_id, alliance_id: old_alliance_id} = state,
alliance_id
)
when old_alliance_id != alliance_id do
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
@@ -1015,7 +1065,13 @@ defmodule WandererApp.Character.Tracker do
end
end
defp update_corporation(%{character_id: character_id} = state, corporation_id) do
defp maybe_update_alliance(state, _alliance_id), do: state
defp maybe_update_corporation(
%{character_id: character_id, corporation_id: old_corporation_id} = state,
corporation_id
)
when old_corporation_id != corporation_id do
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|> case do
@@ -1027,16 +1083,13 @@ defmodule WandererApp.Character.Tracker do
|> WandererApp.Esi.get_corporation_info()
|> case do
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
alliance_id = Map.get(corporation_info, "alliance_id")
{:ok, character} =
WandererApp.Character.get_character(character_id)
character_update = %{
corporation_id: corporation_id,
corporation_name: corporation_name,
corporation_ticker: corporation_ticker,
alliance_id: alliance_id
corporation_ticker: corporation_ticker
}
{:ok, _character} =
@@ -1057,8 +1110,7 @@ defmodule WandererApp.Character.Tracker do
)
state
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|> maybe_update_alliance()
|> Map.merge(%{corporation_id: corporation_id})
error ->
Logger.warning(
@@ -1072,6 +1124,8 @@ defmodule WandererApp.Character.Tracker do
end
end
defp maybe_update_corporation(state, _corporation_id), do: state
defp maybe_update_ship(
%{
character_id: character_id
@@ -1153,58 +1207,6 @@ defmodule WandererApp.Character.Tracker do
structure_id != new_structure_id ||
station_id != new_station_id
defp maybe_update_corporation(
state,
character_eve_id
)
when not is_nil(character_eve_id) and is_integer(character_eve_id) do
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
update_corporation(state, character_aff_info |> Map.get("corporation_id"))
_error ->
state
end
end
defp maybe_update_corporation(
state,
_info
),
do: state
defp maybe_update_alliance(
%{character_id: character_id, alliance_id: alliance_id} =
state
) do
case alliance_id do
nil ->
{:ok, character} = WandererApp.Character.get_character(character_id)
character_update = %{
alliance_id: nil,
alliance_name: nil,
alliance_ticker: nil
}
{:ok, _character} =
Character.update_alliance(character, character_update)
WandererApp.Character.update_character(character_id, character_update)
@pubsub_client.broadcast(
WandererApp.PubSub,
"character:#{character_id}:alliance",
{:character_alliance, {character_id, character_update}}
)
state
_ ->
update_alliance(state, alliance_id)
end
end
defp maybe_update_wallet(
%{character_id: character_id} =
state,

View File

@@ -12,6 +12,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
opts: map
}
@check_start_queue_interval :timer.seconds(1)
@garbage_collection_interval :timer.minutes(15)
@untrack_characters_interval :timer.minutes(1)
@inactive_character_timeout :timer.minutes(10)
@@ -23,6 +24,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
def new(args), do: __struct__(args)
def init(args) do
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
@@ -46,25 +48,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
def start_tracking(state, character_id, opts) do
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
false <- Enum.member?(characters, character_id) do
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
if not WandererApp.Cache.has_key?("#{character_id}:track_requested") do
WandererApp.Cache.insert(
"#{character_id}:track_requested",
true
)
tracked_characters = [character_id | characters] |> Enum.uniq()
WandererApp.Cache.insert("tracked_characters", tracked_characters)
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
character_id,
%{opts: opts}
])
WandererApp.Cache.insert_or_update(
"track_characters_queue",
[character_id],
fn existing ->
[character_id | existing] |> Enum.uniq()
end
)
end
state
@@ -178,6 +174,21 @@ defmodule WandererApp.Character.TrackerManager.Impl do
end
end
def handle_info(
:check_start_queue,
state
) do
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
{:ok, track_characters_queue} = WandererApp.Cache.lookup("track_characters_queue", [])
track_characters_queue
|> Enum.each(fn character_id ->
track_character(character_id, %{})
end)
state
end
def handle_info(
:garbage_collect,
state
@@ -294,8 +305,56 @@ defmodule WandererApp.Character.TrackerManager.Impl do
state
end
def handle_info(_event, state),
do: state
def track_character(character_id, opts) do
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
false <- Enum.member?(characters, character_id) do
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
WandererApp.Cache.insert_or_update(
"tracked_characters",
[character_id],
fn existing ->
[character_id | existing] |> Enum.uniq()
end
)
WandererApp.Cache.insert_or_update(
"track_characters_queue",
[],
fn existing ->
existing
|> Enum.reject(fn c_id -> c_id == character_id end)
end
)
WandererApp.Cache.delete("#{character_id}:track_requested")
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
character_id,
%{opts: opts}
])
else
_ ->
WandererApp.Cache.insert_or_update(
"track_characters_queue",
[],
fn existing ->
existing
|> Enum.reject(fn c_id -> c_id == character_id end)
end
)
WandererApp.Cache.delete("#{character_id}:track_requested")
end
end
def character_is_present(map_id, character_id) do
{:ok, presence_character_ids} =

View File

@@ -23,7 +23,7 @@ defmodule WandererApp.Character.TrackerPool do
@check_ship_errors_interval :timer.minutes(1)
@check_location_errors_interval :timer.minutes(1)
@update_ship_interval :timer.seconds(2)
@update_info_interval :timer.minutes(1)
@update_info_interval :timer.minutes(2)
@update_wallet_interval :timer.minutes(1)
@logger Application.compile_env(:wanderer_app, :logger)
@@ -124,7 +124,7 @@ defmodule WandererApp.Character.TrackerPool do
Process.send_after(self(), :check_online_errors, :timer.seconds(60))
Process.send_after(self(), :check_ship_errors, :timer.seconds(90))
Process.send_after(self(), :check_location_errors, :timer.seconds(120))
Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
# Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
Process.send_after(self(), :update_location, 300)
Process.send_after(self(), :update_ship, 500)
Process.send_after(self(), :update_info, 1500)

View File

@@ -20,7 +20,7 @@ defmodule WandererApp.Character.TrackingUtils do
)
when not is_nil(caller_pid) do
with {:ok, character} <-
WandererApp.Character.get_by_eve_id(character_eve_id),
WandererApp.Character.get_by_eve_id("#{character_eve_id}"),
{:ok, %{tracked: is_tracked}} <-
do_update_character_tracking(character, map_id, track, caller_pid) do
# Determine which event to send based on tracking mode and previous state
@@ -55,15 +55,19 @@ defmodule WandererApp.Character.TrackingUtils do
Builds tracking data for all characters with access to a map.
"""
def build_tracking_data(map_id, current_user_id) do
with {:ok, map} <- WandererApp.MapRepo.get(map_id, [:acls]),
{:ok, character_settings} <-
WandererApp.Character.Activity.get_map_character_settings(map_id),
with {:ok, map} <-
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
),
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
{:ok, %{characters: characters_with_access}} <-
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
WandererApp.Maps.load_characters(map, current_user_id) do
# Map characters to tracking data
{:ok, characters_data} =
build_character_tracking_data(characters_with_access, character_settings)
build_character_tracking_data(characters_with_access)
{:ok, main_character} =
get_main_character(user_settings, characters_with_access, characters_with_access)
@@ -98,21 +102,19 @@ defmodule WandererApp.Character.TrackingUtils do
end
# Helper to build tracking data for each character
defp build_character_tracking_data(characters, character_settings) do
defp build_character_tracking_data(characters) do
{:ok,
Enum.map(characters, fn char ->
setting = Enum.find(character_settings, &(&1.character_id == char.id))
%{
character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
tracked: (setting && setting.tracked) || false
tracked: char.tracked
}
end)}
end
# Private implementation of update character tracking
defp do_update_character_tracking(character, map_id, track, caller_pid) do
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id)
WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|> case do
# Untracking flow
{:ok, %{tracked: true} = existing_settings} ->

View File

@@ -11,49 +11,50 @@ defmodule WandererApp.Env do
def vsn(), do: Application.spec(@app)[:vsn]
def git_sha(), do: get_key(:git_sha, "<GIT_SHA>")
def base_url, do: get_key(:web_app_url, "<BASE_URL>")
def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
def invites, do: get_key(:invites, false)
def base_url(), do: get_key(:web_app_url, "<BASE_URL>")
def base_metrics_only(), do: get_key(:base_metrics_only, false)
def custom_route_base_url(), do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
def invites(), do: get_key(:invites, false)
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
def websocket_events_enabled?, do: get_key(:websocket_events_enabled, false)
def public_api_disabled?, do: get_key(:public_api_disabled, false)
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
def websocket_events_enabled?(), do: get_key(:websocket_events_enabled, false)
def public_api_disabled?(), do: get_key(:public_api_disabled, false)
@decorate cacheable(
cache: WandererApp.Cache,
key: "active_tracking_pool"
)
def active_tracking_pool, do: get_key(:active_tracking_pool, "default")
def active_tracking_pool(), do: get_key(:active_tracking_pool, "default")
@decorate cacheable(
cache: WandererApp.Cache,
key: "tracking_pool_max_size"
)
def tracking_pool_max_size, do: get_key(:tracking_pool_max_size, 300)
def character_tracking_pause_disabled?, do: get_key(:character_tracking_pause_disabled, true)
def character_api_disabled?, do: get_key(:character_api_disabled, false)
def wanderer_kills_service_enabled?, do: get_key(:wanderer_kills_service_enabled, false)
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
def admins, do: get_key(:admins, [])
def admin_username, do: get_key(:admin_username)
def admin_password, do: get_key(:admin_password)
def corp_wallet, do: get_key(:corp_wallet, "")
def corp_wallet_eve_id, do: get_key(:corp_wallet_eve_id, "-1")
def corp_eve_id, do: get_key(:corp_id, -1)
def subscription_settings, do: get_key(:subscription_settings)
def tracking_pool_max_size(), do: get_key(:tracking_pool_max_size, 300)
def character_tracking_pause_disabled?(), do: get_key(:character_tracking_pause_disabled, true)
def character_api_disabled?(), do: get_key(:character_api_disabled, false)
def wanderer_kills_service_enabled?(), do: get_key(:wanderer_kills_service_enabled, false)
def wallet_tracking_enabled?(), do: get_key(:wallet_tracking_enabled, false)
def admins(), do: get_key(:admins, [])
def admin_username(), do: get_key(:admin_username)
def admin_password(), do: get_key(:admin_password)
def corp_wallet(), do: get_key(:corp_wallet, "")
def corp_wallet_eve_id(), do: get_key(:corp_wallet_eve_id, "-1")
def corp_eve_id(), do: get_key(:corp_id, -1)
def subscription_settings(), do: get_key(:subscription_settings)
@decorate cacheable(
cache: WandererApp.Cache,
key: "restrict_maps_creation"
)
def restrict_maps_creation?, do: get_key(:restrict_maps_creation, false)
def restrict_maps_creation?(), do: get_key(:restrict_maps_creation, false)
def sse_enabled? do
def sse_enabled?() do
Application.get_env(@app, :sse, [])
|> Keyword.get(:enabled, false)
end
def webhooks_enabled? do
def webhooks_enabled?() do
Application.get_env(@app, :external_events, [])
|> Keyword.get(:webhooks_enabled, false)
end
@@ -62,19 +63,19 @@ defmodule WandererApp.Env do
cache: WandererApp.Cache,
key: "map-connection-auto-expire-hours"
)
def map_connection_auto_expire_hours, do: get_key(:map_connection_auto_expire_hours)
def map_connection_auto_expire_hours(), do: get_key(:map_connection_auto_expire_hours)
@decorate cacheable(
cache: WandererApp.Cache,
key: "map-connection-auto-eol-hours"
)
def map_connection_auto_eol_hours, do: get_key(:map_connection_auto_eol_hours)
def map_connection_auto_eol_hours(), do: get_key(:map_connection_auto_eol_hours)
@decorate cacheable(
cache: WandererApp.Cache,
key: "map-connection-eol-expire-timeout-mins"
)
def map_connection_eol_expire_timeout_mins,
def map_connection_eol_expire_timeout_mins(),
do: get_key(:map_connection_eol_expire_timeout_mins)
def get_key(key, default \\ nil), do: Application.get_env(@app, key, default)
@@ -83,7 +84,7 @@ defmodule WandererApp.Env do
A single map containing environment variables
made available to react
"""
def to_client_env do
def to_client_env() do
%{detailedKillsDisabled: not wanderer_kills_service_enabled?()}
end
end

View File

@@ -287,8 +287,8 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
def get_alliance_info(eve_id, opts \\ []) do
case _get_alliance_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
case get_alliance_info(eve_id, "", opts) do
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
@@ -309,8 +309,8 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
def get_corporation_info(eve_id, opts \\ []) do
case _get_corporation_info(eve_id, "", opts) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
case get_corporation_info(eve_id, "", opts) do
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
@@ -327,7 +327,7 @@ defmodule WandererApp.Esi.ApiClient do
opts,
@cache_opts
) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
error -> error
end
@@ -434,7 +434,7 @@ defmodule WandererApp.Esi.ApiClient do
defp get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
defp _get_alliance_info(alliance_eve_id, info_path, opts),
defp get_alliance_info(alliance_eve_id, info_path, opts),
do:
get(
"/alliances/#{alliance_eve_id}/#{info_path}",
@@ -442,7 +442,7 @@ defmodule WandererApp.Esi.ApiClient do
@cache_opts
)
defp _get_corporation_info(corporation_eve_id, info_path, opts),
defp get_corporation_info(corporation_eve_id, info_path, opts),
do:
get(
"/corporations/#{corporation_eve_id}/#{info_path}",
@@ -830,7 +830,8 @@ defmodule WandererApp.Esi.ApiClient do
expires_at,
scopes
) do
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at, :second)
expires_at_datetime = DateTime.from_unix!(expires_at)
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
Logger.warning("TOKEN_REFRESH_FAILED: Invalid grant error during token refresh",
character_id: character_id,
@@ -857,7 +858,8 @@ defmodule WandererApp.Esi.ApiClient do
expires_at,
scopes
) do
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at, :second)
expires_at_datetime = DateTime.from_unix!(expires_at)
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
Logger.warning("TOKEN_REFRESH_FAILED: Connection refused during token refresh",
character_id: character_id,

View File

@@ -51,7 +51,7 @@ defmodule WandererApp.ExternalEvents.Event do
def new(map_id, event_type, payload) when is_binary(map_id) and is_map(payload) do
if valid_event_type?(event_type) do
%__MODULE__{
id: Ulid.generate(System.system_time(:millisecond)),
id: Ecto.ULID.generate(System.system_time(:millisecond)),
map_id: map_id,
type: event_type,
payload: payload,
@@ -97,7 +97,7 @@ defmodule WandererApp.ExternalEvents.Event do
:locked,
# ADD
:temporary_name,
# ADD
# ADD
:labels,
# ADD
:description,

View File

@@ -448,7 +448,7 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
"connected" ->
%{
"type" => "connection_status",
"id" => event["id"] || Ulid.generate(),
"id" => event["id"] || Ecto.ULID.generate(),
"attributes" => %{
"status" => "connected",
"server_time" => payload["server_time"],
@@ -465,7 +465,7 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
# Use existing payload structure but wrap it in JSON:API format
%{
"type" => "events",
"id" => event["id"] || Ulid.generate(),
"id" => event["id"] || Ecto.ULID.generate(),
"attributes" => payload,
"relationships" => %{
"map" => %{

View File

@@ -248,6 +248,6 @@ defmodule WandererApp.ExternalEvents.MapEventRelay do
defp datetime_to_ulid(datetime) do
timestamp = DateTime.to_unix(datetime, :millisecond)
# Create a ULID with the timestamp (rest will be zeros for comparison)
Ulid.generate(timestamp)
Ecto.ULID.generate(timestamp)
end
end

View File

@@ -1,31 +1,28 @@
defmodule WandererApp.Map.Audit do
@moduledoc """
Manager map subscription plans
This module now delegates to SecurityAudit for consistency.
It maintains backward compatibility while using the centralized audit system.
"""
require Ash.Query
require Logger
alias WandererApp.SecurityAudit
@week_seconds :timer.hours(24 * 7)
@month_seconds @week_seconds * 4
@audit_expired_seconds @month_seconds * 3
def track_map_subscription_event(event_type, metadata) do
case event_type do
"subscription.created" ->
track_map_event(event_type, metadata)
mapped_type =
case event_type do
"subscription.created" -> :subscription_created
"subscription.updated" -> :subscription_updated
"subscription.deleted" -> :subscription_deleted
_ -> :subscription_unknown
end
"subscription.updated" ->
track_map_event(event_type, metadata)
"subscription.deleted" ->
track_map_event(event_type, metadata)
_ ->
{:ok, nil}
end
track_map_event(mapped_type, metadata)
end
def archive() do
@@ -39,192 +36,14 @@ defmodule WandererApp.Map.Audit do
:ok
end
def get_activity_query(map_id, period, activity) do
{from, to} = period |> get_period()
defdelegate get_map_activity_query(map_id, period, activity),
to: WandererApp.SecurityAudit
query =
WandererApp.Api.UserActivity
|> Ash.Query.filter(
and: [
[entity_id: map_id],
[inserted_at: [greater_than_or_equal: from]],
[inserted_at: [less_than_or_equal: to]]
]
)
defdelegate track_acl_event(event_type, metadata),
to: WandererApp.SecurityAudit
query =
activity
|> case do
"all" ->
query
activity ->
query
|> Ash.Query.filter(event_type: activity)
end
query
|> Ash.Query.sort(inserted_at: :desc)
end
@doc """
Get combined activity including security events for a map.
"""
def get_combined_activity_query(map_id, period, activity) do
{from, to} = period |> get_period()
# Get regular map activity
map_query = get_activity_query(map_id, period, activity)
# Get security events related to this map
security_query =
WandererApp.Api.UserActivity
|> Ash.Query.filter(entity_type: :security_event)
|> Ash.Query.filter(inserted_at: [greater_than_or_equal: from])
|> Ash.Query.filter(inserted_at: [less_than_or_equal: to])
|> Ash.Query.sort(inserted_at: :desc)
# Execute both queries and combine results
case {Ash.read(map_query), Ash.read(security_query)} do
{{:ok, map_activities}, {:ok, security_activities}} ->
# Combine and sort by timestamp
combined =
(map_activities ++ security_activities)
|> Enum.sort_by(& &1.inserted_at, {:desc, DateTime})
{:ok, combined}
{{:error, _} = error, _} ->
error
{_, {:error, _} = error} ->
error
end
end
@doc """
Get security events for a specific map.
"""
def get_security_events_for_map(map_id, period \\ "1D") do
{from, to} = period |> get_period()
# Get security events that might be related to this map
# This could include data access events, permission denied events, etc.
SecurityAudit.get_events_in_range(from, to)
|> Enum.filter(fn event ->
case Jason.decode(event.event_data || "{}") do
{:ok, data} ->
# Check if the event data contains references to this map
data["resource_id"] == map_id ||
data["entity_id"] == map_id ||
data["map_id"] == map_id
_ ->
false
end
end)
end
def track_acl_event(
event_type,
%{user_id: user_id, acl_id: acl_id} = metadata
)
when not is_nil(user_id) and not is_nil(acl_id),
do:
WandererApp.Api.UserActivity.new(%{
user_id: user_id,
entity_type: :access_list,
entity_id: acl_id,
event_type: event_type,
event_data: metadata |> Map.drop([:user_id, :acl_id]) |> Jason.encode!()
})
def track_acl_event(_event_type, _metadata), do: {:ok, nil}
def track_map_event(
event_type,
%{character_id: character_id, user_id: user_id, map_id: map_id} = metadata
)
when not is_nil(character_id) and not is_nil(user_id) and not is_nil(map_id) do
# Log regular map activity
result =
WandererApp.Api.UserActivity.new(%{
character_id: character_id,
user_id: user_id,
entity_type: :map,
entity_id: map_id,
event_type: event_type,
event_data: metadata |> Map.drop([:character_id, :user_id, :map_id]) |> Jason.encode!()
})
# Also log security-relevant map events
if security_relevant_event?(event_type) do
SecurityAudit.log_data_access(
"map",
map_id,
user_id,
event_type,
metadata
)
end
result
end
def track_map_event(_event_type, _metadata), do: {:ok, nil}
defp get_period("1H") do
now = DateTime.utc_now()
start_date = now |> DateTime.add(-1 * 3600, :second)
{start_date, now}
end
defp get_period("1D") do
now = DateTime.utc_now()
start_date = now |> DateTime.add(-24 * 3600, :second)
{start_date, now}
end
defp get_period("1W") do
now = DateTime.utc_now()
start_date = now |> DateTime.add(-24 * 3600 * 7, :second)
{start_date, now}
end
defp get_period("1M") do
now = DateTime.utc_now()
start_date = now |> DateTime.add(-24 * 3600 * 31, :second)
{start_date, now}
end
defp get_period("2M") do
now = DateTime.utc_now()
start_date = now |> DateTime.add(-24 * 3600 * 31 * 2, :second)
{start_date, now}
end
defp get_period("3M") do
now = DateTime.utc_now()
start_date = now |> DateTime.add(-24 * 3600 * 31 * 3, :second)
{start_date, now}
end
defp get_period(_), do: get_period("1H")
defdelegate track_map_event(event_type, metadata),
to: WandererApp.SecurityAudit
defp get_expired_at(), do: DateTime.utc_now() |> DateTime.add(-@audit_expired_seconds, :second)
defp security_relevant_event?(event_type) do
# Define which map events should also be logged as security events
event_type in [
:map_acl_added,
:map_acl_removed,
:map_acl_updated,
:map_acl_member_added,
:map_acl_member_removed,
:map_acl_member_updated,
:map_removed,
:character_added,
:character_removed
]
end
end

View File

@@ -8,6 +8,19 @@ defmodule WandererApp.Map.Manager do
require Logger
alias WandererApp.Map.Server
alias WandererApp.Map.ServerSupervisor
alias WandererApp.Api.MapSystemSignature
@maps_start_per_second 10
@maps_start_interval 1000
@maps_queue :maps_queue
@garbage_collection_interval :timer.hours(1)
@check_maps_queue_interval :timer.seconds(1)
@signatures_cleanup_interval :timer.minutes(30)
@delete_after_minutes 30
@pings_cleanup_interval :timer.minutes(10)
@pings_expire_minutes 60
# Test-aware async task runner
defp safe_async_task(fun) do
@@ -25,20 +38,6 @@ defmodule WandererApp.Map.Manager do
end
end
alias WandererApp.Map.ServerSupervisor
alias WandererApp.Api.MapSystemSignature
@maps_start_per_second 5
@maps_start_interval 1000
@maps_queue :maps_queue
@garbage_collection_interval :timer.hours(1)
@check_maps_queue_interval :timer.seconds(1)
@signatures_cleanup_interval :timer.minutes(30)
@delete_after_minutes 30
@pings_cleanup_interval :timer.minutes(10)
@pings_expire_minutes 60
def start_map(map_id) when is_binary(map_id),
do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
@@ -247,22 +246,29 @@ defmodule WandererApp.Map.Manager do
Logger.debug(fn -> "All maps started" end)
else
# In production, run async as normal
tasks =
for chunk <- chunks do
task =
Task.async(fn ->
chunk
|> Enum.map(&start_map_server/1)
end)
chunks
|> Task.async_stream(
fn chunk ->
chunk
|> Enum.map(&start_map_server/1)
:timer.sleep(@maps_start_interval)
end,
max_concurrency: System.schedulers_online(),
on_timeout: :kill_task,
timeout: :timer.seconds(60)
)
|> Enum.each(fn result ->
case result do
{:ok, _} ->
:ok
task
_ ->
:ok
end
end)
Logger.debug(fn -> "Waiting for maps to start" end)
Task.await_many(tasks)
Logger.debug(fn -> "All maps started" end)
Logger.info(fn -> "All maps started" end)
end
end

View File

@@ -88,11 +88,18 @@ defmodule WandererApp.Map.Server do
|> map_pid!
|> GenServer.cast({&Impl.remove_character/2, [character_id]})
def untrack_characters(map_id, character_ids) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.cast({&Impl.untrack_characters/2, [character_ids]})
def untrack_characters(map_id, character_ids) when is_binary(map_id) do
map_id
|> map_pid()
|> case do
pid when is_pid(pid) ->
GenServer.cast(pid, {&Impl.untrack_characters/2, [character_ids]})
_ ->
WandererApp.Cache.insert("map_#{map_id}:started", false)
:ok
end
end
def add_system(map_id, system_info, user_id, character_id) when is_binary(map_id),
do:

View File

@@ -16,7 +16,13 @@ defmodule WandererApp.Map.Operations.Signatures do
systems
|> Enum.flat_map(fn sys ->
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
sigs
# Add solar_system_id to each signature and remove system_id
Enum.map(sigs, fn sig ->
sig
|> Map.from_struct()
|> Map.put(:solar_system_id, sys.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
end)
else
err ->
Logger.error("[list_signatures] error: #{inspect(err)}")
@@ -32,28 +38,70 @@ defmodule WandererApp.Map.Operations.Signatures do
def create_signature(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
%{"solar_system_id" => _solar_system_id} = params
) do
attrs = Map.put(params, "character_eve_id", char_id)
%{"solar_system_id" => solar_system_id} = params
)
when is_integer(solar_system_id) do
# Convert solar_system_id to system_id for internal use
with {:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
attrs =
params
|> Map.put("character_eve_id", char_id)
|> Map.put("system_id", system.id)
|> Map.delete("solar_system_id")
case Server.update_signatures(map_id, %{
added_signatures: [attrs],
updated_signatures: [],
removed_signatures: [],
solar_system_id: params["solar_system_id"],
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
}) do
:ok ->
{:ok, attrs}
case Server.update_signatures(map_id, %{
added_signatures: [attrs],
updated_signatures: [],
removed_signatures: [],
solar_system_id: solar_system_id,
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: false
}) do
:ok ->
# Try to fetch the created signature to return with proper fields
with {:ok, sigs} <-
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
sig when not is_nil(sig) <- List.first(sigs) do
result =
sig
|> Map.from_struct()
|> Map.put(:solar_system_id, system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
err ->
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
{:ok, result}
else
_ ->
# Fallback: return attrs with solar_system_id added
attrs_result =
attrs
|> Map.put(:solar_system_id, solar_system_id)
|> Map.drop(["system_id"])
{:ok, attrs_result}
end
err ->
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
{:error, :unexpected_error}
end
else
_ ->
Logger.error(
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
)
{:error, :system_not_found}
end
end
def create_signature(
%{assigns: %{map_id: _map_id, owner_character_id: _char_id, owner_user_id: _user_id}} =
_conn,
%{"solar_system_id" => _invalid} = _params
),
do: {:error, :missing_params}
def create_signature(_conn, _params), do: {:error, :missing_params}
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
@@ -90,7 +138,18 @@ defmodule WandererApp.Map.Operations.Signatures do
delete_connection_with_sigs: false
})
{:ok, attrs}
# Fetch the updated signature to return with proper fields
with {:ok, updated_sig} <- MapSystemSignature.by_id(sig_id) do
result =
updated_sig
|> Map.from_struct()
|> Map.put(:solar_system_id, system.solar_system_id)
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
{:ok, result}
else
_ -> {:ok, attrs}
end
else
err ->
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")

View File

@@ -59,6 +59,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
map_update = %{acls: map.acls, scope: map.scope}
WandererApp.Map.update_map(map_id, map_update)
WandererApp.Cache.delete("map_characters-#{map_id}")
broadcast_acl_updates({:ok, result}, map_id)
@@ -66,7 +67,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
end
def handle_acl_updated(map_id, acl_id) do
{:ok, map} =
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
@@ -74,8 +75,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
]
)
if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
WandererApp.Map.update_map(map_id, %{acls: map.acls})
if acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
WandererApp.Map.update_map(map_id, %{acls: acls})
WandererApp.Cache.delete("map_characters-#{map_id}")
:ok =
acl_id
@@ -85,7 +87,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
end
def handle_acl_deleted(map_id, _acl_id) do
{:ok, map} =
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
@@ -93,7 +95,8 @@ defmodule WandererApp.Map.Server.AclsImpl do
]
)
WandererApp.Map.update_map(map_id, %{acls: map.acls})
WandererApp.Map.update_map(map_id, %{acls: acls})
WandererApp.Cache.delete("map_characters-#{map_id}")
character_ids =
map_id

View File

@@ -78,15 +78,12 @@ defmodule WandererApp.Map.Server.CharactersImpl do
characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
"map_#{map_id}:invalidate_character_ids",
[]
)
WandererApp.Cache.insert(
WandererApp.Cache.insert_or_update(
"map_#{map_id}:invalidate_character_ids",
(invalidate_character_ids ++ characters_to_remove) |> Enum.uniq()
characters_to_remove,
fn ids ->
(ids ++ characters_to_remove) |> Enum.uniq()
end
)
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
@@ -126,15 +123,18 @@ defmodule WandererApp.Map.Server.CharactersImpl do
def cleanup_characters(map_id, owner_id) do
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
WandererApp.Cache.get_and_remove(
"map_#{map_id}:invalidate_character_ids",
[]
)
acls =
map_id
|> WandererApp.Map.get_map!()
|> Map.get(:acls, [])
{:ok, %{acls: acls}} =
WandererApp.MapRepo.get(map_id,
acls: [
:owner_id,
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
]
)
invalidate_character_ids
|> Task.async_stream(
@@ -186,11 +186,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:error, reason} ->
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
end)
WandererApp.Cache.insert(
"map_#{map_id}:invalidate_character_ids",
[]
)
end
defp remove_and_untrack_characters(map_id, character_ids) do
@@ -373,6 +368,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:ok, character} =
WandererApp.Character.get_map_character(map_id, character_id, not_present: true)
WandererApp.Cache.delete("character:#{character.id}:tracking_paused")
add_character(%{map_id: map_id}, character, true)
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{

View File

@@ -527,33 +527,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
when not is_nil(from_solar_system_id) and not is_nil(to_solar_system_id) do
{:ok, known_jumps} =
WandererApp.Api.MapSolarSystemJumps.find(%{
before_system_id: from_solar_system_id,
current_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)
case scope do
:wormholes ->
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
known_jumps |> Enum.empty?()
:stargates ->
# For stargates, we need to check:
# 1. Both systems are in known space (HS, LS, NS)
# 2. There is a known jump between them
# 3. Neither system is prohibited
from_system_static_info.system_class in @known_space and
to_system_static_info.system_class in @known_space and
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
case scope do
:wormholes ->
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?())
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
known_jumps |> Enum.empty?()
:stargates ->
# For stargates, we need to check:
# 1. Both systems are in known space (HS, LS, NS)
# 2. There is a known jump between them
# 3. Neither system is prohibited
from_system_static_info.system_class in @known_space and
to_system_static_info.system_class in @known_space and
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?())
end
else
_ -> false
end
end
@@ -570,6 +567,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
end
end
defp find_solar_system_jump(from_solar_system_id, to_solar_system_id) do
case WandererApp.CachedInfo.get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
{:ok, jump} when not is_nil(jump) -> {:ok, [jump]}
_ -> {:ok, []}
end
end
defp get_system_static_info(solar_system_id) do
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
{:ok, system_static_info} when not is_nil(system_static_info) ->

View File

@@ -25,7 +25,7 @@ defmodule WandererApp.Map.Server.Impl do
]
@systems_cleanup_timeout :timer.minutes(30)
@characters_cleanup_timeout :timer.minutes(1)
@characters_cleanup_timeout :timer.minutes(5)
@connections_cleanup_timeout :timer.minutes(2)
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
@@ -100,7 +100,7 @@ defmodule WandererApp.Map.Server.Impl do
Process.send_after(self(), :update_presence, @update_presence_timeout)
Process.send_after(self(), :cleanup_connections, 5_000)
Process.send_after(self(), :cleanup_systems, 10_000)
Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
Process.send_after(self(), :backup_state, @backup_state_timeout)
WandererApp.Cache.insert("map_#{map_id}:started", true)
@@ -127,6 +127,7 @@ defmodule WandererApp.Map.Server.Impl do
Logger.debug(fn -> "Stopping map server for #{map_id}" end)
WandererApp.Cache.delete("map_#{map_id}:started")
WandererApp.Cache.delete("map_characters-#{map_id}")
:telemetry.execute([:wanderer_app, :map, :stopped], %{count: 1})
@@ -278,7 +279,7 @@ defmodule WandererApp.Map.Server.Impl do
end
def handle_event({:acl_deleted, %{acl_id: acl_id}}, %{map_id: map_id} = state) do
AclsImpl.handle_acl_updated(map_id, acl_id)
AclsImpl.handle_acl_deleted(map_id, acl_id)
state
end

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