mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-04-19 09:07:57 +00:00
Compare commits
242 Commits
migrations
...
tests-fixe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a8dc4dbe5 | ||
|
|
7eb6d093cf | ||
|
|
a23e544a9f | ||
|
|
845ea7a576 | ||
|
|
ae8fbf30e4 | ||
|
|
3de385c902 | ||
|
|
5f3d4dba37 | ||
|
|
8acc7ddc25 | ||
|
|
ed6d25f3ea | ||
|
|
ab07d1321d | ||
|
|
a81e61bd70 | ||
|
|
d2d33619c2 | ||
|
|
fa464110c6 | ||
|
|
a5fa60e699 | ||
|
|
6db994852f | ||
|
|
0a68676957 | ||
|
|
9b82dd8f43 | ||
|
|
aac2c33fd2 | ||
|
|
1665b65619 | ||
|
|
e1a946bb1d | ||
|
|
543ec7f071 | ||
|
|
bf40d2cb8d | ||
|
|
48ac40ea55 | ||
|
|
5a3f3c40fe | ||
|
|
d5bac311ff | ||
|
|
34a7c854ed | ||
|
|
ebb6090be9 | ||
|
|
7a4d31db60 | ||
|
|
2acf9ed5dc | ||
|
|
46df025200 | ||
|
|
43a363b5ab | ||
|
|
03688387d8 | ||
|
|
5060852918 | ||
|
|
57381b9782 | ||
|
|
6014c60e13 | ||
|
|
1b711d7b4b | ||
|
|
f761ba9746 | ||
|
|
20a795c5b5 | ||
|
|
0c80894c65 | ||
|
|
21844f0550 | ||
|
|
f7716ca45a | ||
|
|
de74714c77 | ||
|
|
4dfa83bd30 | ||
|
|
cb4dba8dc2 | ||
|
|
1d75b8f063 | ||
|
|
2a42c4e6df | ||
|
|
0ee6160bcd | ||
|
|
5826d2492b | ||
|
|
a643e20247 | ||
|
|
66dc680281 | ||
|
|
5e0965ead4 | ||
|
|
46f46c745e | ||
|
|
00bf620e35 | ||
|
|
46eef60d86 | ||
|
|
4c39c6fb39 | ||
|
|
fe836442ab | ||
|
|
9514806dbb | ||
|
|
4e6423ebc8 | ||
|
|
a97e598299 | ||
|
|
9c26b50aac | ||
|
|
3f2ddf5cc4 | ||
|
|
233b2bd7a4 | ||
|
|
0d35268efc | ||
|
|
d169220eb2 | ||
|
|
182d5ec9fb | ||
|
|
32958253b7 | ||
|
|
c011d56ce7 | ||
|
|
73d1921d42 | ||
|
|
7bb810e1e6 | ||
|
|
c90ac7b1e3 | ||
|
|
005e0c2bc6 | ||
|
|
808acb540e | ||
|
|
06626f910b | ||
|
|
812582d955 | ||
|
|
f3077c0bf1 | ||
|
|
32c70cbbad | ||
|
|
8934935e10 | ||
|
|
20c8a53712 | ||
|
|
b22970fef3 | ||
|
|
cf72394ef9 | ||
|
|
e6dbba7283 | ||
|
|
843b3b86b2 | ||
|
|
bd865b9f64 | ||
|
|
ae91cd2f92 | ||
|
|
0be7a5f9d0 | ||
|
|
e15bfa426a | ||
|
|
4198e4b07a | ||
|
|
03ee08ff67 | ||
|
|
ac4dd4c28b | ||
|
|
308e81a464 | ||
|
|
6f4240d931 | ||
|
|
847b45a431 | ||
|
|
5ec97d74ca | ||
|
|
74359a5542 | ||
|
|
0020f46dd8 | ||
|
|
a6751b45c6 | ||
|
|
f48aeb5cec | ||
|
|
a5f25646c9 | ||
|
|
23cf1fd96f | ||
|
|
6f15521069 | ||
|
|
9d41e57c06 | ||
|
|
ea9a22df09 | ||
|
|
0d4fd6f214 | ||
|
|
87a6c20545 | ||
|
|
c375f4e4ce | ||
|
|
843a6d7320 | ||
|
|
98c54a3413 | ||
|
|
0439110938 | ||
|
|
8ce1e5fa3e | ||
|
|
ebaf6bcdc6 | ||
|
|
40d947bebc | ||
|
|
61d1c3848f | ||
|
|
e152ce179f | ||
|
|
7bbe387183 | ||
|
|
b1555ff03c | ||
|
|
e624499244 | ||
|
|
6a1976dec6 | ||
|
|
3db24c4344 | ||
|
|
883c09f255 | ||
|
|
ff24d80038 | ||
|
|
63cbc9c0b9 | ||
|
|
8056972a27 | ||
|
|
1759d46740 | ||
|
|
e4b7d2e45b | ||
|
|
41573cbee3 | ||
|
|
24ffc20bb8 | ||
|
|
e077849b66 | ||
|
|
375a9ef65b | ||
|
|
9bf90ab752 | ||
|
|
90c3481151 | ||
|
|
e36b08a7e5 | ||
|
|
e1f79170c3 | ||
|
|
68b5455e91 | ||
|
|
f28e75c7f4 | ||
|
|
6091adb28e | ||
|
|
d4657b335f | ||
|
|
7fee850902 | ||
|
|
648c168a66 | ||
|
|
f5c4b2c407 | ||
|
|
b592223d52 | ||
|
|
5cf118c6ee | ||
|
|
b25013c652 | ||
|
|
cf43861b11 | ||
|
|
b5fe8f8878 | ||
|
|
5e5068c7de | ||
|
|
624b51edfb | ||
|
|
a72f8e60c4 | ||
|
|
dec8ae50c9 | ||
|
|
0332d36a8e | ||
|
|
8444c7f82d | ||
|
|
ec3fc7447e | ||
|
|
20ec2800c9 | ||
|
|
6fbf43e860 | ||
|
|
697da38020 | ||
|
|
4bc65b43d2 | ||
|
|
910ec97fd1 | ||
|
|
40ed58ee8c | ||
|
|
c18d241c77 | ||
|
|
8b42908a5c | ||
|
|
6d32505a59 | ||
|
|
fe8a34c77d | ||
|
|
d12cafcca8 | ||
|
|
38a9c76ff0 | ||
|
|
d6c30b4a53 | ||
|
|
53a81daaf5 | ||
|
|
92081c99e3 | ||
|
|
d78020d2f5 | ||
|
|
fb1a9b440d | ||
|
|
0141ac46e3 | ||
|
|
d2bf6a8f86 | ||
|
|
1844e4c757 | ||
|
|
d407efe805 | ||
|
|
021e04d87a | ||
|
|
7844c9db34 | ||
|
|
355beb8394 | ||
|
|
d82eeba792 | ||
|
|
0396b05e58 | ||
|
|
9494a9eb37 | ||
|
|
8238f84ac7 | ||
|
|
1cf19b2a50 | ||
|
|
e8543fd2f8 | ||
|
|
c7f360e1fa | ||
|
|
a2b83f7f0c | ||
|
|
ae5689a403 | ||
|
|
c46af1d286 | ||
|
|
d17ba2168c | ||
|
|
80c14716eb | ||
|
|
8541fcd29b | ||
|
|
65d6acd7fb | ||
|
|
8b5f83d6b2 | ||
|
|
5e18891f4b | ||
|
|
74e0b85748 | ||
|
|
81d3495b65 | ||
|
|
d1959ca09f | ||
|
|
ec7a5ecf10 | ||
|
|
70b9ec99ba | ||
|
|
7147d79166 | ||
|
|
1dad9316bd | ||
|
|
872f7dcf48 | ||
|
|
02b450325e | ||
|
|
136bc4cbb9 | ||
|
|
dab49df9aa | ||
|
|
6286087f3e | ||
|
|
4ce7160f79 | ||
|
|
2913bf19b0 | ||
|
|
7bd6be6fd0 | ||
|
|
705daa286b | ||
|
|
614d06be66 | ||
|
|
dec3e9a7ce | ||
|
|
0017ac3373 | ||
|
|
ae34744578 | ||
|
|
76885058ef | ||
|
|
fccb007036 | ||
|
|
a9f8901bd5 | ||
|
|
8ae968b5be | ||
|
|
beffd45e4f | ||
|
|
4488d81e8d | ||
|
|
618cc8c5f1 | ||
|
|
3fb22a877e | ||
|
|
8759409b82 | ||
|
|
245647ae6a | ||
|
|
eb7d33ea07 | ||
|
|
3575b16def | ||
|
|
a6fb680be8 | ||
|
|
9e17df5544 | ||
|
|
683fde7be4 | ||
|
|
ee68ce92a2 | ||
|
|
8b4e38d795 | ||
|
|
4995202627 | ||
|
|
986b997a6a | ||
|
|
9a957af759 | ||
|
|
c5a0a96016 | ||
|
|
8715a6c0ac | ||
|
|
c9810095aa | ||
|
|
69eb888469 | ||
|
|
748347df9a | ||
|
|
aa4d49027c | ||
|
|
a9d7387e40 | ||
|
|
dc4d260c9b | ||
|
|
dc430491bf | ||
|
|
42cd261ea7 | ||
|
|
35af4fdc09 |
@@ -1,5 +1,7 @@
|
||||
export WEB_APP_URL="http://localhost:8000"
|
||||
export RELEASE_COOKIE="PDpbnyo6mEI_0T4ZsHH_ESmi1vT1toQ8PTc0vbfg5FIT4Ih-Lh98mw=="
|
||||
# Erlang node name for distributed Erlang (optional - defaults to wanderer@hostname)
|
||||
# export RELEASE_NODE="wanderer@localhost"
|
||||
export EVE_CLIENT_ID="<EVE_CLIENT_ID>"
|
||||
export EVE_CLIENT_SECRET="<EVE_CLIENT_SECRET>"
|
||||
export EVE_CLIENT_WITH_WALLET_ID="<EVE_CLIENT_WITH_WALLET_ID>"
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
name: Build Docker Image
|
||||
name: Build Develop
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
branches:
|
||||
- develop
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
@@ -18,12 +18,85 @@ permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: 🛠 Build
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.ref == 'refs/heads/develop' && github.event_name == 'push' }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
repository-projects: write
|
||||
strategy:
|
||||
matrix:
|
||||
otp: ["27"]
|
||||
elixir: ["1.17"]
|
||||
node-version: ["18.x"]
|
||||
outputs:
|
||||
commit_hash: ${{ steps.set-commit-develop.outputs.commit_hash }}
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Elixir
|
||||
uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: ${{matrix.otp}}
|
||||
elixir-version: ${{matrix.elixir}}
|
||||
# nix build would also work here because `todos` is the default package
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ssh-key: "${{ secrets.COMMIT_KEY }}"
|
||||
fetch-depth: 0
|
||||
- name: 😅 Cache deps
|
||||
id: cache-deps
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-elixir-deps
|
||||
with:
|
||||
path: |
|
||||
deps
|
||||
key: ${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-${{ hashFiles('**/mix.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-mix-${{ matrix.elixir }}-${{ matrix.otp }}-
|
||||
- name: 😅 Cache compiled build
|
||||
id: cache-build
|
||||
uses: actions/cache@v4
|
||||
env:
|
||||
cache-name: cache-compiled-build
|
||||
with:
|
||||
path: |
|
||||
_build
|
||||
key: ${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-${{ hashFiles( '**/lib/**/*.{ex,eex}', '**/config/*.exs', '**/mix.exs' ) }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-${{ hashFiles('**/mix.lock') }}-
|
||||
${{ runner.os }}-build-
|
||||
# Step: Download project dependencies. If unchanged, uses
|
||||
# the cached version.
|
||||
- name: 🌐 Install dependencies
|
||||
run: mix deps.get --only "prod"
|
||||
|
||||
# Step: Compile the project treating any warnings as errors.
|
||||
# Customize this step if a different behavior is desired.
|
||||
- name: 🛠 Compiles without warnings
|
||||
if: steps.cache-build.outputs.cache-hit != 'true'
|
||||
run: mix compile
|
||||
|
||||
- name: Set commit hash for develop
|
||||
id: set-commit-develop
|
||||
run: |
|
||||
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
docker:
|
||||
name: 🛠 Build Docker Images
|
||||
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
|
||||
@@ -37,6 +110,7 @@ jobs:
|
||||
matrix:
|
||||
platform:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -46,25 +120,9 @@ jobs:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ needs.build.outputs.commit_hash }}
|
||||
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
|
||||
@@ -113,24 +171,6 @@ jobs:
|
||||
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:
|
||||
@@ -161,9 +201,8 @@ jobs:
|
||||
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 }}
|
||||
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
|
||||
type=raw,value=develop-{{sha}},enable=${{ github.ref == 'refs/heads/develop' }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: /tmp/digests
|
||||
@@ -176,12 +215,20 @@ jobs:
|
||||
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
|
||||
|
||||
notify:
|
||||
name: 🏷 Notify about release
|
||||
name: 🏷 Notify about develop 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 }}
|
||||
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL_DEV }}
|
||||
content: |
|
||||
📣 New develop release available 🚀
|
||||
|
||||
**Commit**: `${{ github.sha }}`
|
||||
**Status**: Development/Testing Release
|
||||
|
||||
Docker image: `wandererltd/community-edition:develop`
|
||||
|
||||
⚠️ This is an unstable development release for testing purposes.
|
||||
56
.github/workflows/build.yml
vendored
56
.github/workflows/build.yml
vendored
@@ -4,7 +4,6 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
@@ -22,7 +21,7 @@ jobs:
|
||||
build:
|
||||
name: 🛠 Build
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && github.event_name == 'push' }}
|
||||
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
|
||||
permissions:
|
||||
checks: write
|
||||
contents: write
|
||||
@@ -37,7 +36,7 @@ jobs:
|
||||
elixir: ["1.17"]
|
||||
node-version: ["18.x"]
|
||||
outputs:
|
||||
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash || steps.set-commit-develop.outputs.commit_hash }}
|
||||
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash }}
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
@@ -91,7 +90,6 @@ jobs:
|
||||
|
||||
- name: Generate Changelog & Update Tag Version
|
||||
id: generate-changelog
|
||||
if: github.ref == 'refs/heads/main'
|
||||
run: |
|
||||
git config --global user.name 'CI'
|
||||
git config --global user.email 'ci@users.noreply.github.com'
|
||||
@@ -102,15 +100,16 @@ jobs:
|
||||
|
||||
- name: Set commit hash for develop
|
||||
id: set-commit-develop
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
run: |
|
||||
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
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,6 +136,17 @@ jobs:
|
||||
ref: ${{ needs.build.outputs.commit_hash }}
|
||||
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: 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
|
||||
@@ -185,6 +195,24 @@ jobs:
|
||||
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:
|
||||
@@ -215,8 +243,9 @@ jobs:
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
type=raw,value=develop,enable=${{ github.ref == 'refs/heads/develop' }}
|
||||
type=raw,value=develop-{{sha}},enable=${{ github.ref == 'refs/heads/develop' }}
|
||||
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
|
||||
@@ -259,3 +288,14 @@ jobs:
|
||||
## How to Promote?
|
||||
In order to promote this to prod, edit the draft and press **"Publish release"**.
|
||||
draft: true
|
||||
|
||||
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-arm.yml
vendored
187
.github/workflows/docker-arm.yml
vendored
@@ -1,187 +0,0 @@
|
||||
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 }}
|
||||
569
CHANGELOG.md
569
CHANGELOG.md
@@ -2,6 +2,575 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.85.3](https://github.com/wanderer-industries/wanderer/compare/v1.85.2...v1.85.3) (2025-11-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed connection time status issues. fixed character alliance update issues
|
||||
|
||||
## [v1.85.2](https://github.com/wanderer-industries/wanderer/compare/v1.85.1...v1.85.2) (2025-11-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: increased API pool limits
|
||||
|
||||
## [v1.85.1](https://github.com/wanderer-industries/wanderer/compare/v1.85.0...v1.85.1) (2025-11-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: increased API pool limits
|
||||
|
||||
## [v1.85.0](https://github.com/wanderer-industries/wanderer/compare/v1.84.37...v1.85.0) (2025-11-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* core: added support for new ship types
|
||||
|
||||
## [v1.84.37](https://github.com/wanderer-industries/wanderer/compare/v1.84.36...v1.84.37) (2025-11-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* auth: fixed character auth issues
|
||||
|
||||
## [v1.84.36](https://github.com/wanderer-industries/wanderer/compare/v1.84.35...v1.84.36) (2025-11-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* fixed duplicated map slugs
|
||||
|
||||
## [v1.84.35](https://github.com/wanderer-industries/wanderer/compare/v1.84.34...v1.84.35) (2025-11-19)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* structure search / paste issues
|
||||
|
||||
## [v1.84.34](https://github.com/wanderer-industries/wanderer/compare/v1.84.33...v1.84.34) (2025-11-18)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed character tracking issues
|
||||
|
||||
## [v1.84.33](https://github.com/wanderer-industries/wanderer/compare/v1.84.32...v1.84.33) (2025-11-18)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed character tracking issues
|
||||
|
||||
## [v1.84.32](https://github.com/wanderer-industries/wanderer/compare/v1.84.31...v1.84.32) (2025-11-18)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed character tracking issues
|
||||
|
||||
## [v1.84.31](https://github.com/wanderer-industries/wanderer/compare/v1.84.30...v1.84.31) (2025-11-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed connactions validation logic
|
||||
|
||||
## [v1.84.30](https://github.com/wanderer-industries/wanderer/compare/v1.84.29...v1.84.30) (2025-11-17)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.84.29](https://github.com/wanderer-industries/wanderer/compare/v1.84.28...v1.84.29) (2025-11-17)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.84.28](https://github.com/wanderer-industries/wanderer/compare/v1.84.27...v1.84.28) (2025-11-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed ACL updates
|
||||
|
||||
## [v1.84.27](https://github.com/wanderer-industries/wanderer/compare/v1.84.26...v1.84.27) (2025-11-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: supported characters_updates for external events
|
||||
|
||||
* core: improved character tracking
|
||||
|
||||
* core: improved character tracking
|
||||
|
||||
* core: improved character location tracking
|
||||
|
||||
## [v1.84.26](https://github.com/wanderer-industries/wanderer/compare/v1.84.25...v1.84.26) (2025-11-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: disable character tracker pausing
|
||||
|
||||
## [v1.84.25](https://github.com/wanderer-industries/wanderer/compare/v1.84.24...v1.84.25) (2025-11-16)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: used upsert for adding map systems
|
||||
|
||||
## [v1.84.24](https://github.com/wanderer-industries/wanderer/compare/v1.84.23...v1.84.24) (2025-11-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed problem related with error if settings was removed and mapper crashed. Fixed settings reset.
|
||||
|
||||
## [v1.84.23](https://github.com/wanderer-industries/wanderer/compare/v1.84.22...v1.84.23) (2025-11-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map pings cancel errors
|
||||
|
||||
## [v1.84.22](https://github.com/wanderer-industries/wanderer/compare/v1.84.21...v1.84.22) (2025-11-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map initialization
|
||||
|
||||
## [v1.84.21](https://github.com/wanderer-industries/wanderer/compare/v1.84.20...v1.84.21) (2025-11-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map characters adding
|
||||
|
||||
## [v1.84.20](https://github.com/wanderer-industries/wanderer/compare/v1.84.19...v1.84.20) (2025-11-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map start issues
|
||||
|
||||
## [v1.84.19](https://github.com/wanderer-industries/wanderer/compare/v1.84.18...v1.84.19) (2025-11-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed map start issues
|
||||
|
||||
## [v1.84.18](https://github.com/wanderer-industries/wanderer/compare/v1.84.17...v1.84.18) (2025-11-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added gracefull map poll recovery from saved state. added map slug unique checks
|
||||
|
||||
## [v1.84.17](https://github.com/wanderer-industries/wanderer/compare/v1.84.16...v1.84.17) (2025-11-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed activity tracking issues
|
||||
|
||||
## [v1.84.16](https://github.com/wanderer-industries/wanderer/compare/v1.84.15...v1.84.16) (2025-11-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: removed maps auto-start logic
|
||||
|
||||
## [v1.84.15](https://github.com/wanderer-industries/wanderer/compare/v1.84.14...v1.84.15) (2025-11-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed maps start/stop logic, added server downtime period support
|
||||
|
||||
## [v1.84.14](https://github.com/wanderer-industries/wanderer/compare/v1.84.13...v1.84.14) (2025-11-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed problem related with error if settings was removed and mapper crashed. Fixed settings reset.
|
||||
|
||||
## [v1.84.13](https://github.com/wanderer-industries/wanderer/compare/v1.84.12...v1.84.13) (2025-11-13)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.84.12](https://github.com/wanderer-industries/wanderer/compare/v1.84.11...v1.84.12) (2025-11-13)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.84.11](https://github.com/wanderer-industries/wanderer/compare/v1.84.10...v1.84.11) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* api and doc updates
|
||||
|
||||
## [v1.84.10](https://github.com/wanderer-industries/wanderer/compare/v1.84.9...v1.84.10) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed adding system on character dock
|
||||
|
||||
## [v1.84.9](https://github.com/wanderer-industries/wanderer/compare/v1.84.8...v1.84.9) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.84.8](https://github.com/wanderer-industries/wanderer/compare/v1.84.7...v1.84.8) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: added cleanup jobs for old system signatures & chain passages
|
||||
|
||||
## [v1.84.7](https://github.com/wanderer-industries/wanderer/compare/v1.84.6...v1.84.7) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* api and structure search fixes
|
||||
|
||||
## [v1.84.6](https://github.com/wanderer-industries/wanderer/compare/v1.84.5...v1.84.6) (2025-11-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Added map slug uniqness checking while using API
|
||||
|
||||
## [v1.84.5](https://github.com/wanderer-industries/wanderer/compare/v1.84.4...v1.84.5) (2025-11-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Added tracking for map & character event handling errors
|
||||
|
||||
## [v1.84.4](https://github.com/wanderer-industries/wanderer/compare/v1.84.3...v1.84.4) (2025-11-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed issue with updating system signatures
|
||||
|
||||
## [v1.84.3](https://github.com/wanderer-industries/wanderer/compare/v1.84.2...v1.84.3) (2025-11-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed linked signature time status update
|
||||
|
||||
## [v1.84.2](https://github.com/wanderer-industries/wanderer/compare/v1.84.1...v1.84.2) (2025-11-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* api: fixed api for get/update map systems
|
||||
|
||||
* add index for map/systems api
|
||||
|
||||
## [v1.84.1](https://github.com/wanderer-industries/wanderer/compare/v1.84.0...v1.84.1) (2025-11-01)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed connection time status update issue
|
||||
|
||||
## [v1.84.0](https://github.com/wanderer-industries/wanderer/compare/v1.83.4...v1.84.0) (2025-10-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: ESI API rate limits support
|
||||
|
||||
## [v1.83.4](https://github.com/wanderer-industries/wanderer/compare/v1.83.3...v1.83.4) (2025-10-29)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed page reloads
|
||||
|
||||
## [v1.83.3](https://github.com/wanderer-industries/wanderer/compare/v1.83.2...v1.83.3) (2025-10-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed old map API for systems & added small QOL improvements
|
||||
|
||||
## [v1.83.2](https://github.com/wanderer-industries/wanderer/compare/v1.83.1...v1.83.2) (2025-10-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Connections: Set new connection time status based on to/from system class
|
||||
|
||||
## [v1.83.1](https://github.com/wanderer-industries/wanderer/compare/v1.83.0...v1.83.1) (2025-10-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Kills: Fixed zkb links (added following '/').
|
||||
|
||||
## [v1.83.0](https://github.com/wanderer-industries/wanderer/compare/v1.82.3...v1.83.0) (2025-10-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Added map roles settings for copy/paste
|
||||
|
||||
* Core: Added map roles settings for copy/paste
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Copy-Paste restriction: support from FE side - fixed problem with incorrect disabling copy and paste buttons
|
||||
|
||||
* Map: Copy-Paste restriction: support from FE side - removed unnecessary constant
|
||||
|
||||
* Map: Copy-Paste restriction: support from FE side
|
||||
|
||||
* Core: Added Eve data downloaded files cleanup logic
|
||||
|
||||
## [v1.82.3](https://github.com/wanderer-industries/wanderer/compare/v1.82.2...v1.82.3) (2025-10-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix system static info - add source region for U319 from Null-sec
|
||||
|
||||
## [v1.82.2](https://github.com/wanderer-industries/wanderer/compare/v1.82.1...v1.82.2) (2025-10-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix system static info - for J012635 add D382; for J015092 - changed from J244, Z060 to N110, J244; for J000487 removed C008
|
||||
|
||||
## [v1.82.1](https://github.com/wanderer-industries/wanderer/compare/v1.82.0...v1.82.1) (2025-10-20)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed 'viewer' map access & characters tracking
|
||||
|
||||
## [v1.82.0](https://github.com/wanderer-industries/wanderer/compare/v1.81.15...v1.82.0) (2025-10-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: Added an ability to copy/paste selected map area between maps
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Add ability to copy and past systems (UI part)
|
||||
|
||||
## [v1.81.15](https://github.com/wanderer-industries/wanderer/compare/v1.81.14...v1.81.15) (2025-10-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed problem with commit - for correct restore deprecated data - change config key
|
||||
|
||||
## [v1.81.14](https://github.com/wanderer-industries/wanderer/compare/v1.81.13...v1.81.14) (2025-10-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed problem with commit - for correct restore deprecated data
|
||||
|
||||
## [v1.81.13](https://github.com/wanderer-industries/wanderer/compare/v1.81.12...v1.81.13) (2025-10-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed system select after tab switch
|
||||
|
||||
## [v1.81.12](https://github.com/wanderer-industries/wanderer/compare/v1.81.11...v1.81.12) (2025-10-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Core: Fixed map events buffering on tab switch
|
||||
|
||||
## [v1.81.11](https://github.com/wanderer-industries/wanderer/compare/v1.81.10...v1.81.11) (2025-10-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signatures: Fixed EOL indication for un-splashed and signatures list
|
||||
|
||||
## [v1.81.10](https://github.com/wanderer-industries/wanderer/compare/v1.81.9...v1.81.10) (2025-10-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signatures: Rework for lazy signatures deletion
|
||||
|
||||
## [v1.81.9](https://github.com/wanderer-industries/wanderer/compare/v1.81.8...v1.81.9) (2025-10-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Signatures: Fixed issue with wrong linked signatures deletions
|
||||
|
||||
## [v1.81.8](https://github.com/wanderer-industries/wanderer/compare/v1.81.7...v1.81.8) (2025-10-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix problem with restoring settings on widgets
|
||||
|
||||
## [v1.81.7](https://github.com/wanderer-industries/wanderer/compare/v1.81.6...v1.81.7) (2025-10-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed problem with rendering dropdown classes in signatures
|
||||
|
||||
## [v1.81.6](https://github.com/wanderer-industries/wanderer/compare/v1.81.5...v1.81.6) (2025-10-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed problem with a lot unnecessary loads zkb data on resize map
|
||||
|
||||
* Map: Added ability to see focused element
|
||||
|
||||
* Map: Removed unnecessary vertical scroller in Character Tracking dialog. Main always first in list of tracking characters, following next after main, another characters sorting by name
|
||||
|
||||
* Map: Added Search tool for systems what on the map
|
||||
|
||||
* Map: Added migration mechanism
|
||||
|
||||
* Map: Remove settings some default values if migration from very old settings system
|
||||
|
||||
* Map: MIGRATION: support from old store settings import
|
||||
|
||||
* Map: Add common migration mechanism. ATTENTION! This is a non-reversible stored map settings commit â it means we do not guarantee that settings will work if you check out back. Weâve tried to migrate old settings, but it may not work well or may NOT work at all.
|
||||
|
||||
* Map: Add front-end migrations for local store settings
|
||||
|
||||
## [v1.81.5](https://github.com/wanderer-industries/wanderer/compare/v1.81.4...v1.81.5) (2025-10-09)
|
||||
|
||||
|
||||
|
||||
3
Makefile
3
Makefile
@@ -30,7 +30,7 @@ format f:
|
||||
mix format
|
||||
|
||||
test t:
|
||||
mix test
|
||||
MIX_ENV=test mix test
|
||||
|
||||
coverage cover co:
|
||||
mix test --cover
|
||||
@@ -45,4 +45,3 @@ versions v:
|
||||
@cat .tool-versions
|
||||
@cat Aptfile
|
||||
@echo
|
||||
|
||||
|
||||
@@ -73,7 +73,9 @@ body > div:first-of-type {
|
||||
}
|
||||
|
||||
.maps_bg {
|
||||
background-image: url('../images/maps_bg.webp');
|
||||
/* OLD image */
|
||||
/* background-image: url('../images/maps_bg.webp'); */
|
||||
background-image: url('https://wanderer-industries.github.io/wanderer-assets/images/eve-screen-catalyst-expansion-bg.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
width: 100%;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useMapperHandlers } from './useMapperHandlers';
|
||||
import { MapRootContent } from '@/hooks/Mapper/components/mapRootContent/MapRootContent.tsx';
|
||||
import { MapRootProvider } from '@/hooks/Mapper/mapRootProvider';
|
||||
import './common-styles/main.scss';
|
||||
import { ToastProvider } from '@/hooks/Mapper/ToastProvider.tsx';
|
||||
|
||||
const ErrorFallback = () => {
|
||||
return <div className="!z-100 absolute w-screen h-screen bg-transparent"></div>;
|
||||
@@ -39,13 +40,15 @@ export default function MapRoot({ hooks }) {
|
||||
|
||||
return (
|
||||
<PrimeReactProvider>
|
||||
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
|
||||
<ReactFlowProvider>
|
||||
<MapRootContent />
|
||||
</ReactFlowProvider>
|
||||
</ErrorBoundary>
|
||||
</MapRootProvider>
|
||||
<ToastProvider>
|
||||
<MapRootProvider fwdRef={providerRef} outCommand={handleCommand}>
|
||||
<ErrorBoundary FallbackComponent={ErrorFallback} onError={logError}>
|
||||
<ReactFlowProvider>
|
||||
<MapRootContent />
|
||||
</ReactFlowProvider>
|
||||
</ErrorBoundary>
|
||||
</MapRootProvider>
|
||||
</ToastProvider>
|
||||
</PrimeReactProvider>
|
||||
);
|
||||
}
|
||||
|
||||
31
assets/js/hooks/Mapper/ToastProvider.tsx
Normal file
31
assets/js/hooks/Mapper/ToastProvider.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React, { createContext, useContext, useRef } from 'react';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import type { ToastMessage } from 'primereact/toast';
|
||||
|
||||
interface ToastContextValue {
|
||||
toastRef: React.RefObject<Toast>;
|
||||
show: (message: ToastMessage | ToastMessage[]) => void;
|
||||
}
|
||||
|
||||
const ToastContext = createContext<ToastContextValue | null>(null);
|
||||
|
||||
export const ToastProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const toastRef = useRef<Toast>(null);
|
||||
|
||||
const show = (message: ToastMessage | ToastMessage[]) => {
|
||||
toastRef.current?.show(message);
|
||||
};
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ toastRef, show }}>
|
||||
<Toast ref={toastRef} position="top-right" />
|
||||
{children}
|
||||
</ToastContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useToast = (): ToastContextValue => {
|
||||
const context = useContext(ToastContext);
|
||||
if (!context) throw new Error('useToast must be used within a ToastProvider');
|
||||
return context;
|
||||
};
|
||||
@@ -51,20 +51,8 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
['border-lime-600/70']: character.online,
|
||||
},
|
||||
)}
|
||||
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : character.name}
|
||||
title={character.name}
|
||||
>
|
||||
{character.tracking_paused && (
|
||||
<>
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute flex flex-col p-[2px] top-[0px] left-[0px] w-[35px] h-[35px]',
|
||||
'text-yellow-500 text-[9px] z-10 bg-gray-800/40',
|
||||
'pi',
|
||||
PrimeIcons.PAUSE,
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{mainCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
|
||||
@@ -118,7 +118,11 @@ export const useContextMenuSystemItems = ({
|
||||
});
|
||||
|
||||
if (isShowPingBtn) {
|
||||
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
|
||||
return (
|
||||
<WdMenuItem icon={iconClasses} className="!ml-[-2px]">
|
||||
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
|
||||
</WdMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -126,7 +130,7 @@ export const useContextMenuSystemItems = ({
|
||||
infoTitle="Locked. Ping can be set only for one system."
|
||||
infoClass="pi-lock text-stone-500 mr-[12px]"
|
||||
>
|
||||
<WdMenuItem disabled icon={iconClasses}>
|
||||
<WdMenuItem disabled icon={iconClasses} className="!ml-[-2px]">
|
||||
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
|
||||
@@ -2,25 +2,60 @@ import React, { RefObject, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface ContextMenuSystemMultipleProps {
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
onDeleteSystems(): void;
|
||||
onCopySystems(): void;
|
||||
}
|
||||
|
||||
export const ContextMenuSystemMultiple: React.FC<ContextMenuSystemMultipleProps> = ({
|
||||
contextMenuRef,
|
||||
onDeleteSystems,
|
||||
onCopySystems,
|
||||
}) => {
|
||||
const {
|
||||
data: { options, userPermissions },
|
||||
} = useMapRootState();
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const allowCopy = checkPermissions(userPermissions, options.allowed_copy_for);
|
||||
return [
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: PrimeIcons.TRASH,
|
||||
icon: clsx(PrimeIcons.TRASH, 'text-red-400'),
|
||||
command: onDeleteSystems,
|
||||
},
|
||||
{ separator: true },
|
||||
{
|
||||
label: 'Copy',
|
||||
icon: PrimeIcons.COPY,
|
||||
command: onCopySystems,
|
||||
disabled: !allowCopy,
|
||||
template: () => {
|
||||
if (allowCopy) {
|
||||
return <WdMenuItem icon="pi pi-copy">Copy</WdMenuItem>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Action is blocked because you don’t have permission to Copy."
|
||||
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
|
||||
tooltipWrapperClassName="flex"
|
||||
>
|
||||
<WdMenuItem disabled icon="pi pi-copy">
|
||||
Copy
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [onDeleteSystems]);
|
||||
}, [onCopySystems, onDeleteSystems, options, userPermissions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -6,27 +6,34 @@ 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';
|
||||
import { encodeJsonToUriBase64 } from '@/hooks/Mapper/utils';
|
||||
import { useToast } from '@/hooks/Mapper/ToastProvider.tsx';
|
||||
|
||||
export const useContextMenuSystemMultipleHandlers = () => {
|
||||
const {
|
||||
data: { pings },
|
||||
data: { pings, connections },
|
||||
} = useMapRootState();
|
||||
|
||||
const { show } = useToast();
|
||||
|
||||
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 refVars = useRef({ systems, ping, connections, deleteSystems });
|
||||
refVars.current = { systems, ping, connections, deleteSystems };
|
||||
|
||||
const handleSystemMultipleContext: NodeSelectionMouseHandler = (ev, systems_) => {
|
||||
const handleSystemMultipleContext = useCallback<NodeSelectionMouseHandler>((ev, systems_) => {
|
||||
setSystems(systems_);
|
||||
ev.preventDefault();
|
||||
ctxManager.next('ctxSysMult', contextMenuRef.current);
|
||||
contextMenuRef.current?.show(ev);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onDeleteSystems = useCallback(() => {
|
||||
const { systems, ping, deleteSystems } = refVars.current;
|
||||
|
||||
if (!systems) {
|
||||
return;
|
||||
}
|
||||
@@ -41,11 +48,34 @@ export const useContextMenuSystemMultipleHandlers = () => {
|
||||
}
|
||||
|
||||
deleteSystems(sysToDel);
|
||||
}, [deleteSystems, systems, ping]);
|
||||
}, []);
|
||||
|
||||
const onCopySystems = useCallback(async () => {
|
||||
const { systems, connections } = refVars.current;
|
||||
if (!systems) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionToCopy = connections.filter(
|
||||
c => systems.filter(s => [c.target, c.source].includes(s.id)).length == 2,
|
||||
);
|
||||
|
||||
await navigator.clipboard.writeText(
|
||||
encodeJsonToUriBase64({ systems: systems.map(x => x.data), connections: connectionToCopy }),
|
||||
);
|
||||
|
||||
show({
|
||||
severity: 'success',
|
||||
summary: 'Copied to clipboard',
|
||||
detail: `Successfully copied to clipboard - [${systems.length}] systems and [${connectionToCopy.length}] connections`,
|
||||
life: 3000,
|
||||
});
|
||||
}, [show]);
|
||||
|
||||
return {
|
||||
handleSystemMultipleContext,
|
||||
contextMenuRef,
|
||||
onDeleteSystems,
|
||||
onCopySystems,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { LayoutEventBlocker, TooltipPosition, WdImageSize, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import classes from './FastSystemActions.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import classes from './FastSystemActions.module.scss';
|
||||
|
||||
export interface FastSystemActionsProps {
|
||||
systemId: string;
|
||||
@@ -27,7 +27,7 @@ export const FastSystemActions = ({
|
||||
ref.current = { systemId, systemName, regionName, isWH };
|
||||
|
||||
const handleOpenZKB = useCallback(
|
||||
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}`, '_blank'),
|
||||
() => window.open(`https://zkillboard.com/system/${ref.current.systemId}/`, '_blank'),
|
||||
[],
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,4 @@ export type WaypointSetContextHandlerProps = {
|
||||
destination: string;
|
||||
};
|
||||
export type WaypointSetContextHandler = (props: WaypointSetContextHandlerProps) => void;
|
||||
export type NodeSelectionMouseHandler =
|
||||
| ((event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void)
|
||||
| undefined;
|
||||
export type NodeSelectionMouseHandler = (event: React.MouseEvent<Element, MouseEvent>, nodes: Node[]) => void;
|
||||
|
||||
@@ -120,7 +120,7 @@ const MapComp = ({
|
||||
useMapHandlers(refn, onSelectionChange);
|
||||
useUpdateNodes(nodes);
|
||||
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem });
|
||||
const { handleRootContext, ...rootCtxProps } = useContextMenuRootHandlers({ onAddSystem, onCommand });
|
||||
const { handleConnectionContext, ...connectionCtxProps } = useContextMenuConnectionHandlers();
|
||||
const { update } = useMapState();
|
||||
const { variant, gap, size, color } = useBackgroundVars(theme);
|
||||
|
||||
@@ -2,22 +2,70 @@ import React, { RefObject, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { PasteSystemsAndConnections } from '@/hooks/Mapper/components/map/components';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { checkPermissions } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { MenuItemWithInfo, WdMenuItem } from '@/hooks/Mapper/components/ui-kit';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface ContextMenuRootProps {
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
pasteSystemsAndConnections: PasteSystemsAndConnections | undefined;
|
||||
onAddSystem(): void;
|
||||
onPasteSystemsAnsConnections(): void;
|
||||
}
|
||||
|
||||
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({ contextMenuRef, onAddSystem }) => {
|
||||
export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
|
||||
contextMenuRef,
|
||||
onAddSystem,
|
||||
onPasteSystemsAnsConnections,
|
||||
pasteSystemsAndConnections,
|
||||
}) => {
|
||||
const {
|
||||
data: { options, userPermissions },
|
||||
} = useMapState();
|
||||
|
||||
const items: MenuItem[] = useMemo(() => {
|
||||
const allowPaste = checkPermissions(userPermissions, options.allowed_paste_for);
|
||||
|
||||
return [
|
||||
{
|
||||
label: 'Add System',
|
||||
icon: PrimeIcons.PLUS,
|
||||
command: onAddSystem,
|
||||
},
|
||||
...(pasteSystemsAndConnections != null
|
||||
? [
|
||||
{
|
||||
icon: 'pi pi-clipboard',
|
||||
disabled: !allowPaste,
|
||||
command: onPasteSystemsAnsConnections,
|
||||
template: () => {
|
||||
if (allowPaste) {
|
||||
return (
|
||||
<WdMenuItem icon="pi pi-clipboard">
|
||||
Paste
|
||||
</WdMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItemWithInfo
|
||||
infoTitle="Action is blocked because you don’t have permission to Paste."
|
||||
infoClass={clsx(PrimeIcons.QUESTION_CIRCLE, 'text-stone-500 mr-[12px]')}
|
||||
tooltipWrapperClassName="flex"
|
||||
>
|
||||
<WdMenuItem disabled icon="pi pi-clipboard">
|
||||
Paste
|
||||
</WdMenuItem>
|
||||
</MenuItemWithInfo>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
}, [onAddSystem]);
|
||||
}, [userPermissions, options, onAddSystem, pasteSystemsAndConnections, onPasteSystemsAnsConnections]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,36 +1,76 @@
|
||||
import { useReactFlow, XYPosition } from 'reactflow';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { OnMapAddSystemCallback } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import { recenterSystemsByBounds } from '@/hooks/Mapper/helpers/recenterSystems.ts';
|
||||
import { OutCommand, OutCommandHandler, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { decodeUriBase64ToJson } from '@/hooks/Mapper/utils';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { useReactFlow, XYPosition } from 'reactflow';
|
||||
|
||||
export type PasteSystemsAndConnections = {
|
||||
systems: SolarSystemRawType[];
|
||||
connections: SolarSystemConnection[];
|
||||
};
|
||||
|
||||
type UseContextMenuRootHandlers = {
|
||||
onAddSystem?: OnMapAddSystemCallback;
|
||||
onCommand?: OutCommandHandler;
|
||||
};
|
||||
|
||||
export const useContextMenuRootHandlers = ({ onAddSystem }: UseContextMenuRootHandlers = {}) => {
|
||||
export const useContextMenuRootHandlers = ({ onAddSystem, onCommand }: UseContextMenuRootHandlers = {}) => {
|
||||
const rf = useReactFlow();
|
||||
const contextMenuRef = useRef<ContextMenu | null>(null);
|
||||
const [position, setPosition] = useState<XYPosition | null>(null);
|
||||
const [pasteSystemsAndConnections, setPasteSystemsAndConnections] = useState<PasteSystemsAndConnections>();
|
||||
|
||||
const handleRootContext = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
const handleRootContext = async (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
setPosition(rf.project({ x: e.clientX, y: e.clientY }));
|
||||
e.preventDefault();
|
||||
ctxManager.next('ctxRoot', contextMenuRef.current);
|
||||
contextMenuRef.current?.show(e);
|
||||
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
const result = decodeUriBase64ToJson(text);
|
||||
setPasteSystemsAndConnections(result as PasteSystemsAndConnections);
|
||||
} catch (err) {
|
||||
setPasteSystemsAndConnections(undefined);
|
||||
// do nothing
|
||||
}
|
||||
};
|
||||
|
||||
const ref = useRef({ onAddSystem, position });
|
||||
ref.current = { onAddSystem, position };
|
||||
const ref = useRef({ onAddSystem, position, pasteSystemsAndConnections, onCommand });
|
||||
ref.current = { onAddSystem, position, pasteSystemsAndConnections, onCommand };
|
||||
|
||||
const onAddSystemCallback = useCallback(() => {
|
||||
ref.current.onAddSystem?.({ coordinates: position });
|
||||
}, [position]);
|
||||
|
||||
const onPasteSystemsAnsConnections = useCallback(async () => {
|
||||
const { pasteSystemsAndConnections, onCommand, position } = ref.current;
|
||||
if (!position || !onCommand || !pasteSystemsAndConnections) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { systems } = recenterSystemsByBounds(pasteSystemsAndConnections.systems);
|
||||
|
||||
await onCommand({
|
||||
type: OutCommand.manualPasteSystemsAndConnections,
|
||||
data: {
|
||||
systems: systems.map(({ position: srcPos, ...rest }) => ({
|
||||
position: { x: Math.round(srcPos.x + position.x), y: Math.round(srcPos.y + position.y) },
|
||||
...rest,
|
||||
})),
|
||||
connections: pasteSystemsAndConnections.connections,
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
|
||||
return {
|
||||
handleRootContext,
|
||||
|
||||
pasteSystemsAndConnections,
|
||||
contextMenuRef,
|
||||
onAddSystem: onAddSystemCallback,
|
||||
onPasteSystemsAnsConnections,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use "sass:color";
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@import '@/hooks/Mapper/components/map/styles/solar-system-node';
|
||||
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
|
||||
|
||||
@keyframes move-stripes {
|
||||
from {
|
||||
@@ -26,8 +26,8 @@
|
||||
background-color: var(--rf-node-bg-color, #202020) !important;
|
||||
color: var(--rf-text-color, #ffffff);
|
||||
|
||||
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
||||
border: 1px solid color.adjust($pastel-blue, $lightness: -10%);
|
||||
box-shadow: 0 0 5px rgba(v.$dark-bg, 0.5);
|
||||
border: 1px solid color.adjust(v.$pastel-blue, $lightness: -10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
@@ -99,7 +99,7 @@
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
border-color: v.$pastel-pink;
|
||||
box-shadow: 0 0 10px #9a1af1c2;
|
||||
}
|
||||
|
||||
@@ -113,11 +113,11 @@
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
|
||||
border-color: $neon-color-1;
|
||||
border-color: v.$neon-color-1;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
$neon-color-3 0px,
|
||||
$neon-color-3 8px,
|
||||
v.$neon-color-3 0px,
|
||||
v.$neon-color-3 8px,
|
||||
transparent 8px,
|
||||
transparent 21px
|
||||
);
|
||||
@@ -146,7 +146,7 @@
|
||||
border: 1px solid var(--eve-solar-system-status-color-lookingFor-dark15);
|
||||
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
border-color: v.$pastel-pink;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,13 +347,13 @@
|
||||
.Handle {
|
||||
min-width: initial;
|
||||
min-height: initial;
|
||||
border: 1px solid $pastel-blue;
|
||||
border: 1px solid v.$pastel-blue;
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
pointer-events: auto;
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
border-color: v.$pastel-pink;
|
||||
}
|
||||
|
||||
&.HandleTop {
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
|
||||
import classes from './UnsplashedSignature.module.scss';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { useMemo } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import classes from './UnsplashedSignature.module.scss';
|
||||
|
||||
interface UnsplashedSignatureProps {
|
||||
signature: SystemSignature;
|
||||
@@ -35,7 +36,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
|
||||
}, [customInfo]);
|
||||
|
||||
const isEOL = useMemo(() => {
|
||||
return customInfo?.isEOL;
|
||||
return customInfo?.time_status === TimeStatus._1h;
|
||||
}, [customInfo]);
|
||||
|
||||
const whClassStyle = useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { UserPermission, UserPermissions } from '@/hooks/Mapper/types';
|
||||
|
||||
export const checkPermissions = (permissions: Partial<UserPermissions>, targetPermission: UserPermission) => {
|
||||
return targetPermission != null && permissions[targetPermission];
|
||||
};
|
||||
@@ -4,3 +4,4 @@ export * from './getSystemClassStyles';
|
||||
export * from './getShapeClass';
|
||||
export * from './getBackgroundClass';
|
||||
export * from './prepareUnsplashedChunks';
|
||||
export * from './checkPermissions';
|
||||
|
||||
@@ -14,8 +14,27 @@ export const useCommandsCharacters = () => {
|
||||
const ref = useRef({ update });
|
||||
ref.current = { update };
|
||||
|
||||
const charactersUpdated = useCallback((characters: CommandCharactersUpdated) => {
|
||||
ref.current.update(() => ({ characters: characters.slice() }));
|
||||
const charactersUpdated = useCallback((updatedCharacters: CommandCharactersUpdated) => {
|
||||
ref.current.update(state => {
|
||||
const existing = state.characters ?? [];
|
||||
// Put updatedCharacters into a map keyed by ID
|
||||
const updatedMap = new Map(updatedCharacters.map(c => [c.eve_id, c]));
|
||||
|
||||
// 1. Update existing characters when possible
|
||||
const merged = existing.map(character => {
|
||||
const updated = updatedMap.get(character.eve_id);
|
||||
if (updated) {
|
||||
updatedMap.delete(character.eve_id); // Mark as processed
|
||||
return { ...character, ...updated };
|
||||
}
|
||||
return character;
|
||||
});
|
||||
|
||||
// 2. Any remaining items in updatedMap are NEW characters → add them
|
||||
const newCharacters = Array.from(updatedMap.values());
|
||||
|
||||
return { characters: [...merged, ...newCharacters] };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const characterAdded = useCallback((value: CommandCharacterAdded) => {
|
||||
|
||||
@@ -38,6 +38,8 @@ export const useMapInit = () => {
|
||||
user_characters,
|
||||
present_characters,
|
||||
hubs,
|
||||
options,
|
||||
user_permissions,
|
||||
}: CommandInit) => {
|
||||
const { update } = ref.current;
|
||||
|
||||
@@ -63,6 +65,14 @@ export const useMapInit = () => {
|
||||
updateData.hubs = hubs;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
updateData.options = options;
|
||||
}
|
||||
|
||||
if (options) {
|
||||
updateData.userPermissions = user_permissions;
|
||||
}
|
||||
|
||||
if (systems) {
|
||||
updateData.systems = systems;
|
||||
}
|
||||
|
||||
@@ -4,10 +4,13 @@ import { DEFAULT_WIDGETS } from '@/hooks/Mapper/components/mapInterface/constant
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const MapInterface = () => {
|
||||
// const [items, setItems] = useState<WindowProps[]>(restoreWindowsFromLS);
|
||||
const { windowsSettings, updateWidgetSettings } = useMapRootState();
|
||||
|
||||
const items = useMemo(() => {
|
||||
if (Object.keys(windowsSettings).length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return windowsSettings.windows
|
||||
.map(x => {
|
||||
const content = DEFAULT_WIDGETS.find(y => y.id === x.id)?.content;
|
||||
|
||||
@@ -9,11 +9,12 @@ import {
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CommandLinkSignatureToSystem, SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { SETTINGS_KEYS, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
import { useSystemSignaturesData } from '../../widgets/SystemSignatures/hooks/useSystemSignaturesData';
|
||||
|
||||
const K162_SIGNATURE_TYPE = WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME['K162'].shortName;
|
||||
|
||||
@@ -135,6 +136,11 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
[data, setVisible],
|
||||
);
|
||||
|
||||
const { signatures } = useSystemSignaturesData({
|
||||
systemId: `${data.solar_system_source}`,
|
||||
settings: LINK_SIGNTATURE_SETTINGS,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!targetSystemDynamicInfo) {
|
||||
handleHide();
|
||||
@@ -152,10 +158,12 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
>
|
||||
<SystemSignaturesContent
|
||||
systemId={`${data.solar_system_source}`}
|
||||
hideLinkedSignatures
|
||||
signatures={signatures}
|
||||
hasUnsupportedLanguage={false}
|
||||
settings={LINK_SIGNTATURE_SETTINGS}
|
||||
hideLinkedSignatures
|
||||
selectable
|
||||
onSelect={handleSelect}
|
||||
selectable={true}
|
||||
filterSignature={filterSignature}
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
|
||||
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SystemInfoContent } from './SystemInfoContent';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
|
||||
import { ANOIK_ICON, DOTLAN_ICON, ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
import { SystemInfoContent } from './SystemInfoContent';
|
||||
|
||||
export const SystemInfo = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
@@ -48,7 +48,7 @@ export const SystemInfo = () => {
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-1 items-center">
|
||||
<a href={`https://zkillboard.com/system/${systemId}`} rel="noreferrer" target="_blank">
|
||||
<a href={`https://zkillboard.com/system/${systemId}/`} rel="noreferrer" target="_blank">
|
||||
<img src={ZKB_ICON} width="14" height="14" className="external-icon" />
|
||||
</a>
|
||||
<a href={`http://anoik.is/systems/${solarSystemName}`} rel="noreferrer" target="_blank">
|
||||
|
||||
@@ -1,123 +1,16 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useSignatureUndo } from './hooks/useSignatureUndo';
|
||||
import { useSystemSignaturesData } from './hooks/useSystemSignaturesData';
|
||||
import { SystemSignaturesHeader } from './SystemSignatureHeader';
|
||||
import { SystemSignaturesContent } from './SystemSignaturesContent';
|
||||
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { SystemSignaturesHeader } from './SystemSignatureHeader';
|
||||
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
|
||||
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
|
||||
/**
|
||||
* Custom hook for managing pending signature deletions and undo countdown.
|
||||
*/
|
||||
function useSignatureUndo(
|
||||
systemId: string | undefined,
|
||||
settings: SignatureSettingsType,
|
||||
outCommand: OutCommandHandler,
|
||||
) {
|
||||
const [countdown, setCountdown] = useState<number>(0);
|
||||
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
|
||||
const [deletedSignatures, setDeletedSignatures] = useState<ExtendedSystemSignature[]>([]);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
|
||||
const addDeleted = useCallback((signatures: ExtendedSystemSignature[]) => {
|
||||
const newIds = signatures.map(sig => sig.eve_id);
|
||||
setPendingIds(prev => {
|
||||
const next = new Set(prev);
|
||||
newIds.forEach(id => next.add(id));
|
||||
return next;
|
||||
});
|
||||
setDeletedSignatures(prev => [...prev, ...signatures]);
|
||||
}, []);
|
||||
|
||||
// Clear deleted signatures when system changes
|
||||
useEffect(() => {
|
||||
if (systemId) {
|
||||
setDeletedSignatures([]);
|
||||
setPendingIds(new Set());
|
||||
setCountdown(0);
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [systemId]);
|
||||
|
||||
// kick off or clear countdown whenever pendingIds changes
|
||||
useEffect(() => {
|
||||
// clear any existing timer
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
|
||||
if (pendingIds.size === 0) {
|
||||
setCountdown(0);
|
||||
setDeletedSignatures([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// determine timeout from settings
|
||||
const timeoutMs = getDeletionTimeoutMs(settings);
|
||||
|
||||
// Ensure a minimum of 1 second for immediate deletion so the UI shows
|
||||
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
|
||||
|
||||
setCountdown(Math.ceil(effectiveTimeoutMs / 1000));
|
||||
|
||||
// start new interval
|
||||
intervalRef.current = window.setInterval(() => {
|
||||
setCountdown(prev => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(intervalRef.current!);
|
||||
intervalRef.current = null;
|
||||
setPendingIds(new Set());
|
||||
setDeletedSignatures([]);
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [pendingIds, settings]);
|
||||
|
||||
// undo handler
|
||||
const handleUndo = useCallback(async () => {
|
||||
if (!systemId || pendingIds.size === 0) return;
|
||||
await outCommand({
|
||||
type: OutCommand.undoDeleteSignatures,
|
||||
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
|
||||
});
|
||||
setPendingIds(new Set());
|
||||
setDeletedSignatures([]);
|
||||
setCountdown(0);
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, [systemId, pendingIds, outCommand]);
|
||||
|
||||
return {
|
||||
pendingIds,
|
||||
countdown,
|
||||
deletedSignatures,
|
||||
addDeleted,
|
||||
handleUndo,
|
||||
};
|
||||
}
|
||||
|
||||
export const SystemSignatures = () => {
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [sigCount, setSigCount] = useState(0);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
const {
|
||||
data: { selectedSystems },
|
||||
@@ -127,31 +20,6 @@ export const SystemSignatures = () => {
|
||||
|
||||
const [systemId] = selectedSystems;
|
||||
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
|
||||
const { pendingIds, countdown, deletedSignatures, addDeleted, handleUndo } = useSignatureUndo(
|
||||
systemId,
|
||||
settingsSignatures,
|
||||
outCommand,
|
||||
);
|
||||
|
||||
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
|
||||
if (pendingIds.size > 0 && countdown > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleUndo();
|
||||
}
|
||||
});
|
||||
|
||||
const handleCountChange = useCallback((count: number) => {
|
||||
setSigCount(count);
|
||||
}, []);
|
||||
|
||||
const handleSettingsSave = useCallback(
|
||||
(newSettings: SignatureSettingsType) => {
|
||||
settingsSignaturesUpdate(newSettings);
|
||||
setVisible(false);
|
||||
},
|
||||
[settingsSignaturesUpdate],
|
||||
);
|
||||
|
||||
const handleLazyDeleteToggle = useCallback(
|
||||
(value: boolean) => {
|
||||
@@ -163,7 +31,42 @@ export const SystemSignatures = () => {
|
||||
[settingsSignaturesUpdate],
|
||||
);
|
||||
|
||||
const openSettings = useCallback(() => setVisible(true), []);
|
||||
const {
|
||||
signatures,
|
||||
selectedSignatures,
|
||||
setSelectedSignatures,
|
||||
handleDeleteSelected,
|
||||
handleSelectAll,
|
||||
handlePaste,
|
||||
hasUnsupportedLanguage,
|
||||
} = useSystemSignaturesData({
|
||||
systemId,
|
||||
settings: settingsSignatures,
|
||||
onLazyDeleteChange: handleLazyDeleteToggle,
|
||||
});
|
||||
|
||||
const sigCount = useMemo(() => signatures.length, [signatures]);
|
||||
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
|
||||
|
||||
const { countdown, handleUndo } = useSignatureUndo(systemId, settingsSignatures, deletedSignatures, outCommand);
|
||||
|
||||
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
|
||||
if (deletedSignatures.length > 0 && countdown > 0) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleUndo();
|
||||
}
|
||||
});
|
||||
|
||||
const handleSettingsSave = useCallback(
|
||||
(newSettings: SignatureSettingsType) => {
|
||||
settingsSignaturesUpdate(newSettings);
|
||||
setShowSettings(false);
|
||||
},
|
||||
[settingsSignaturesUpdate],
|
||||
);
|
||||
|
||||
const openSettings = useCallback(() => setShowSettings(true), []);
|
||||
|
||||
return (
|
||||
<Widget
|
||||
@@ -171,7 +74,7 @@ export const SystemSignatures = () => {
|
||||
<SystemSignaturesHeader
|
||||
sigCount={sigCount}
|
||||
lazyDeleteValue={settingsSignatures[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
|
||||
pendingCount={pendingIds.size}
|
||||
pendingCount={deletedSignatures.length}
|
||||
undoCountdown={countdown}
|
||||
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||
onUndoClick={handleUndo}
|
||||
@@ -187,18 +90,21 @@ export const SystemSignatures = () => {
|
||||
) : (
|
||||
<SystemSignaturesContent
|
||||
systemId={systemId}
|
||||
signatures={signatures}
|
||||
selectedSignatures={selectedSignatures}
|
||||
onSelectSignatures={setSelectedSignatures}
|
||||
onDeleteSelected={handleDeleteSelected}
|
||||
onSelectAll={handleSelectAll}
|
||||
onPaste={handlePaste}
|
||||
hasUnsupportedLanguage={hasUnsupportedLanguage}
|
||||
settings={settingsSignatures}
|
||||
deletedSignatures={deletedSignatures}
|
||||
onLazyDeleteChange={handleLazyDeleteToggle}
|
||||
onCountChange={handleCountChange}
|
||||
onSignatureDeleted={addDeleted}
|
||||
/>
|
||||
)}
|
||||
|
||||
{visible && (
|
||||
{showSettings && (
|
||||
<SystemSignatureSettingsDialog
|
||||
settings={settingsSignatures}
|
||||
onCancel={() => setVisible(false)}
|
||||
onCancel={() => setShowSettings(false)}
|
||||
onSave={handleSettingsSave}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -33,34 +33,39 @@ 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';
|
||||
|
||||
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
|
||||
|
||||
interface SystemSignaturesContentProps {
|
||||
systemId: string;
|
||||
signatures: ExtendedSystemSignature[];
|
||||
selectedSignatures?: ExtendedSystemSignature[];
|
||||
onSelectSignatures?: (s: ExtendedSystemSignature[]) => void;
|
||||
onDeleteSelected?: () => Promise<void>;
|
||||
onSelectAll?: () => void;
|
||||
onPaste?: (clipboardString: string) => void;
|
||||
settings: SignatureSettingsType;
|
||||
hideLinkedSignatures?: boolean;
|
||||
hasUnsupportedLanguage?: boolean;
|
||||
selectable?: boolean;
|
||||
onSelect?: (signature: SystemSignature) => void;
|
||||
onLazyDeleteChange?: (value: boolean) => void;
|
||||
onCountChange?: (count: number) => void;
|
||||
filterSignature?: (signature: SystemSignature) => boolean;
|
||||
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
|
||||
deletedSignatures?: ExtendedSystemSignature[];
|
||||
}
|
||||
|
||||
export const SystemSignaturesContent = ({
|
||||
systemId,
|
||||
signatures,
|
||||
selectedSignatures,
|
||||
onSelectSignatures,
|
||||
onDeleteSelected,
|
||||
onSelectAll,
|
||||
onPaste,
|
||||
settings,
|
||||
hideLinkedSignatures,
|
||||
hasUnsupportedLanguage,
|
||||
selectable,
|
||||
onSelect,
|
||||
onLazyDeleteChange,
|
||||
onCountChange,
|
||||
filterSignature,
|
||||
onSignatureDeleted,
|
||||
deletedSignatures = [],
|
||||
}: SystemSignaturesContentProps) => {
|
||||
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
|
||||
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
|
||||
@@ -79,32 +84,18 @@ export const SystemSignaturesContent = ({
|
||||
|
||||
const { clipboardContent, setClipboardContent } = useClipboard();
|
||||
|
||||
const {
|
||||
signatures,
|
||||
selectedSignatures,
|
||||
setSelectedSignatures,
|
||||
handleDeleteSelected,
|
||||
handleSelectAll,
|
||||
handlePaste,
|
||||
hasUnsupportedLanguage,
|
||||
} = useSystemSignaturesData({
|
||||
systemId,
|
||||
settings,
|
||||
onCountChange,
|
||||
onLazyDeleteChange,
|
||||
onSignatureDeleted,
|
||||
});
|
||||
const deletedSignatures = useMemo(() => signatures.filter(s => s.deleted), [signatures]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectable) return;
|
||||
if (!clipboardContent?.text) return;
|
||||
|
||||
handlePaste(clipboardContent.text);
|
||||
onPaste?.(clipboardContent.text);
|
||||
|
||||
setClipboardContent(null);
|
||||
}, [selectable, clipboardContent, handlePaste, setClipboardContent]);
|
||||
}, [selectable, clipboardContent, onPaste, setClipboardContent]);
|
||||
|
||||
useHotkey(true, ['a'], handleSelectAll);
|
||||
useHotkey(true, ['a'], () => onSelectAll?.());
|
||||
|
||||
useHotkey(false, ['Backspace', 'Delete'], (event: KeyboardEvent) => {
|
||||
const targetWindow = (event.target as HTMLHtmlElement)?.closest(`[data-window-id="${SIGNATURE_WINDOW_ID}"]`);
|
||||
@@ -117,7 +108,7 @@ export const SystemSignaturesContent = ({
|
||||
event.stopPropagation();
|
||||
|
||||
// Delete key should always immediately delete, never show pending deletions
|
||||
handleDeleteSelected();
|
||||
onDeleteSelected?.();
|
||||
});
|
||||
|
||||
const handleResize = useCallback(() => {
|
||||
@@ -152,9 +143,9 @@ export const SystemSignaturesContent = ({
|
||||
|
||||
selectable
|
||||
? onSelect?.(selectableSignatures[0])
|
||||
: setSelectedSignatures(selectableSignatures as ExtendedSystemSignature[]);
|
||||
: onSelectSignatures?.(selectableSignatures as ExtendedSystemSignature[]);
|
||||
},
|
||||
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
|
||||
[onSelect, selectable, onSelectSignatures, deletedSignatures],
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -177,9 +168,6 @@ export const SystemSignaturesContent = ({
|
||||
);
|
||||
|
||||
const filteredSignatures = useMemo<ExtendedSystemSignature[]>(() => {
|
||||
// Get the set of deleted signature IDs for quick lookup
|
||||
const deletedIds = new Set(deletedSignatures.map(sig => sig.eve_id));
|
||||
|
||||
// Common filter function
|
||||
const shouldShowSignature = (sig: ExtendedSystemSignature): boolean => {
|
||||
if (filterSignature && !filterSignature(sig)) {
|
||||
@@ -213,24 +201,8 @@ export const SystemSignaturesContent = ({
|
||||
return settings[sig.kind] as boolean;
|
||||
};
|
||||
|
||||
// Filter active signatures, excluding any that are in the deleted list
|
||||
const activeSignatures = signatures.filter(sig => {
|
||||
// Skip if this signature is in the deleted list
|
||||
if (deletedIds.has(sig.eve_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shouldShowSignature(sig);
|
||||
});
|
||||
|
||||
// Add deleted signatures with pending deletion flag, applying the same filters
|
||||
const deletedWithPendingFlag = deletedSignatures.filter(shouldShowSignature).map(sig => ({
|
||||
...sig,
|
||||
pendingDeletion: true,
|
||||
}));
|
||||
|
||||
return [...activeSignatures, ...deletedWithPendingFlag];
|
||||
}, [signatures, hideLinkedSignatures, settings, filterSignature, deletedSignatures]);
|
||||
return signatures.filter(sig => shouldShowSignature(sig));
|
||||
}, [signatures, hideLinkedSignatures, settings, filterSignature]);
|
||||
|
||||
const onRowMouseEnter = useCallback((e: DataTableRowMouseEvent) => {
|
||||
setHoveredSignature(e.data as SystemSignature);
|
||||
@@ -253,20 +225,18 @@ export const SystemSignaturesContent = ({
|
||||
|
||||
return getSignatureRowClass(
|
||||
rowData as ExtendedSystemSignature,
|
||||
refVars.current.selectedSignatures,
|
||||
refVars.current.selectedSignatures || [],
|
||||
refVars.current.settings[SETTINGS_KEYS.COLOR_BY_TYPE] as boolean,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const handleSortSettings = useCallback(
|
||||
(e: DataTableStateEvent) =>
|
||||
refVars.current.settingsSignaturesUpdate({
|
||||
...refVars.current.settingsSignatures,
|
||||
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
|
||||
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const handleSortSettings = useCallback((e: DataTableStateEvent) => {
|
||||
refVars.current.settingsSignaturesUpdate({
|
||||
...refVars.current.settingsSignatures,
|
||||
[SETTINGS_KEYS.SORT_FIELD]: e.sortField,
|
||||
[SETTINGS_KEYS.SORT_ORDER]: e.sortOrder,
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={tableRef} className="h-full">
|
||||
@@ -287,7 +257,7 @@ export const SystemSignaturesContent = ({
|
||||
value={filteredSignatures}
|
||||
size="small"
|
||||
selectionMode="multiple"
|
||||
selection={selectedSignatures}
|
||||
selection={selectedSignatures || []}
|
||||
metaKeySelection
|
||||
onSelectionChange={handleSelectSignatures}
|
||||
dataKey="eve_id"
|
||||
@@ -336,6 +306,8 @@ export const SystemSignaturesContent = ({
|
||||
style={{ maxWidth: nameColumnWidth }}
|
||||
hidden={isCompact || isMedium}
|
||||
body={renderInfoColumn}
|
||||
sortable
|
||||
sortField="name"
|
||||
/>
|
||||
{showDescriptionColumn && (
|
||||
<Column
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { ExtendedSystemSignature, SignatureGroup } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { getRowBackgroundColor } from './getRowBackgroundColor';
|
||||
import classes from './rowStyles.module.scss';
|
||||
|
||||
@@ -20,7 +20,7 @@ export function getSignatureRowClass(
|
||||
return clsx([...baseCls, 'bg-violet-400/40 hover:bg-violet-300/40']);
|
||||
}
|
||||
|
||||
if (row.pendingDeletion) {
|
||||
if (row.deleted) {
|
||||
return clsx([...baseCls, 'bg-red-400/40 hover:bg-red-400/50']);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
export interface UseSystemSignaturesDataProps {
|
||||
systemId: string;
|
||||
settings: SignatureSettingsType;
|
||||
hideLinkedSignatures?: boolean;
|
||||
onCountChange?: (count: number) => void;
|
||||
onPendingChange?: (
|
||||
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
|
||||
undo: () => void,
|
||||
) => void;
|
||||
onLazyDeleteChange?: (value: boolean) => void;
|
||||
deletionTiming?: number;
|
||||
}
|
||||
|
||||
export interface UseFetchingParams {
|
||||
systemId: string;
|
||||
settings: SignatureSettingsType;
|
||||
signaturesRef: React.MutableRefObject<ExtendedSystemSignature[]>;
|
||||
setSignatures: React.Dispatch<React.SetStateAction<ExtendedSystemSignature[]>>;
|
||||
pendingDeletionMapRef: React.MutableRefObject<Record<string, ExtendedSystemSignature>>;
|
||||
}
|
||||
|
||||
export interface UsePendingDeletionParams {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { prepareUpdatePayload } from '../helpers';
|
||||
import { UsePendingDeletionParams } from './types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
export function usePendingDeletions({
|
||||
systemId,
|
||||
setSignatures,
|
||||
onPendingChange,
|
||||
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
|
||||
const { outCommand } = useMapRootState();
|
||||
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
|
||||
|
||||
const processRemovedSignatures = useCallback(
|
||||
async (
|
||||
removed: ExtendedSystemSignature[],
|
||||
added: ExtendedSystemSignature[],
|
||||
updated: ExtendedSystemSignature[],
|
||||
) => {
|
||||
if (!removed.length) return;
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: prepareUpdatePayload(systemId, added, updated, removed),
|
||||
});
|
||||
},
|
||||
[systemId, outCommand],
|
||||
);
|
||||
|
||||
const clearPendingDeletions = useCallback(() => {
|
||||
pendingDeletionMapRef.current = {};
|
||||
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
|
||||
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
pendingDeletionMapRef,
|
||||
processRemovedSignatures,
|
||||
clearPendingDeletions,
|
||||
};
|
||||
}
|
||||
@@ -1,21 +1,27 @@
|
||||
import { useCallback } from 'react';
|
||||
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { prepareUpdatePayload, getActualSigs, mergeLocalPending } from '../helpers';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { getDeletionTimeoutMs } from '../constants';
|
||||
import { getActualSigs, prepareUpdatePayload } from '../helpers';
|
||||
import { UseFetchingParams } from './types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const useSignatureFetching = ({
|
||||
systemId,
|
||||
signaturesRef,
|
||||
setSignatures,
|
||||
pendingDeletionMapRef,
|
||||
}: UseFetchingParams) => {
|
||||
export const useSignatureFetching = ({ systemId, settings, signaturesRef, setSignatures }: UseFetchingParams) => {
|
||||
const {
|
||||
data: { characters },
|
||||
outCommand,
|
||||
} = useMapRootState();
|
||||
|
||||
const deleteTimeout = useMemo(() => {
|
||||
const lazyDelete = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
|
||||
if (!lazyDelete) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getDeletionTimeoutMs(settings);
|
||||
}, [settings]);
|
||||
|
||||
const handleGetSignatures = useCallback(async () => {
|
||||
if (!systemId) {
|
||||
setSignatures([]);
|
||||
@@ -32,24 +38,23 @@ export const useSignatureFetching = ({
|
||||
character_name: characters.find(c => c.eve_id === s.character_eve_id)?.name,
|
||||
})) as ExtendedSystemSignature[];
|
||||
|
||||
setSignatures(() => mergeLocalPending(pendingDeletionMapRef, extended));
|
||||
setSignatures(() => extended);
|
||||
}, [characters, systemId, outCommand]);
|
||||
|
||||
const handleUpdateSignatures = useCallback(
|
||||
async (newList: ExtendedSystemSignature[], updateOnly: boolean, skipUpdateUntouched?: boolean) => {
|
||||
const { added, updated, removed } = getActualSigs(
|
||||
signaturesRef.current,
|
||||
newList,
|
||||
updateOnly,
|
||||
skipUpdateUntouched,
|
||||
);
|
||||
const actualSigs = getActualSigs(signaturesRef.current, newList, updateOnly, skipUpdateUntouched);
|
||||
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: prepareUpdatePayload(systemId, added, updated, removed),
|
||||
});
|
||||
const { added, updated, removed } = actualSigs;
|
||||
|
||||
if (updated.length !== 0 || added.length !== 0 || removed.length !== 0) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: { ...prepareUpdatePayload(systemId, added, updated, removed), deleteTimeout },
|
||||
});
|
||||
}
|
||||
},
|
||||
[systemId, outCommand, signaturesRef],
|
||||
[systemId, deleteTimeout, outCommand, signaturesRef],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
import { ExtendedSystemSignature, OutCommandHandler } from '@/hooks/Mapper/types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { getDeletionTimeoutMs } from '../constants';
|
||||
|
||||
/**
|
||||
* Custom hook for managing pending signature deletions and undo countdown.
|
||||
*/
|
||||
export function useSignatureUndo(
|
||||
systemId: string | undefined,
|
||||
settings: SignatureSettingsType,
|
||||
deletedSignatures: ExtendedSystemSignature[],
|
||||
outCommand: OutCommandHandler,
|
||||
) {
|
||||
const [countdown, setCountdown] = useState<number>(0);
|
||||
const intervalRef = useRef<number | null>(null);
|
||||
|
||||
// Clear deleted signatures when system changes
|
||||
useEffect(() => {
|
||||
if (systemId) {
|
||||
setCountdown(0);
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}
|
||||
}, [systemId]);
|
||||
|
||||
// kick off or clear countdown whenever pendingIds changes
|
||||
useEffect(() => {
|
||||
// clear any existing timer
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
|
||||
if (deletedSignatures.length === 0) {
|
||||
setCountdown(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// determine timeout from settings
|
||||
const timeoutMs = getDeletionTimeoutMs(settings);
|
||||
|
||||
// Ensure a minimum of 1 second for immediate deletion so the UI shows
|
||||
const effectiveTimeoutMs = timeoutMs === 0 ? 1000 : timeoutMs;
|
||||
|
||||
setCountdown(Math.ceil(effectiveTimeoutMs / 1000));
|
||||
|
||||
// start new interval
|
||||
intervalRef.current = window.setInterval(() => {
|
||||
setCountdown(prev => {
|
||||
if (prev <= 1) {
|
||||
clearInterval(intervalRef.current!);
|
||||
intervalRef.current = null;
|
||||
return 0;
|
||||
}
|
||||
return prev - 1;
|
||||
});
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [deletedSignatures, settings]);
|
||||
|
||||
// undo handler
|
||||
const handleUndo = useCallback(async () => {
|
||||
if (!systemId || deletedSignatures.length === 0) return;
|
||||
await outCommand({
|
||||
type: OutCommand.undoDeleteSignatures,
|
||||
data: { system_id: systemId, eve_ids: deletedSignatures.map(s => s.eve_id) },
|
||||
});
|
||||
setCountdown(0);
|
||||
if (intervalRef.current != null) {
|
||||
clearInterval(intervalRef.current);
|
||||
intervalRef.current = null;
|
||||
}
|
||||
}, [systemId, deletedSignatures, outCommand]);
|
||||
|
||||
return {
|
||||
countdown,
|
||||
handleUndo,
|
||||
};
|
||||
}
|
||||
@@ -1,44 +1,29 @@
|
||||
import { useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { parseSignatures } from '@/hooks/Mapper/helpers';
|
||||
import { Commands, ExtendedSystemSignature, SignatureKind } from '@/hooks/Mapper/types';
|
||||
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import useRefState from 'react-usestateref';
|
||||
|
||||
import { getDeletionTimeoutMs } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { getActualSigs } from '../helpers';
|
||||
import { UseSystemSignaturesDataProps } from './types';
|
||||
import { usePendingDeletions } from './usePendingDeletions';
|
||||
import { useSignatureFetching } from './useSignatureFetching';
|
||||
import { SETTINGS_KEYS } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { UseSystemSignaturesDataProps } from './types';
|
||||
import { useSignatureFetching } from './useSignatureFetching';
|
||||
|
||||
export const useSystemSignaturesData = ({
|
||||
systemId,
|
||||
settings,
|
||||
onCountChange,
|
||||
onPendingChange,
|
||||
onLazyDeleteChange,
|
||||
onSignatureDeleted,
|
||||
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & {
|
||||
onSignatureDeleted?: (deletedSignatures: ExtendedSystemSignature[]) => void;
|
||||
}) => {
|
||||
const { outCommand } = useMapRootState();
|
||||
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
|
||||
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
|
||||
const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState<boolean>(false);
|
||||
|
||||
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
|
||||
systemId,
|
||||
setSignatures,
|
||||
onPendingChange,
|
||||
});
|
||||
|
||||
const { handleGetSignatures, handleUpdateSignatures } = useSignatureFetching({
|
||||
systemId,
|
||||
settings,
|
||||
signaturesRef,
|
||||
setSignatures,
|
||||
pendingDeletionMapRef,
|
||||
});
|
||||
|
||||
const handlePaste = useCallback(
|
||||
@@ -67,40 +52,14 @@ export const useSystemSignaturesData = ({
|
||||
setHasUnsupportedLanguage(false);
|
||||
}
|
||||
|
||||
const currentNonPending = lazyDeleteValue
|
||||
? signaturesRef.current.filter(sig => !sig.pendingDeletion)
|
||||
: signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition);
|
||||
|
||||
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, false);
|
||||
|
||||
if (removed.length > 0) {
|
||||
await processRemovedSignatures(removed, added, updated);
|
||||
|
||||
// Show pending deletions if lazy deletion is enabled
|
||||
// The deletion timing controls how long the countdown lasts, not whether lazy delete is active
|
||||
if (onSignatureDeleted && lazyDeleteValue) {
|
||||
onSignatureDeleted(removed);
|
||||
}
|
||||
}
|
||||
|
||||
if (updated.length !== 0 || added.length !== 0) {
|
||||
await outCommand({
|
||||
type: OutCommand.updateSignatures,
|
||||
data: {
|
||||
system_id: systemId,
|
||||
added,
|
||||
updated,
|
||||
removed: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
await handleUpdateSignatures(incomingSignatures, !lazyDeleteValue, false);
|
||||
|
||||
const keepLazy = settings[SETTINGS_KEYS.KEEP_LAZY_DELETE] as boolean;
|
||||
if (lazyDeleteValue && !keepLazy) {
|
||||
onLazyDeleteChange?.(false);
|
||||
}
|
||||
},
|
||||
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
|
||||
[settings, handleUpdateSignatures, onLazyDeleteChange],
|
||||
);
|
||||
|
||||
const handleDeleteSelected = useCallback(async () => {
|
||||
@@ -109,23 +68,15 @@ export const useSystemSignaturesData = ({
|
||||
const selectedIds = selectedSignatures.map(s => s.eve_id);
|
||||
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
|
||||
|
||||
// IMPORTANT: Send deletion to server BEFORE updating local state
|
||||
// Otherwise signaturesRef.current will be updated and getActualSigs won't detect removals
|
||||
await handleUpdateSignatures(finalList, false, true);
|
||||
|
||||
// Update local state after server call
|
||||
setSignatures(finalList);
|
||||
setSelectedSignatures([]);
|
||||
}, [handleUpdateSignatures, selectedSignatures, signatures, setSignatures]);
|
||||
|
||||
await handleUpdateSignatures(finalList, false, true);
|
||||
}, [handleUpdateSignatures, selectedSignatures, signatures]);
|
||||
|
||||
const handleSelectAll = useCallback(() => {
|
||||
setSelectedSignatures(signatures);
|
||||
}, [signatures]);
|
||||
|
||||
const undoPending = useCallback(() => {
|
||||
clearPendingDeletions();
|
||||
}, [clearPendingDeletions]);
|
||||
|
||||
useMapEventListener(event => {
|
||||
if (event.name === Commands.signaturesUpdated && String(event.data) === String(systemId)) {
|
||||
handleGetSignatures();
|
||||
@@ -136,18 +87,13 @@ export const useSystemSignaturesData = ({
|
||||
useEffect(() => {
|
||||
if (!systemId) {
|
||||
setSignatures([]);
|
||||
undoPending();
|
||||
return;
|
||||
}
|
||||
handleGetSignatures();
|
||||
}, [systemId]);
|
||||
|
||||
useEffect(() => {
|
||||
onCountChange?.(signatures.length);
|
||||
}, [signatures]);
|
||||
|
||||
return {
|
||||
signatures: signatures.filter(sig => !sig.deleted),
|
||||
signatures,
|
||||
selectedSignatures,
|
||||
setSelectedSignatures,
|
||||
handleDeleteSelected,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
import { renderK162Type } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { renderName } from './renderName.tsx';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
import clsx from 'clsx';
|
||||
import { renderName } from './renderName.tsx';
|
||||
|
||||
export const renderInfoColumn = (row: SystemSignature) => {
|
||||
if (!row.group || row.group === SignatureGroup.Wormhole) {
|
||||
@@ -18,7 +18,9 @@ export const renderInfoColumn = (row: SystemSignature) => {
|
||||
|
||||
return (
|
||||
<div className="flex justify-start items-center gap-[4px]">
|
||||
{customInfo.isEOL && (
|
||||
{row.temporary_name && <span className={clsx('text-[12px]')}>{row.temporary_name}</span>}
|
||||
|
||||
{customInfo.time_status === TimeStatus._1h && (
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as EOL">
|
||||
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
@@ -30,10 +30,14 @@ export const SystemStructures: React.FC = () => {
|
||||
|
||||
const processClipboard = useCallback(
|
||||
(text: string) => {
|
||||
if (!systemId) {
|
||||
console.warn('Cannot update structures: no system selected');
|
||||
return;
|
||||
}
|
||||
const updated = processSnippetText(text, structures);
|
||||
handleUpdateStructures(updated);
|
||||
},
|
||||
[structures, handleUpdateStructures],
|
||||
[systemId, structures, handleUpdateStructures],
|
||||
);
|
||||
|
||||
const handlePaste = useCallback(
|
||||
|
||||
@@ -30,9 +30,6 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
|
||||
const { outCommand } = useMapRootState();
|
||||
|
||||
const [prevQuery, setPrevQuery] = useState('');
|
||||
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (structure) {
|
||||
setEditData(structure);
|
||||
@@ -46,34 +43,24 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
// Searching corporation owners via auto-complete
|
||||
const searchOwners = useCallback(
|
||||
async (e: { query: string }) => {
|
||||
const newQuery = e.query.trim();
|
||||
if (!newQuery) {
|
||||
const query = e.query.trim();
|
||||
if (!query) {
|
||||
setOwnerSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If user typed more text but we have partial match in prevResults
|
||||
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
|
||||
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
|
||||
setOwnerSuggestions(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO fix it
|
||||
const { results = [] } = await outCommand({
|
||||
type: OutCommand.getCorporationNames,
|
||||
data: { search: newQuery },
|
||||
data: { search: query },
|
||||
});
|
||||
setOwnerSuggestions(results);
|
||||
setPrevQuery(newQuery);
|
||||
setPrevResults(results);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch owners:', err);
|
||||
setOwnerSuggestions([]);
|
||||
}
|
||||
},
|
||||
[prevQuery, prevResults, outCommand],
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const handleChange = (field: keyof StructureItem, val: string | Date) => {
|
||||
@@ -122,7 +109,6 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
|
||||
// fetch corporation ticker if we have an ownerId
|
||||
if (editData.ownerId) {
|
||||
try {
|
||||
// TODO fix it
|
||||
const { ticker } = await outCommand({
|
||||
type: OutCommand.getCorporationTicker,
|
||||
data: { corp_id: editData.ownerId },
|
||||
|
||||
@@ -56,6 +56,11 @@ export function useSystemStructures({ systemId, outCommand }: UseSystemStructure
|
||||
|
||||
const handleUpdateStructures = useCallback(
|
||||
async (newList: StructureItem[]) => {
|
||||
if (!systemId) {
|
||||
console.warn('Cannot update structures: systemId is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
const { added, updated, removed } = getActualStructures(structures, newList);
|
||||
|
||||
const sanitizedAdded = added.map(sanitizeIds);
|
||||
|
||||
@@ -10,9 +10,14 @@ import { useCallback } from 'react';
|
||||
import { TooltipPosition, WdButton, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const CommonSettings = () => {
|
||||
const { renderSettingItem } = useMapSettings();
|
||||
const {
|
||||
storedSettings: { resetSettings },
|
||||
} = useMapRootState();
|
||||
|
||||
const { cfShow, cfHide, cfVisible, cfRef } = useConfirmPopup();
|
||||
|
||||
const renderSettingsList = useCallback(
|
||||
@@ -22,7 +27,7 @@ export const CommonSettings = () => {
|
||||
[renderSettingItem],
|
||||
);
|
||||
|
||||
const handleResetSettings = () => {};
|
||||
const handleResetSettings = useCallback(() => resetSettings(), [resetSettings]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full gap-1">
|
||||
|
||||
@@ -64,7 +64,7 @@ export const ImportExport = () => {
|
||||
|
||||
// INFO: WE NOT SUPPORT MIGRATIONS FOR OLD FILES AND Clipboard
|
||||
const parsed = parseMapUserSettings(text);
|
||||
if (applySettings(applyMigrations(parsed))) {
|
||||
if (applySettings(applyMigrations(parsed) || createDefaultStoredSettings())) {
|
||||
toast.current?.show({
|
||||
severity: 'success',
|
||||
summary: 'Import',
|
||||
|
||||
@@ -35,7 +35,7 @@ export const ServerSettings = () => {
|
||||
|
||||
try {
|
||||
//INFO: INSTEAD CHECK WE WILL TRY TO APPLY MIGRATION
|
||||
applySettings(applyMigrations(JSON.parse(res.default_settings)));
|
||||
applySettings(applyMigrations(JSON.parse(res.default_settings)) || createDefaultStoredSettings());
|
||||
callToastSuccess(toast.current, 'Settings synchronized successfully');
|
||||
} catch (error) {
|
||||
applySettings(createDefaultStoredSettings());
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
import {
|
||||
SignatureGroupContent,
|
||||
SignatureGroupSelect,
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { Controller, FormProvider, useForm } from 'react-hook-form';
|
||||
|
||||
type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & {
|
||||
linked_system: string;
|
||||
@@ -119,6 +119,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
added: [],
|
||||
updated: [out],
|
||||
removed: [],
|
||||
deleteTimeout: 0,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@use "sass:color";
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@import '@/hooks/Mapper/components/map/styles/solar-system-node';
|
||||
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
|
||||
|
||||
:root {
|
||||
--rf-has-user-characters: #ffc75d;
|
||||
@@ -108,7 +108,7 @@
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
border-color: v.$pastel-pink;
|
||||
box-shadow: 0 0 10px #9a1af1c2;
|
||||
}
|
||||
|
||||
@@ -122,11 +122,11 @@
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
|
||||
border-color: $neon-color-1;
|
||||
border-color: v.$neon-color-1;
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
$neon-color-3 0px,
|
||||
$neon-color-3 8px,
|
||||
v.$neon-color-3 0px,
|
||||
v.$neon-color-3 8px,
|
||||
transparent 8px,
|
||||
transparent 21px
|
||||
);
|
||||
@@ -152,7 +152,7 @@
|
||||
&.eve-system-status-lookingFor {
|
||||
background-image: linear-gradient(275deg, #45ff8f2f, #457fff2f);
|
||||
&.selected {
|
||||
border-color: $pastel-pink;
|
||||
border-color: v.$pastel-pink;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
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';
|
||||
import { ContextMenuSystem, useContextMenuSystemHandlers } from '@/hooks/Mapper/components/contexts';
|
||||
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import {
|
||||
SystemCustomLabelDialog,
|
||||
SystemLinkSignatureDialog,
|
||||
SystemSettingsDialog,
|
||||
} from '@/hooks/Mapper/components/mapInterface/components';
|
||||
import { Connections } from '@/hooks/Mapper/components/mapRootContent/components/Connections';
|
||||
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
|
||||
import { getSystemById } from '@/hooks/Mapper/helpers';
|
||||
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CommandSelectSystems, OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
|
||||
import { Commands } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import isEqual from 'lodash.isequal';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Node, useReactFlow, Viewport, XYPosition } from 'reactflow';
|
||||
import { ContextMenuSystemMultiple, useContextMenuSystemMultipleHandlers } from '../contexts/ContextMenuSystemMultiple';
|
||||
|
||||
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
import { emitMapEvent, useMapEventListener } from '@/hooks/Mapper/events';
|
||||
import { useCommandsSystems } from '@/hooks/Mapper/mapRootProvider/hooks/api';
|
||||
|
||||
import { useDeleteSystems } from '@/hooks/Mapper/components/contexts/hooks';
|
||||
import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts';
|
||||
import {
|
||||
AddSystemDialog,
|
||||
SearchOnSubmitCallback,
|
||||
} from '@/hooks/Mapper/components/mapInterface/components/AddSystemDialog';
|
||||
import { useHotkey } from '../../hooks/useHotkey';
|
||||
import { PingType } from '@/hooks/Mapper/types/ping.ts';
|
||||
import { SystemPingDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemPingDialog';
|
||||
import { MiniMapPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { useCommonMapEventProcessor } from '@/hooks/Mapper/components/mapWrapper/hooks/useCommonMapEventProcessor.ts';
|
||||
import { MINIMAP_PLACEMENT_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { MiniMapPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { PingType } from '@/hooks/Mapper/types/ping.ts';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
import { useHotkey } from '../../hooks/useHotkey';
|
||||
import { MINI_MAP_PLACEMENT_OFFSETS } from './constants.ts';
|
||||
|
||||
// TODO: INFO - this component needs for abstract work with Map instance
|
||||
@@ -106,7 +106,7 @@ export const MapWrapper = () => {
|
||||
|
||||
runCommand({
|
||||
name: Commands.selectSystems,
|
||||
data: { systems: selectedSystems } as CommandSelectSystems,
|
||||
data: { systems: selectedSystems, delay: 200 } as CommandSelectSystems,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -4,8 +4,17 @@ import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrap
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit/WdTooltip';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type MenuItemWithInfoProps = { infoTitle: ReactNode; infoClass?: string } & WithChildren;
|
||||
export const MenuItemWithInfo = ({ children, infoClass, infoTitle }: MenuItemWithInfoProps) => {
|
||||
type MenuItemWithInfoProps = {
|
||||
infoTitle: ReactNode;
|
||||
infoClass?: string;
|
||||
tooltipWrapperClassName?: string;
|
||||
} & WithChildren;
|
||||
export const MenuItemWithInfo = ({
|
||||
children,
|
||||
infoClass,
|
||||
infoTitle,
|
||||
tooltipWrapperClassName,
|
||||
}: MenuItemWithInfoProps) => {
|
||||
return (
|
||||
<div className="flex justify-between w-full h-full items-center">
|
||||
{children}
|
||||
@@ -13,6 +22,7 @@ export const MenuItemWithInfo = ({ children, infoClass, infoTitle }: MenuItemWit
|
||||
content={infoTitle}
|
||||
position={TooltipPosition.top}
|
||||
className="!opacity-100 !pointer-events-auto"
|
||||
wrapperClassName={tooltipWrapperClassName}
|
||||
>
|
||||
<div className={clsx('pi text-orange-400', infoClass)} />
|
||||
</WdTooltipWrapper>
|
||||
|
||||
@@ -45,40 +45,42 @@ export const WHClassView = ({
|
||||
const whClass = useMemo(() => WORMHOLES_ADDITIONAL_INFO[whData.dest], [whData.dest]);
|
||||
const whClassStyle = WORMHOLE_CLASS_STYLES[whClass?.wormholeClassID] ?? '';
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.WHClassViewRoot, className)}>
|
||||
{!hideTooltip && (
|
||||
<WdTooltipWrapper
|
||||
position={TooltipPosition.bottom}
|
||||
content={
|
||||
<div className="flex gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title="Total mass">{prepareMass(whData.total_mass)}</InfoDrawer>
|
||||
<InfoDrawer title="Jump mass">{prepareMass(whData.max_mass_per_jump)}</InfoDrawer>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title="Lifetime">{whData.lifetime}h</InfoDrawer>
|
||||
<InfoDrawer title="Mass regen">{prepareMass(whData.mass_regen)}</InfoDrawer>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
classes.WHClassViewContent,
|
||||
{ [classes.NoOffset]: noOffset },
|
||||
'wh-name select-none cursor-help',
|
||||
)}
|
||||
>
|
||||
{!hideWhClassName && <span className={clsx({ [whClassStyle]: highlightName })}>{whClassName}</span>}
|
||||
{!hideWhClass && whClass && (
|
||||
<span className={clsx(classes.WHClassName, whClassStyle, classNameWh)}>
|
||||
{useShortTitle ? whClass.shortTitle : whClass.shortName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
const content = (
|
||||
<div
|
||||
className={clsx(classes.WHClassViewContent, { [classes.NoOffset]: noOffset }, 'wh-name select-none cursor-help')}
|
||||
>
|
||||
{!hideWhClassName && <span className={clsx({ [whClassStyle]: highlightName })}>{whClassName}</span>}
|
||||
{!hideWhClass && whClass && (
|
||||
<span className={clsx(classes.WHClassName, whClassStyle, classNameWh)}>
|
||||
{useShortTitle ? whClass.shortTitle : whClass.shortName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
if (hideTooltip) {
|
||||
return <div className={clsx(classes.WHClassViewRoot, className)}>{content}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.WHClassViewRoot, className)}>
|
||||
<WdTooltipWrapper
|
||||
position={TooltipPosition.bottom}
|
||||
content={
|
||||
<div className="flex gap-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title="Total mass">{prepareMass(whData.total_mass)}</InfoDrawer>
|
||||
<InfoDrawer title="Jump mass">{prepareMass(whData.max_mass_per_jump)}</InfoDrawer>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title="Lifetime">{whData.lifetime}h</InfoDrawer>
|
||||
<InfoDrawer title="Mass regen">{prepareMass(whData.mass_regen)}</InfoDrawer>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{content}
|
||||
</WdTooltipWrapper>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type WdMenuItemProps = { icon?: string; disabled?: boolean } & WithChildren;
|
||||
export const WdMenuItem = ({ children, icon, disabled }: WdMenuItemProps) => {
|
||||
type WdMenuItemProps = { icon?: string; disabled?: boolean } & WithChildren & WithClassName;
|
||||
export const WdMenuItem = ({ children, icon, disabled, className }: WdMenuItemProps) => {
|
||||
return (
|
||||
<a
|
||||
className={clsx('flex gap-[6px] w-full h-full items-center px-[12px] !py-0 ml-[-2px]', 'p-menuitem-link', {
|
||||
'p-disabled': disabled,
|
||||
})}
|
||||
className={clsx(
|
||||
'flex gap-[6px] w-full h-full items-center px-[12px] !py-0',
|
||||
'p-menuitem-link',
|
||||
{
|
||||
'p-disabled': disabled,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{icon && <div className={clsx('min-w-[20px]', icon)}></div>}
|
||||
<div className="w-full">{children}</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ export type WdTooltipWrapperProps = {
|
||||
interactive?: boolean;
|
||||
smallPaddings?: boolean;
|
||||
tooltipClassName?: string;
|
||||
wrapperClassName?: string;
|
||||
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
|
||||
Omit<TooltipProps, 'content'>;
|
||||
|
||||
@@ -26,6 +27,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
|
||||
smallPaddings,
|
||||
size,
|
||||
tooltipClassName,
|
||||
wrapperClassName,
|
||||
...props
|
||||
},
|
||||
forwardedRef,
|
||||
@@ -36,7 +38,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.WdTooltipWrapperRoot, className)} {...props}>
|
||||
{targetSelector ? <>{children}</> : <div className={autoClass}>{children}</div>}
|
||||
{targetSelector ? <>{children}</> : <div className={clsx(autoClass, wrapperClassName)}>{children}</div>}
|
||||
|
||||
<WdTooltip
|
||||
ref={forwardedRef}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { PingsPlacement } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
|
||||
export enum SESSION_KEY {
|
||||
viewPort = 'viewPort',
|
||||
windows = 'windows',
|
||||
windowsVisible = 'windowsVisible',
|
||||
routes = 'routes',
|
||||
}
|
||||
|
||||
export const SYSTEM_FOCUSED_LIFETIME = 10000;
|
||||
|
||||
export const GRADIENT_MENU_ACTIVE_CLASSES = 'bg-gradient-to-br from-transparent/10 to-fuchsia-300/10';
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './parseSignatures';
|
||||
export * from './getSystemById';
|
||||
export * from './getEveImageUrl';
|
||||
export * from './toastHelpers';
|
||||
export * from './recenterSystems';
|
||||
|
||||
39
assets/js/hooks/Mapper/helpers/recenterSystems.ts
Normal file
39
assets/js/hooks/Mapper/helpers/recenterSystems.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { XYPosition } from 'reactflow';
|
||||
|
||||
export type WithPosition<T = unknown> = T & { position: XYPosition };
|
||||
|
||||
export const computeBoundsCenter = (items: Array<WithPosition>): XYPosition => {
|
||||
if (items.length === 0) return { x: 0, y: 0 };
|
||||
|
||||
let minX = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let minY = Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
for (const { position } of items) {
|
||||
if (position.x < minX) minX = position.x;
|
||||
if (position.x > maxX) maxX = position.x;
|
||||
if (position.y < minY) minY = position.y;
|
||||
if (position.y > maxY) maxY = position.y;
|
||||
}
|
||||
|
||||
return {
|
||||
x: minX + (maxX - minX) / 2,
|
||||
y: minY + (maxY - minY) / 2,
|
||||
};
|
||||
};
|
||||
|
||||
/** Смещает все точки так, чтобы центр области стал (0,0) */
|
||||
export const recenterSystemsByBounds = <T extends WithPosition>(items: T[]): { center: XYPosition; systems: T[] } => {
|
||||
const center = computeBoundsCenter(items);
|
||||
|
||||
const systems = items.map(it => ({
|
||||
...it,
|
||||
position: {
|
||||
x: it.position.x - center.x,
|
||||
y: it.position.y - center.y,
|
||||
},
|
||||
}));
|
||||
|
||||
return { center, systems };
|
||||
};
|
||||
@@ -28,14 +28,17 @@ export const useEventBuffer = <T>(handler: UseEventBufferHandler<T>) => {
|
||||
eventTickRef.current = eventTick;
|
||||
|
||||
// @ts-ignore
|
||||
const handleEvent = useCallback(event => {
|
||||
if (!eventTickRef.current) {
|
||||
return;
|
||||
}
|
||||
const handleEvent = useCallback(
|
||||
event => {
|
||||
if (!eventTickRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventsBufferRef.current.push(event);
|
||||
eventTickRef.current();
|
||||
}, []);
|
||||
eventsBufferRef.current.push(event);
|
||||
eventTickRef.current();
|
||||
},
|
||||
[eventTickRef.current],
|
||||
);
|
||||
|
||||
return { handleEvent };
|
||||
};
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
MapUnionTypes,
|
||||
OutCommandHandler,
|
||||
SolarSystemConnection,
|
||||
StringBoolean,
|
||||
TrackingCharacter,
|
||||
UseCharactersCacheData,
|
||||
UseCommentsData,
|
||||
UserPermission,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { useCharactersCache, useComments, useMapRootHandlers } from '@/hooks/Mapper/mapRootProvider/hooks';
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
@@ -80,7 +82,16 @@ const INITIAL_DATA: MapRootData = {
|
||||
selectedSystems: [],
|
||||
selectedConnections: [],
|
||||
userPermissions: {},
|
||||
options: {},
|
||||
options: {
|
||||
allowed_copy_for: UserPermission.VIEW_SYSTEM,
|
||||
allowed_paste_for: UserPermission.VIEW_SYSTEM,
|
||||
layout: '',
|
||||
restrict_offline_showing: 'false',
|
||||
show_linked_signature_id: 'false',
|
||||
show_linked_signature_id_temp_name: 'false',
|
||||
show_temp_system_name: 'false',
|
||||
store_custom_labels: 'false',
|
||||
},
|
||||
isSubscriptionActive: false,
|
||||
linkSignatureToSystem: null,
|
||||
mainCharacterEveId: null,
|
||||
@@ -135,7 +146,7 @@ export interface MapRootContextProps {
|
||||
hasOldSettings: boolean;
|
||||
getSettingsForExport(): string | undefined;
|
||||
applySettings(settings: MapUserSettings): boolean;
|
||||
resetSettings(settings: MapUserSettings): void;
|
||||
resetSettings(): void;
|
||||
checkOldSettings(): void;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const createWidgetSettings = <T>(settings: T) => {
|
||||
export const createDefaultStoredSettings = (): MapUserSettings => {
|
||||
return {
|
||||
version: STORED_SETTINGS_VERSION,
|
||||
migratedFromOld: true,
|
||||
migratedFromOld: false,
|
||||
killsWidget: createWidgetSettings(DEFAULT_KILLS_WIDGET_SETTINGS),
|
||||
localWidget: createWidgetSettings(DEFAULT_WIDGET_LOCAL_SETTINGS),
|
||||
widgets: createWidgetSettings(getDefaultWidgetProps()),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import {
|
||||
CommandCharacterAdded,
|
||||
CommandCharacterRemoved,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
CommandCharacterUpdated,
|
||||
CommandPresentCharacters,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
export const useCommandsCharacters = () => {
|
||||
const { update } = useMapRootState();
|
||||
@@ -14,8 +14,27 @@ export const useCommandsCharacters = () => {
|
||||
const ref = useRef({ update });
|
||||
ref.current = { update };
|
||||
|
||||
const charactersUpdated = useCallback((characters: CommandCharactersUpdated) => {
|
||||
ref.current.update(() => ({ characters: characters.slice() }));
|
||||
const charactersUpdated = useCallback((updatedCharacters: CommandCharactersUpdated) => {
|
||||
ref.current.update(state => {
|
||||
const existing = state.characters ?? [];
|
||||
// Put updatedCharacters into a map keyed by ID
|
||||
const updatedMap = new Map(updatedCharacters.map(c => [c.eve_id, c]));
|
||||
|
||||
// 1. Update existing characters when possible
|
||||
const merged = existing.map(character => {
|
||||
const updated = updatedMap.get(character.eve_id);
|
||||
if (updated) {
|
||||
updatedMap.delete(character.eve_id); // Mark as processed
|
||||
return { ...character, ...updated };
|
||||
}
|
||||
return character;
|
||||
});
|
||||
|
||||
// 2. Any remaining items in updatedMap are NEW characters → add them
|
||||
const newCharacters = Array.from(updatedMap.values());
|
||||
|
||||
return { characters: [...merged, ...newCharacters] };
|
||||
});
|
||||
}, []);
|
||||
|
||||
const characterAdded = useCallback((value: CommandCharacterAdded) => {
|
||||
|
||||
@@ -42,7 +42,7 @@ export const useActualizeRemoteMapSettings = ({
|
||||
}
|
||||
|
||||
try {
|
||||
applySettings(applyMigrations(JSON.parse(res.default_settings)));
|
||||
applySettings(applyMigrations(JSON.parse(res.default_settings) || createDefaultStoredSettings()));
|
||||
} catch (error) {
|
||||
applySettings(createDefaultStoredSettings());
|
||||
}
|
||||
|
||||
@@ -115,10 +115,15 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
|
||||
}
|
||||
|
||||
try {
|
||||
// here we try to restore settings
|
||||
let oldMapData;
|
||||
if (!currentMapUserSettings.migratedFromOld) {
|
||||
const allData = extractData(LS_KEY_LEGASY);
|
||||
oldMapData = allData?.[map_slug];
|
||||
}
|
||||
|
||||
// INFO: after migrations migratedFromOld always will be true
|
||||
const migratedResult = applyMigrations(
|
||||
!currentMapUserSettings.migratedFromOld ? extractData(LS_KEY_LEGASY) : currentMapUserSettings,
|
||||
);
|
||||
const migratedResult = applyMigrations(oldMapData ? oldMapData : currentMapUserSettings);
|
||||
|
||||
if (!migratedResult) {
|
||||
setIsReady(true);
|
||||
@@ -143,10 +148,6 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
|
||||
setHasOldSettings(!!(widgetsOld || interfaceSettings || widgetRoutes || widgetLocal || widgetKills || onTheMapOld));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
checkOldSettings();
|
||||
}, [checkOldSettings]);
|
||||
|
||||
const getSettingsForExport = useCallback(() => {
|
||||
const { map_slug } = ref.current;
|
||||
|
||||
@@ -161,6 +162,24 @@ export const useMapUserSettings = ({ map_slug }: MapRootData, outCommand: OutCom
|
||||
applySettings(createDefaultStoredSettings());
|
||||
}, [applySettings]);
|
||||
|
||||
useEffect(() => {
|
||||
checkOldSettings();
|
||||
}, [checkOldSettings]);
|
||||
|
||||
// IN Case if in runtime someone clear settings
|
||||
useEffect(() => {
|
||||
if (Object.keys(windowsSettings).length !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isReady) {
|
||||
return;
|
||||
}
|
||||
|
||||
resetSettings();
|
||||
location.reload();
|
||||
}, [isReady, resetSettings, windowsSettings]);
|
||||
|
||||
return {
|
||||
isReady,
|
||||
hasOldSettings,
|
||||
|
||||
@@ -26,7 +26,7 @@ export const applyMigrations = (mapSettings: any) => {
|
||||
return { ...currentMapSettings, version: STORED_SETTINGS_VERSION, migratedFromOld: true };
|
||||
}
|
||||
|
||||
return;
|
||||
return currentMapSettings;
|
||||
}
|
||||
|
||||
const cmVersion = currentMapSettings.version || 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const STORED_SETTINGS_VERSION = 2;
|
||||
|
||||
export const LS_KEY_LEGASY = 'map-user-settings';
|
||||
export const LS_KEY = 'map-user-settings-v2';
|
||||
export const LS_KEY = 'map-user-settings-v3';
|
||||
|
||||
@@ -33,7 +33,6 @@ export type CharacterTypeRaw = {
|
||||
corporation_id: number;
|
||||
corporation_name: string;
|
||||
corporation_ticker: string;
|
||||
tracking_paused: boolean;
|
||||
};
|
||||
|
||||
export interface TrackingCharacter {
|
||||
|
||||
@@ -9,3 +9,4 @@ export * from './connectionPassages';
|
||||
export * from './permissions';
|
||||
export * from './comment';
|
||||
export * from './ping';
|
||||
export * from './options';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CommentType, PingData, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { CommentType, MapOptions, PingData, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { ActivitySummary, CharacterTypeRaw, TrackingCharacter } from '@/hooks/Mapper/types/character.ts';
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { DetailedKill, Kill } from '@/hooks/Mapper/types/kills.ts';
|
||||
@@ -94,7 +94,7 @@ export type CommandInit = {
|
||||
hubs: string[];
|
||||
user_hubs: string[];
|
||||
routes: RoutesList;
|
||||
options: Record<string, string | boolean>;
|
||||
options: MapOptions;
|
||||
reset?: boolean;
|
||||
is_subscription_active?: boolean;
|
||||
main_character_eve_id?: string | null;
|
||||
@@ -247,6 +247,7 @@ export enum OutCommand {
|
||||
deleteSystems = 'delete_systems',
|
||||
manualAddSystem = 'manual_add_system',
|
||||
manualAddConnection = 'manual_add_connection',
|
||||
manualPasteSystemsAndConnections = 'manual_paste_systems_and_connections',
|
||||
manualDeleteConnection = 'manual_delete_connection',
|
||||
setAutopilotWaypoint = 'set_autopilot_waypoint',
|
||||
addSystem = 'add_system',
|
||||
|
||||
@@ -4,7 +4,7 @@ import { CharacterTypeRaw } from '@/hooks/Mapper/types/character.ts';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types/system.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { SolarSystemConnection } from '@/hooks/Mapper/types/connection.ts';
|
||||
import { PingData, UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { MapOptions, PingData, UserPermissions } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
|
||||
export type MapUnionTypes = {
|
||||
@@ -23,7 +23,7 @@ export type MapUnionTypes = {
|
||||
kills: Record<number, number>;
|
||||
connections: SolarSystemConnection[];
|
||||
userPermissions: Partial<UserPermissions>;
|
||||
options: Record<string, string | boolean>;
|
||||
options: MapOptions;
|
||||
isSubscriptionActive: boolean;
|
||||
|
||||
mainCharacterEveId: string | null;
|
||||
|
||||
14
assets/js/hooks/Mapper/types/options.ts
Normal file
14
assets/js/hooks/Mapper/types/options.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { UserPermission } from '@/hooks/Mapper/types/permissions.ts';
|
||||
|
||||
export type StringBoolean = 'true' | 'false';
|
||||
|
||||
export type MapOptions = {
|
||||
allowed_copy_for: UserPermission;
|
||||
allowed_paste_for: UserPermission;
|
||||
layout: string;
|
||||
restrict_offline_showing: StringBoolean;
|
||||
show_linked_signature_id: StringBoolean;
|
||||
show_linked_signature_id_temp_name: StringBoolean;
|
||||
show_temp_system_name: StringBoolean;
|
||||
store_custom_labels: StringBoolean;
|
||||
};
|
||||
@@ -29,7 +29,7 @@ export type GroupType = {
|
||||
|
||||
export type SignatureCustomInfo = {
|
||||
k162Type?: string;
|
||||
isEOL?: boolean;
|
||||
time_status?: number;
|
||||
isCrit?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEventBuffer } from '@/hooks/Mapper/hooks';
|
||||
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
|
||||
import debounce from 'lodash.debounce';
|
||||
|
||||
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { RefObject, useCallback, useEffect, useRef } from 'react';
|
||||
@@ -16,23 +16,6 @@ export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRe
|
||||
const visibleRef = useRef(visible);
|
||||
visibleRef.current = visible;
|
||||
|
||||
// @ts-ignore
|
||||
const handleBufferedEvent = useCallback(({ type, body }) => {
|
||||
if (!visibleRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
handlerRefs.forEach(ref => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current?.command(type, body);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleMapEvent } = useEventBuffer<any>(handleBufferedEvent);
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// const [record, setRecord] = useLocalStorageState<boolean>('record', {
|
||||
// defaultValue: false,
|
||||
@@ -73,6 +56,52 @@ export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRe
|
||||
[hooksRef.current],
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const eventsBufferRef = useRef<{ type; body }[]>([]);
|
||||
|
||||
const eventTick = useCallback(
|
||||
debounce(() => {
|
||||
if (eventsBufferRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = eventsBufferRef.current.shift()!;
|
||||
handlerRefs.forEach(ref => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current?.command(type, body);
|
||||
});
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
|
||||
|
||||
if (eventsBufferRef.current.length > 0) {
|
||||
eventTick();
|
||||
}
|
||||
}, 10),
|
||||
[],
|
||||
);
|
||||
const eventTickRef = useRef(eventTick);
|
||||
eventTickRef.current = eventTick;
|
||||
|
||||
// @ts-ignore
|
||||
const handleMapEvent = useCallback(({ type, body }) => {
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// const currentTime = +new Date();
|
||||
// const timeDiff = currentTime - prevEventTime;
|
||||
// prevEventTime = currentTime;
|
||||
// console.log('JOipP', `IN [${inIndex++}] [${timeDiff}] ${getFormattedTime()}`, { type, body });
|
||||
|
||||
if (!eventTickRef.current || !visibleRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventsBufferRef.current.push({ type, body });
|
||||
eventTickRef.current();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible && !wasHiddenOnce.current) {
|
||||
wasHiddenOnce.current = true;
|
||||
|
||||
@@ -3,3 +3,4 @@ export * from './getQueryVariable';
|
||||
export * from './loadTextFile';
|
||||
export * from './saveToFile';
|
||||
export * from './omit';
|
||||
export * from './jsonToUriBase64';
|
||||
|
||||
26
assets/js/hooks/Mapper/utils/jsonToUriBase64.ts
Normal file
26
assets/js/hooks/Mapper/utils/jsonToUriBase64.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export const encodeJsonToUriBase64 = (value: unknown): string => {
|
||||
const json = JSON.stringify(value);
|
||||
const uriEncoded = encodeURIComponent(json);
|
||||
|
||||
if (typeof window !== 'undefined' && typeof window.btoa === 'function') {
|
||||
return window.btoa(uriEncoded);
|
||||
}
|
||||
// Node.js
|
||||
// @ts-ignore
|
||||
return Buffer.from(uriEncoded, 'utf8').toString('base64');
|
||||
};
|
||||
|
||||
export const decodeUriBase64ToJson = <T = unknown>(base64: string): T => {
|
||||
let uriEncoded: string;
|
||||
|
||||
if (typeof window !== 'undefined' && typeof window.atob === 'function') {
|
||||
uriEncoded = window.atob(base64);
|
||||
} else {
|
||||
// Node.js
|
||||
// @ts-ignore
|
||||
uriEncoded = Buffer.from(base64, 'base64').toString('utf8');
|
||||
}
|
||||
|
||||
const json = decodeURIComponent(uriEncoded);
|
||||
return JSON.parse(json) as T;
|
||||
};
|
||||
@@ -12,11 +12,11 @@ const animateBg = function (bgCanvas) {
|
||||
*/
|
||||
const randomInRange = (max, min) => Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
const BASE_SIZE = 1;
|
||||
const VELOCITY_INC = 1.01;
|
||||
const VELOCITY_INC = 1.002;
|
||||
const VELOCITY_INIT_INC = 0.525;
|
||||
const JUMP_VELOCITY_INC = 0.55;
|
||||
const JUMP_SIZE_INC = 1.15;
|
||||
const SIZE_INC = 1.01;
|
||||
const SIZE_INC = 1.002;
|
||||
const RAD = Math.PI / 180;
|
||||
const WARP_COLORS = [
|
||||
[197, 239, 247],
|
||||
|
||||
BIN
assets/static/images/eo_pp.png
Normal file
BIN
assets/static/images/eo_pp.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -25,13 +25,9 @@ config :wanderer_app,
|
||||
ecto_repos: [WandererApp.Repo],
|
||||
ash_domains: [WandererApp.Api],
|
||||
generators: [timestamp_type: :utc_datetime],
|
||||
ddrt: DDRT,
|
||||
ddrt: WandererApp.Map.CacheRTree,
|
||||
logger: Logger,
|
||||
pubsub_client: Phoenix.PubSub,
|
||||
wanderer_kills_base_url:
|
||||
System.get_env("WANDERER_KILLS_BASE_URL", "ws://host.docker.internal:4004"),
|
||||
wanderer_kills_service_enabled:
|
||||
System.get_env("WANDERER_KILLS_SERVICE_ENABLED", "false") == "true"
|
||||
pubsub_client: Phoenix.PubSub
|
||||
|
||||
config :wanderer_app, WandererAppWeb.Endpoint,
|
||||
adapter: Bandit.PhoenixAdapter,
|
||||
|
||||
@@ -4,7 +4,7 @@ import Config
|
||||
config :wanderer_app, WandererApp.Repo,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
hostname: System.get_env("DB_HOST", "localhost"),
|
||||
hostname: "localhost",
|
||||
database: "wanderer_dev",
|
||||
stacktrace: true,
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
|
||||
@@ -177,7 +177,34 @@ config :wanderer_app,
|
||||
],
|
||||
extra_characters_50: map_subscription_extra_characters_50_price,
|
||||
extra_hubs_10: map_subscription_extra_hubs_10_price
|
||||
}
|
||||
},
|
||||
# Finch pool configuration - separate pools for different services
|
||||
# ESI Character Tracking pool - high capacity for bulk character operations
|
||||
# With 30+ TrackerPools × ~100 concurrent tasks, need large pool
|
||||
finch_esi_character_pool_size:
|
||||
System.get_env("WANDERER_FINCH_ESI_CHARACTER_POOL_SIZE", "200") |> String.to_integer(),
|
||||
finch_esi_character_pool_count:
|
||||
System.get_env("WANDERER_FINCH_ESI_CHARACTER_POOL_COUNT", "4") |> String.to_integer(),
|
||||
# ESI General pool - standard capacity for general ESI operations
|
||||
finch_esi_general_pool_size:
|
||||
System.get_env("WANDERER_FINCH_ESI_GENERAL_POOL_SIZE", "50") |> String.to_integer(),
|
||||
finch_esi_general_pool_count:
|
||||
System.get_env("WANDERER_FINCH_ESI_GENERAL_POOL_COUNT", "4") |> String.to_integer(),
|
||||
# Webhooks pool - isolated from ESI rate limits
|
||||
finch_webhooks_pool_size:
|
||||
System.get_env("WANDERER_FINCH_WEBHOOKS_POOL_SIZE", "25") |> String.to_integer(),
|
||||
finch_webhooks_pool_count:
|
||||
System.get_env("WANDERER_FINCH_WEBHOOKS_POOL_COUNT", "2") |> String.to_integer(),
|
||||
# Default pool - everything else (email, license manager, etc.)
|
||||
finch_default_pool_size:
|
||||
System.get_env("WANDERER_FINCH_DEFAULT_POOL_SIZE", "25") |> String.to_integer(),
|
||||
finch_default_pool_count:
|
||||
System.get_env("WANDERER_FINCH_DEFAULT_POOL_COUNT", "2") |> String.to_integer(),
|
||||
# Character tracker concurrency settings
|
||||
# Location updates need high concurrency for <2s response with 3000+ characters
|
||||
location_concurrency:
|
||||
System.get_env("WANDERER_LOCATION_CONCURRENCY", "#{System.schedulers_online() * 12}")
|
||||
|> String.to_integer()
|
||||
|
||||
config :ueberauth, Ueberauth,
|
||||
providers: [
|
||||
@@ -258,7 +285,9 @@ config :wanderer_app, WandererApp.Scheduler,
|
||||
timezone: :utc,
|
||||
jobs:
|
||||
[
|
||||
{"@daily", {WandererApp.Map.Audit, :archive, []}}
|
||||
{"@daily", {WandererApp.Map.Audit, :archive, []}},
|
||||
{"@daily", {WandererApp.Map.GarbageCollector, :cleanup_chain_passages, []}},
|
||||
{"@daily", {WandererApp.Map.GarbageCollector, :cleanup_system_signatures, []}}
|
||||
] ++ sheduler_jobs,
|
||||
timeout: :infinity
|
||||
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
defmodule WandererApp.Api.Changes.SlugifyName do
|
||||
@moduledoc """
|
||||
Ensures map slugs are unique by:
|
||||
1. Slugifying the provided slug/name
|
||||
2. Checking for existing slugs (optimization)
|
||||
3. Finding next available slug with numeric suffix if needed
|
||||
4. Relying on database unique constraint as final arbiter
|
||||
|
||||
Race Condition Mitigation:
|
||||
- Optimistic check reduces DB roundtrips for most cases
|
||||
- Database unique index ensures no duplicates slip through
|
||||
- Proper error messages for constraint violations
|
||||
- Telemetry events for monitoring conflicts
|
||||
"""
|
||||
use Ash.Resource.Change
|
||||
|
||||
alias Ash.Changeset
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
# Maximum number of attempts to find a unique slug
|
||||
@max_attempts 100
|
||||
|
||||
@impl true
|
||||
@spec change(Changeset.t(), keyword, Change.context()) :: Changeset.t()
|
||||
@@ -12,10 +30,95 @@ defmodule WandererApp.Api.Changes.SlugifyName do
|
||||
defp maybe_slugify_name(changeset) do
|
||||
case Changeset.get_attribute(changeset, :slug) do
|
||||
slug when is_binary(slug) ->
|
||||
Changeset.force_change_attribute(changeset, :slug, Slug.slugify(slug))
|
||||
base_slug = Slug.slugify(slug)
|
||||
unique_slug = ensure_unique_slug(changeset, base_slug)
|
||||
Changeset.force_change_attribute(changeset, :slug, unique_slug)
|
||||
|
||||
_ ->
|
||||
changeset
|
||||
end
|
||||
end
|
||||
|
||||
defp ensure_unique_slug(changeset, base_slug) do
|
||||
# Get the current record ID if this is an update operation
|
||||
current_id = Changeset.get_attribute(changeset, :id)
|
||||
|
||||
# Check if the base slug is available (optimization to avoid numeric suffixes when possible)
|
||||
if slug_available?(base_slug, current_id) do
|
||||
base_slug
|
||||
else
|
||||
# Find the next available slug with a numeric suffix
|
||||
find_available_slug(base_slug, current_id, 2)
|
||||
end
|
||||
end
|
||||
|
||||
defp find_available_slug(base_slug, current_id, n) when n <= @max_attempts do
|
||||
candidate_slug = "#{base_slug}-#{n}"
|
||||
|
||||
if slug_available?(candidate_slug, current_id) do
|
||||
# Emit telemetry when we had to use a suffix (indicates potential conflict)
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :slug_suffix_used],
|
||||
%{suffix_number: n},
|
||||
%{base_slug: base_slug, final_slug: candidate_slug}
|
||||
)
|
||||
|
||||
candidate_slug
|
||||
else
|
||||
find_available_slug(base_slug, current_id, n + 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp find_available_slug(base_slug, _current_id, n) when n > @max_attempts do
|
||||
# Fallback: use timestamp suffix if we've tried too many numeric suffixes
|
||||
# This handles edge cases where many maps have similar names
|
||||
timestamp = System.system_time(:millisecond)
|
||||
fallback_slug = "#{base_slug}-#{timestamp}"
|
||||
|
||||
Logger.warning(
|
||||
"Slug generation exceeded #{@max_attempts} attempts for '#{base_slug}', using timestamp fallback",
|
||||
base_slug: base_slug,
|
||||
fallback_slug: fallback_slug
|
||||
)
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :map, :slug_fallback_used],
|
||||
%{attempts: n},
|
||||
%{base_slug: base_slug, fallback_slug: fallback_slug}
|
||||
)
|
||||
|
||||
fallback_slug
|
||||
end
|
||||
|
||||
defp slug_available?(slug, current_id) do
|
||||
query =
|
||||
WandererApp.Api.Map
|
||||
|> Ash.Query.filter(slug == ^slug)
|
||||
|> then(fn query ->
|
||||
# Exclude the current record if this is an update
|
||||
if current_id do
|
||||
Ash.Query.filter(query, id != ^current_id)
|
||||
else
|
||||
query
|
||||
end
|
||||
end)
|
||||
|> Ash.Query.limit(1)
|
||||
|
||||
case Ash.read(query) do
|
||||
{:ok, []} ->
|
||||
true
|
||||
|
||||
{:ok, _existing} ->
|
||||
false
|
||||
|
||||
{:error, error} ->
|
||||
# Log error but be conservative - assume slug is not available
|
||||
Logger.warning("Error checking slug availability",
|
||||
slug: slug,
|
||||
error: inspect(error)
|
||||
)
|
||||
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,14 +30,14 @@ defmodule WandererApp.Api.Map do
|
||||
# Routes configuration
|
||||
routes do
|
||||
base("/maps")
|
||||
get(:read)
|
||||
index :read
|
||||
get(:by_slug, route: "/:slug")
|
||||
# index :read
|
||||
post(:new)
|
||||
patch(:update)
|
||||
delete(:destroy)
|
||||
|
||||
# Custom action for map duplication
|
||||
post(:duplicate, route: "/:id/duplicate")
|
||||
# post(:duplicate, route: "/:id/duplicate")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@ defmodule WandererApp.Api.MapConnection do
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_chain_v1")
|
||||
|
||||
custom_indexes do
|
||||
# Critical index for list_connections query performance
|
||||
index [:map_id], name: "map_chain_v1_map_id_index"
|
||||
end
|
||||
end
|
||||
|
||||
json_api do
|
||||
|
||||
@@ -65,7 +65,7 @@ defmodule WandererApp.Api.MapSubscription do
|
||||
defaults [:create, :read, :update, :destroy]
|
||||
|
||||
read :all_active do
|
||||
prepare build(sort: [updated_at: :asc])
|
||||
prepare build(sort: [updated_at: :asc], load: [:map])
|
||||
|
||||
filter(expr(status == :active))
|
||||
end
|
||||
|
||||
@@ -1,6 +1,26 @@
|
||||
defmodule WandererApp.Api.MapSystem do
|
||||
@moduledoc false
|
||||
|
||||
@derive {Jason.Encoder,
|
||||
only: [
|
||||
:id,
|
||||
:map_id,
|
||||
:name,
|
||||
:solar_system_id,
|
||||
:position_x,
|
||||
:position_y,
|
||||
:status,
|
||||
:visible,
|
||||
:locked,
|
||||
:custom_name,
|
||||
:description,
|
||||
:tag,
|
||||
:temporary_name,
|
||||
:labels,
|
||||
:added_at,
|
||||
:linked_sig_eve_id
|
||||
]}
|
||||
|
||||
use Ash.Resource,
|
||||
domain: WandererApp.Api,
|
||||
data_layer: AshPostgres.DataLayer,
|
||||
@@ -9,6 +29,11 @@ defmodule WandererApp.Api.MapSystem do
|
||||
postgres do
|
||||
repo(WandererApp.Repo)
|
||||
table("map_system_v1")
|
||||
|
||||
custom_indexes do
|
||||
# Partial index for efficient visible systems query
|
||||
index [:map_id], where: "visible = true", name: "map_system_v1_map_id_visible_index"
|
||||
end
|
||||
end
|
||||
|
||||
json_api do
|
||||
@@ -16,6 +41,17 @@ defmodule WandererApp.Api.MapSystem do
|
||||
|
||||
includes([:map])
|
||||
|
||||
default_fields([
|
||||
:name,
|
||||
:solar_system_id,
|
||||
:status,
|
||||
:custom_name,
|
||||
:description,
|
||||
:tag,
|
||||
:temporary_name,
|
||||
:labels
|
||||
])
|
||||
|
||||
derive_filter?(true)
|
||||
derive_sort?(true)
|
||||
|
||||
@@ -31,6 +67,7 @@ defmodule WandererApp.Api.MapSystem do
|
||||
|
||||
code_interface do
|
||||
define(:create, action: :create)
|
||||
define(:upsert, action: :upsert)
|
||||
define(:destroy, action: :destroy)
|
||||
|
||||
define(:by_id,
|
||||
@@ -93,6 +130,31 @@ defmodule WandererApp.Api.MapSystem do
|
||||
|
||||
defaults [:create, :update, :destroy]
|
||||
|
||||
create :upsert do
|
||||
primary? false
|
||||
upsert? true
|
||||
upsert_identity :map_solar_system_id
|
||||
|
||||
# Update these fields on conflict
|
||||
upsert_fields [
|
||||
:position_x,
|
||||
:position_y,
|
||||
:visible,
|
||||
:name
|
||||
]
|
||||
|
||||
accept [
|
||||
:map_id,
|
||||
:solar_system_id,
|
||||
:name,
|
||||
:position_x,
|
||||
:position_y,
|
||||
:visible,
|
||||
:locked,
|
||||
:status
|
||||
]
|
||||
end
|
||||
|
||||
read :read do
|
||||
primary?(true)
|
||||
|
||||
|
||||
@@ -16,15 +16,48 @@ defmodule WandererApp.Application do
|
||||
WandererApp.Vault,
|
||||
WandererApp.Repo,
|
||||
{Phoenix.PubSub, name: WandererApp.PubSub, adapter_name: Phoenix.PubSub.PG2},
|
||||
# Multiple Finch pools for different services to prevent connection pool exhaustion
|
||||
# ESI Character Tracking pool - high capacity for bulk character operations
|
||||
{
|
||||
Finch,
|
||||
name: WandererApp.Finch.ESI.CharacterTracking,
|
||||
pools: %{
|
||||
default: [
|
||||
size: Application.get_env(:wanderer_app, :finch_esi_character_pool_size, 100),
|
||||
count: Application.get_env(:wanderer_app, :finch_esi_character_pool_count, 4)
|
||||
]
|
||||
}
|
||||
},
|
||||
# ESI General pool - standard capacity for general ESI operations
|
||||
{
|
||||
Finch,
|
||||
name: WandererApp.Finch.ESI.General,
|
||||
pools: %{
|
||||
default: [
|
||||
size: Application.get_env(:wanderer_app, :finch_esi_general_pool_size, 50),
|
||||
count: Application.get_env(:wanderer_app, :finch_esi_general_pool_count, 4)
|
||||
]
|
||||
}
|
||||
},
|
||||
# Webhooks pool - isolated from ESI rate limits
|
||||
{
|
||||
Finch,
|
||||
name: WandererApp.Finch.Webhooks,
|
||||
pools: %{
|
||||
default: [
|
||||
size: Application.get_env(:wanderer_app, :finch_webhooks_pool_size, 25),
|
||||
count: Application.get_env(:wanderer_app, :finch_webhooks_pool_count, 2)
|
||||
]
|
||||
}
|
||||
},
|
||||
# Default pool - everything else (email, license manager, etc.)
|
||||
{
|
||||
Finch,
|
||||
name: WandererApp.Finch,
|
||||
pools: %{
|
||||
default: [
|
||||
# number of connections per pool
|
||||
size: 50,
|
||||
# number of pools (so total 50 connections)
|
||||
count: 4
|
||||
size: Application.get_env(:wanderer_app, :finch_default_pool_size, 25),
|
||||
count: Application.get_env(:wanderer_app, :finch_default_pool_count, 2)
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -38,7 +71,12 @@ defmodule WandererApp.Application do
|
||||
),
|
||||
Supervisor.child_spec({Cachex, name: :ship_types_cache}, id: :ship_types_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_cache}, id: :character_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :acl_cache}, id: :acl_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :map_cache}, id: :map_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :map_pool_cache},
|
||||
id: :map_pool_cache_worker
|
||||
),
|
||||
Supervisor.child_spec({Cachex, name: :map_state_cache}, id: :map_state_cache_worker),
|
||||
Supervisor.child_spec({Cachex, name: :character_state_cache},
|
||||
id: :character_state_cache_worker
|
||||
),
|
||||
@@ -48,10 +86,7 @@ defmodule WandererApp.Application do
|
||||
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,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
WandererAppWeb.PresenceGracePeriodManager,
|
||||
@@ -78,6 +113,7 @@ defmodule WandererApp.Application do
|
||||
WandererApp.Server.ServerStatusTracker,
|
||||
WandererApp.Server.TheraDataFetcher,
|
||||
{WandererApp.Character.TrackerPoolSupervisor, []},
|
||||
{WandererApp.Map.MapPoolSupervisor, []},
|
||||
WandererApp.Character.TrackerManager,
|
||||
WandererApp.Map.Manager
|
||||
] ++ security_audit_children
|
||||
|
||||
@@ -73,6 +73,54 @@ defmodule WandererApp.Cache do
|
||||
|
||||
def filter_by_attr_in(type, attr, includes), do: type |> get() |> filter_in(attr, includes)
|
||||
|
||||
@doc """
|
||||
Batch lookup multiple keys from cache.
|
||||
Returns a map of key => value pairs, with `default` used for missing keys.
|
||||
"""
|
||||
def lookup_all(keys, default \\ nil) when is_list(keys) do
|
||||
# Get all values from cache
|
||||
values = get_all(keys)
|
||||
|
||||
# Build result map with defaults for missing keys
|
||||
result =
|
||||
keys
|
||||
|> Enum.map(fn key ->
|
||||
value = Map.get(values, key, default)
|
||||
{key, value}
|
||||
end)
|
||||
|> Map.new()
|
||||
|
||||
{:ok, result}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Batch insert multiple key-value pairs into cache.
|
||||
Accepts a map of key => value pairs or a list of {key, value} tuples.
|
||||
Skips nil values (deletes the key instead).
|
||||
"""
|
||||
def insert_all(entries, opts \\ [])
|
||||
|
||||
def insert_all(entries, opts) when is_map(entries) do
|
||||
# Filter out nil values and delete those keys
|
||||
{to_delete, to_insert} =
|
||||
entries
|
||||
|> Enum.split_with(fn {_key, value} -> is_nil(value) end)
|
||||
|
||||
# Delete keys with nil values
|
||||
Enum.each(to_delete, fn {key, _} -> delete(key) end)
|
||||
|
||||
# Insert non-nil values
|
||||
unless Enum.empty?(to_insert) do
|
||||
put_all(to_insert, opts)
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def insert_all(entries, opts) when is_list(entries) do
|
||||
insert_all(Map.new(entries), opts)
|
||||
end
|
||||
|
||||
defp find(list, %{} = attrs, match: match) do
|
||||
list
|
||||
|> Enum.find(fn item ->
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
defmodule WandererApp.CachedInfo do
|
||||
require Logger
|
||||
|
||||
alias WandererAppWeb.Helpers.APIUtils
|
||||
|
||||
def run(_arg) do
|
||||
:ok = cache_trig_systems()
|
||||
end
|
||||
@@ -29,14 +31,71 @@ defmodule WandererApp.CachedInfo do
|
||||
)
|
||||
end)
|
||||
|
||||
Cachex.get(:ship_types_cache, type_id)
|
||||
get_ship_type_from_cache_or_api(type_id)
|
||||
|
||||
{:ok, ship_type} ->
|
||||
{:ok, ship_type}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_ship_type_from_cache_or_api(type_id) do
|
||||
case Cachex.get(:ship_types_cache, type_id) do
|
||||
{:ok, ship_type} when not is_nil(ship_type) ->
|
||||
{:ok, ship_type}
|
||||
|
||||
{:ok, nil} ->
|
||||
case WandererApp.Esi.get_type_info(type_id) do
|
||||
{:ok, info} when not is_nil(info) ->
|
||||
ship_type = parse_type(type_id, info)
|
||||
{:ok, group_info} = get_group_info(ship_type.group_id)
|
||||
|
||||
{:ok, ship_type_info} =
|
||||
WandererApp.Api.ShipTypeInfo |> Ash.create(ship_type |> Map.merge(group_info))
|
||||
|
||||
{:ok,
|
||||
ship_type_info
|
||||
|> Map.take([
|
||||
:type_id,
|
||||
:group_id,
|
||||
:group_name,
|
||||
:name,
|
||||
:description,
|
||||
:mass,
|
||||
:capacity,
|
||||
:volume
|
||||
])}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to get ship_type #{type_id} from ESI: #{inspect(reason)}")
|
||||
{:ok, nil}
|
||||
|
||||
error ->
|
||||
Logger.error("Failed to get ship_type #{type_id} from ESI: #{inspect(error)}")
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_group_info(nil), do: {:ok, nil}
|
||||
|
||||
def get_group_info(group_id) do
|
||||
case WandererApp.Esi.get_group_info(group_id) do
|
||||
{:ok, info} when not is_nil(info) ->
|
||||
{:ok, parse_group(group_id, info)}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to get group_info #{group_id} from ESI: #{inspect(reason)}")
|
||||
{:ok, %{group_name: ""}}
|
||||
|
||||
error ->
|
||||
Logger.error("Failed to get group_info #{group_id} from ESI: #{inspect(error)}")
|
||||
{:ok, %{group_name: ""}}
|
||||
end
|
||||
end
|
||||
|
||||
def get_system_static_info(solar_system_id) do
|
||||
{:ok, solar_system_id} = APIUtils.parse_int(solar_system_id)
|
||||
|
||||
case Cachex.get(:system_static_info_cache, solar_system_id) do
|
||||
{:ok, nil} ->
|
||||
case WandererApp.Api.MapSolarSystem.read() do
|
||||
@@ -116,7 +175,7 @@ defmodule WandererApp.CachedInfo do
|
||||
def get_solar_system_jumps() do
|
||||
case WandererApp.Cache.lookup(:solar_system_jumps) do
|
||||
{:ok, nil} ->
|
||||
data = WandererApp.EveDataService.get_solar_system_jumps_data()
|
||||
{:ok, data} = WandererApp.Api.MapSolarSystemJumps.read()
|
||||
|
||||
cache_items(data, :solar_system_jumps)
|
||||
|
||||
@@ -149,6 +208,25 @@ defmodule WandererApp.CachedInfo do
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_group(group_id, group) do
|
||||
%{
|
||||
group_id: group_id,
|
||||
group_name: Map.get(group, "name")
|
||||
}
|
||||
end
|
||||
|
||||
defp parse_type(type_id, type) do
|
||||
%{
|
||||
type_id: type_id,
|
||||
name: Map.get(type, "name"),
|
||||
description: Map.get(type, "description"),
|
||||
group_id: Map.get(type, "group_id"),
|
||||
mass: "#{Map.get(type, "mass")}",
|
||||
capacity: "#{Map.get(type, "capacity")}",
|
||||
volume: "#{Map.get(type, "volume")}"
|
||||
}
|
||||
end
|
||||
|
||||
defp build_jump_index() do
|
||||
case get_solar_system_jumps() do
|
||||
{:ok, jumps} ->
|
||||
|
||||
@@ -4,6 +4,8 @@ defmodule WandererApp.Character do
|
||||
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Cache
|
||||
|
||||
@read_character_wallet_scope "esi-wallet.read_character_wallet.v1"
|
||||
@read_corp_wallet_scope "esi-wallet.read_corporation_wallets.v1"
|
||||
|
||||
@@ -16,6 +18,9 @@ defmodule WandererApp.Character do
|
||||
ship_item_id: nil
|
||||
}
|
||||
|
||||
@present_on_map_ttl :timer.seconds(10)
|
||||
@not_present_on_map_ttl :timer.minutes(2)
|
||||
|
||||
def get_by_eve_id(character_eve_id) when is_binary(character_eve_id) do
|
||||
WandererApp.Api.Character.by_eve_id(character_eve_id)
|
||||
end
|
||||
@@ -28,7 +33,7 @@ defmodule WandererApp.Character do
|
||||
Cachex.put(:character_cache, character_id, character)
|
||||
{:ok, character}
|
||||
|
||||
error ->
|
||||
_error ->
|
||||
{:error, :not_found}
|
||||
end
|
||||
|
||||
@@ -41,7 +46,7 @@ defmodule WandererApp.Character do
|
||||
|
||||
def get_character!(character_id) do
|
||||
case get_character(character_id) do
|
||||
{:ok, character} ->
|
||||
{:ok, character} when not is_nil(character) ->
|
||||
character
|
||||
|
||||
_ ->
|
||||
@@ -50,16 +55,10 @@ defmodule WandererApp.Character do
|
||||
end
|
||||
end
|
||||
|
||||
def get_map_character(map_id, character_id, opts \\ []) do
|
||||
def get_map_character(map_id, character_id) do
|
||||
case get_character(character_id) do
|
||||
{:ok, character} ->
|
||||
# If we are forcing the character to not be present, we merge the character state with map settings
|
||||
character_is_present =
|
||||
if opts |> Keyword.get(:not_present, false) do
|
||||
false
|
||||
else
|
||||
WandererApp.Character.TrackerManager.Impl.character_is_present(map_id, character_id)
|
||||
end
|
||||
{:ok, character} when not is_nil(character) ->
|
||||
character_is_present = character_is_present?(map_id, character_id)
|
||||
|
||||
{:ok,
|
||||
character
|
||||
@@ -187,12 +186,16 @@ defmodule WandererApp.Character do
|
||||
{:ok, result} ->
|
||||
{:ok, result |> prepare_search_results()}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
|
||||
{:ok, []}
|
||||
|
||||
error ->
|
||||
Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
error ->
|
||||
_error ->
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
@@ -263,22 +266,26 @@ defmodule WandererApp.Character do
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "character-present-#{map_id}-#{character_id}",
|
||||
opts: [ttl: @present_on_map_ttl]
|
||||
)
|
||||
defp character_is_present?(map_id, character_id),
|
||||
do: WandererApp.Character.TrackerManager.Impl.character_is_present(map_id, character_id)
|
||||
|
||||
character
|
||||
|> Map.merge(%{tracking_paused: tracking_paused})
|
||||
end
|
||||
defp maybe_merge_map_character_settings(character, _map_id, true), do: character
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "not-present-map-character-#{map_id}-#{character_id}",
|
||||
opts: [ttl: @not_present_on_map_ttl]
|
||||
)
|
||||
defp maybe_merge_map_character_settings(
|
||||
%{id: character_id} = character,
|
||||
map_id,
|
||||
_character_is_present
|
||||
false
|
||||
) do
|
||||
{:ok, tracking_paused} =
|
||||
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
|
||||
|
||||
WandererApp.MapCharacterSettingsRepo.get(map_id, character_id)
|
||||
|> case do
|
||||
{:ok, settings} when not is_nil(settings) ->
|
||||
@@ -296,7 +303,7 @@ defmodule WandererApp.Character do
|
||||
character
|
||||
|> Map.merge(@default_character_tracking_data)
|
||||
end
|
||||
|> Map.merge(%{online: false, tracking_paused: tracking_paused})
|
||||
|> Map.merge(%{online: false})
|
||||
end
|
||||
|
||||
defp prepare_search_results(result) do
|
||||
@@ -324,7 +331,7 @@ defmodule WandererApp.Character do
|
||||
do:
|
||||
{:ok,
|
||||
Enum.map(eve_ids, fn eve_id ->
|
||||
Task.async(fn -> apply(WandererApp.Esi.ApiClient, method, [eve_id]) end)
|
||||
Task.async(fn -> apply(WandererApp.Esi, method, [eve_id]) end)
|
||||
end)
|
||||
# 145000 == Timeout in milliseconds
|
||||
|> Enum.map(fn task -> Task.await(task, 145_000) end)
|
||||
|
||||
@@ -14,8 +14,8 @@ defmodule WandererApp.Character.Tracker do
|
||||
active_maps: [],
|
||||
is_online: false,
|
||||
track_online: true,
|
||||
track_location: true,
|
||||
track_ship: true,
|
||||
track_location: false,
|
||||
track_ship: false,
|
||||
track_wallet: false,
|
||||
status: "new"
|
||||
]
|
||||
@@ -36,14 +36,11 @@ defmodule WandererApp.Character.Tracker do
|
||||
status: binary()
|
||||
}
|
||||
|
||||
@pause_tracking_timeout :timer.minutes(60 * 10)
|
||||
@offline_timeout :timer.minutes(5)
|
||||
@online_error_timeout :timer.minutes(10)
|
||||
@ship_error_timeout :timer.minutes(10)
|
||||
@location_error_timeout :timer.minutes(10)
|
||||
@location_error_timeout :timer.seconds(30)
|
||||
@location_error_threshold 3
|
||||
@online_forbidden_ttl :timer.seconds(7)
|
||||
@offline_check_delay_ttl :timer.seconds(15)
|
||||
@online_limit_ttl :timer.seconds(7)
|
||||
@forbidden_ttl :timer.seconds(10)
|
||||
@limit_ttl :timer.seconds(5)
|
||||
@location_limit_ttl :timer.seconds(1)
|
||||
@@ -93,81 +90,16 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
end
|
||||
|
||||
def check_online_errors(character_id),
|
||||
do: check_tracking_errors(character_id, "online", @online_error_timeout)
|
||||
|
||||
def check_ship_errors(character_id),
|
||||
do: check_tracking_errors(character_id, "ship", @ship_error_timeout)
|
||||
|
||||
def check_location_errors(character_id),
|
||||
do: check_tracking_errors(character_id, "location", @location_error_timeout)
|
||||
|
||||
defp check_tracking_errors(character_id, type, timeout) do
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:#{type}_error_time")
|
||||
|> case do
|
||||
nil ->
|
||||
:skip
|
||||
|
||||
error_time ->
|
||||
duration = DateTime.diff(DateTime.utc_now(), error_time, :millisecond)
|
||||
|
||||
if duration >= timeout do
|
||||
pause_tracking(character_id)
|
||||
WandererApp.Cache.delete("character:#{character_id}:#{type}_error_time")
|
||||
|
||||
:ok
|
||||
else
|
||||
:skip
|
||||
end
|
||||
end
|
||||
defp increment_location_error_count(character_id) do
|
||||
cache_key = "character:#{character_id}:location_error_count"
|
||||
current_count = WandererApp.Cache.lookup!(cache_key) || 0
|
||||
new_count = current_count + 1
|
||||
WandererApp.Cache.put(cache_key, new_count)
|
||||
new_count
|
||||
end
|
||||
|
||||
defp pause_tracking(character_id) 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
|
||||
Logger.debug(fn ->
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(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))}"
|
||||
end)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
# Original log kept for backward compatibility
|
||||
Logger.warning("[CharacterTracker] paused for #{character_id}")
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:tracking_paused",
|
||||
true,
|
||||
ttl: @pause_tracking_timeout
|
||||
)
|
||||
|
||||
{:ok, %{solar_system_id: solar_system_id}} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
{:ok, %{active_maps: active_maps}} =
|
||||
WandererApp.Character.get_character_state(character_id)
|
||||
|
||||
active_maps
|
||||
|> Enum.each(fn map_id ->
|
||||
WandererApp.Cache.put(
|
||||
"map:#{map_id}:character:#{character_id}:start_solar_system_id",
|
||||
solar_system_id
|
||||
)
|
||||
end)
|
||||
end
|
||||
defp reset_location_error_count(character_id) do
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_count")
|
||||
end
|
||||
|
||||
def update_settings(character_id, track_settings) do
|
||||
@@ -194,8 +126,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
|
||||
when not is_nil(access_token) ->
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden")
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
@@ -224,9 +155,10 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
end
|
||||
|
||||
if online.online == true && online.online != is_online do
|
||||
if online.online == true && not is_online do
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_count")
|
||||
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
|
||||
@@ -294,12 +226,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
reset_seconds =
|
||||
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
|
||||
remaining =
|
||||
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
@@ -357,8 +283,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
defp get_reset_timeout(_headers, default_timeout), do: default_timeout
|
||||
|
||||
def update_info(character_id) do
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:info_forbidden")
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
@@ -442,8 +367,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
|
||||
when not is_nil(access_token) ->
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:ship_forbidden"))
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
@@ -552,7 +476,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
|
||||
when not is_nil(access_token) ->
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused")
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:location_forbidden")
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
@@ -565,19 +489,33 @@ defmodule WandererApp.Character.Tracker do
|
||||
character_id: character_id
|
||||
) do
|
||||
{:ok, location} when is_map(location) and not is_struct(location) ->
|
||||
reset_location_error_count(character_id)
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
|
||||
|
||||
character_state
|
||||
|> maybe_update_location(location)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
error_count = increment_location_error_count(character_id)
|
||||
|
||||
Logger.warning("ESI_ERROR: Character location tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
error_count: error_count,
|
||||
endpoint: "character_location"
|
||||
)
|
||||
|
||||
if error_count >= @location_error_threshold do
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:location_forbidden",
|
||||
true,
|
||||
ttl: @location_error_timeout
|
||||
)
|
||||
end
|
||||
|
||||
if is_nil(
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
|
||||
) do
|
||||
@@ -601,13 +539,24 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
error_count = increment_location_error_count(character_id)
|
||||
|
||||
Logger.error("ESI_ERROR: Character location tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
error_count: error_count,
|
||||
endpoint: "character_location"
|
||||
)
|
||||
|
||||
if error_count >= @location_error_threshold do
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:location_forbidden",
|
||||
true,
|
||||
ttl: @location_error_timeout
|
||||
)
|
||||
end
|
||||
|
||||
if is_nil(
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
|
||||
) do
|
||||
@@ -620,13 +569,24 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
_ ->
|
||||
error_count = increment_location_error_count(character_id)
|
||||
|
||||
Logger.error("ESI_ERROR: Character location tracking failed - wrong response",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: "wrong_response",
|
||||
error_count: error_count,
|
||||
endpoint: "character_location"
|
||||
)
|
||||
|
||||
if error_count >= @location_error_threshold do
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:location_forbidden",
|
||||
true,
|
||||
ttl: @location_error_timeout
|
||||
)
|
||||
end
|
||||
|
||||
if is_nil(
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:location_error_time")
|
||||
) do
|
||||
@@ -662,8 +622,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> case do
|
||||
true ->
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:wallet_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:wallet_forbidden"))
|
||||
|> case do
|
||||
true ->
|
||||
{:error, :skipped}
|
||||
@@ -750,6 +709,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
end
|
||||
|
||||
# when old_alliance_id != alliance_id and is_nil(alliance_id)
|
||||
defp maybe_update_alliance(
|
||||
%{character_id: character_id, alliance_id: old_alliance_id} = state,
|
||||
alliance_id
|
||||
@@ -775,6 +735,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: nil})
|
||||
end
|
||||
|
||||
defp maybe_update_alliance(
|
||||
@@ -782,8 +743,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
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"))
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden")
|
||||
|> case do
|
||||
true ->
|
||||
state
|
||||
@@ -813,6 +773,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: alliance_id})
|
||||
|
||||
_error ->
|
||||
Logger.error("Failed to get alliance info for #{alliance_id}")
|
||||
@@ -829,8 +790,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
when old_corporation_id != corporation_id do
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:corporation_info_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:corporation_info_forbidden"))
|
||||
|> case do
|
||||
true ->
|
||||
state
|
||||
@@ -1006,9 +966,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
),
|
||||
do: %{
|
||||
state
|
||||
| track_online: true,
|
||||
track_location: true,
|
||||
track_ship: true
|
||||
| track_online: true
|
||||
}
|
||||
|
||||
defp maybe_start_online_tracking(
|
||||
@@ -1052,11 +1010,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
DateTime.utc_now()
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"map:#{map_id}:character:#{character_id}:start_solar_system_id",
|
||||
track_settings |> Map.get(:solar_system_id)
|
||||
)
|
||||
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
|
||||
@@ -1107,7 +1060,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
end
|
||||
|
||||
state
|
||||
%{state | track_location: false, track_ship: false}
|
||||
end
|
||||
|
||||
defp maybe_stop_tracking(
|
||||
@@ -1137,19 +1090,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
defp get_online(_), do: %{online: false}
|
||||
|
||||
defp get_tracking_duration_minutes(character_id) do
|
||||
case WandererApp.Cache.lookup!("character:#{character_id}:map:*:tracking_start_time") do
|
||||
nil ->
|
||||
0
|
||||
|
||||
start_time when is_struct(start_time, DateTime) ->
|
||||
DateTime.diff(DateTime.utc_now(), start_time, :minute)
|
||||
|
||||
_ ->
|
||||
0
|
||||
end
|
||||
end
|
||||
|
||||
# Telemetry handler for database pool monitoring
|
||||
def handle_pool_query(_event_name, measurements, metadata, _config) do
|
||||
queue_time = measurements[:queue_time]
|
||||
|
||||
@@ -14,8 +14,8 @@ defmodule WandererApp.Character.TrackerManager do
|
||||
GenServer.start_link(__MODULE__, args, name: __MODULE__)
|
||||
end
|
||||
|
||||
def start_tracking(character_id, opts \\ []),
|
||||
do: GenServer.cast(__MODULE__, {&Impl.start_tracking/3, [character_id, opts]})
|
||||
def start_tracking(character_id),
|
||||
do: GenServer.cast(__MODULE__, {&Impl.start_tracking/2, [character_id]})
|
||||
|
||||
def stop_tracking(character_id),
|
||||
do: GenServer.cast(__MODULE__, {&Impl.stop_tracking/2, [character_id]})
|
||||
|
||||
@@ -40,13 +40,13 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
|
||||
tracked_characters
|
||||
|> Enum.each(fn character_id ->
|
||||
start_tracking(state, character_id, %{})
|
||||
start_tracking(state, character_id)
|
||||
end)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def start_tracking(state, character_id, opts) do
|
||||
def start_tracking(state, character_id) do
|
||||
if not WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
||||
WandererApp.Cache.insert(
|
||||
"#{character_id}:track_requested",
|
||||
|
||||
@@ -8,7 +8,8 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
:tracked_ids,
|
||||
:uuid,
|
||||
:characters,
|
||||
server_online: true
|
||||
server_online: false,
|
||||
last_location_duration: 0
|
||||
]
|
||||
|
||||
@name __MODULE__
|
||||
@@ -17,15 +18,21 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
@unique_registry :unique_tracker_pool_registry
|
||||
|
||||
@update_location_interval :timer.seconds(1)
|
||||
@update_online_interval :timer.seconds(5)
|
||||
@update_online_interval :timer.seconds(30)
|
||||
@check_offline_characters_interval :timer.minutes(5)
|
||||
@check_online_errors_interval :timer.minutes(1)
|
||||
@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(2)
|
||||
@update_wallet_interval :timer.minutes(10)
|
||||
|
||||
# Per-operation concurrency limits
|
||||
# Location updates are critical and need high concurrency (100 chars in ~200ms)
|
||||
# Note: This is fetched at runtime since it's configured via runtime.exs
|
||||
defp location_concurrency do
|
||||
Application.get_env(:wanderer_app, :location_concurrency, System.schedulers_online() * 12)
|
||||
end
|
||||
# Other operations can use lower concurrency
|
||||
@standard_concurrency System.schedulers_online() * 2
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
|
||||
def new(), do: __struct__()
|
||||
@@ -46,10 +53,6 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
{:ok, _} = Registry.register(@unique_registry, Module.concat(__MODULE__, uuid), tracked_ids)
|
||||
{:ok, _} = Registry.register(@registry, __MODULE__, uuid)
|
||||
|
||||
# Cachex.get_and_update(@cache, :tracked_characters, fn ids ->
|
||||
# {:commit, ids ++ tracked_ids}
|
||||
# end)
|
||||
|
||||
tracked_ids
|
||||
|> Enum.each(fn id ->
|
||||
Cachex.put(@cache, id, uuid)
|
||||
@@ -79,9 +82,6 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
[tracked_id | r_tracked_ids]
|
||||
end)
|
||||
|
||||
# Cachex.get_and_update(@cache, :tracked_characters, fn ids ->
|
||||
# {:commit, ids ++ [tracked_id]}
|
||||
# end)
|
||||
Cachex.put(@cache, tracked_id, uuid)
|
||||
|
||||
{:noreply, %{state | characters: [tracked_id | characters]}}
|
||||
@@ -96,10 +96,6 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
r_tracked_ids |> Enum.reject(fn id -> id == tracked_id end)
|
||||
end)
|
||||
|
||||
# Cachex.get_and_update(@cache, :tracked_characters, fn ids ->
|
||||
# {:commit, ids |> Enum.reject(fn id -> id == tracked_id end)}
|
||||
# end)
|
||||
#
|
||||
Cachex.del(@cache, tracked_id)
|
||||
|
||||
{:noreply, %{state | characters: characters |> Enum.reject(fn id -> id == tracked_id end)}}
|
||||
@@ -120,17 +116,23 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
"server_status"
|
||||
)
|
||||
|
||||
Process.send_after(self(), :update_online, 100)
|
||||
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))
|
||||
# Stagger pool startups to distribute load across multiple pools
|
||||
# Critical location updates get minimal stagger (0-500ms)
|
||||
# Other operations get wider stagger (0-10s) to reduce thundering herd
|
||||
location_stagger = :rand.uniform(500)
|
||||
online_stagger = :rand.uniform(10_000)
|
||||
ship_stagger = :rand.uniform(10_000)
|
||||
info_stagger = :rand.uniform(60_000)
|
||||
|
||||
Process.send_after(self(), :update_online, 100 + online_stagger)
|
||||
Process.send_after(self(), :update_location, 300 + location_stagger)
|
||||
Process.send_after(self(), :update_ship, 500 + ship_stagger)
|
||||
Process.send_after(self(), :update_info, 1500 + info_stagger)
|
||||
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)
|
||||
|
||||
if WandererApp.Env.wallet_tracking_enabled?() do
|
||||
Process.send_after(self(), :update_wallet, 1000)
|
||||
wallet_stagger = :rand.uniform(120_000)
|
||||
Process.send_after(self(), :update_wallet, 1000 + wallet_stagger)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
@@ -180,7 +182,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_online(character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
max_concurrency: @standard_concurrency,
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
)
|
||||
@@ -191,6 +193,8 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
[Tracker Pool] update_online => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
|
||||
ErrorTracker.report(e, __STACKTRACE__)
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
@@ -241,7 +245,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
WandererApp.Character.Tracker.check_offline(character_id)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
max_concurrency: @standard_concurrency,
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
@@ -259,126 +263,6 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:check_online_errors,
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :check_online_errors, @check_online_errors_interval)
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(
|
||||
WandererApp.Character.Tracker,
|
||||
:check_online_errors,
|
||||
[
|
||||
character_id
|
||||
]
|
||||
)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
error -> @logger.error("Error in check_online_errors: #{inspect(error)}")
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] check_online_errors => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:check_ship_errors,
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :check_ship_errors, @check_ship_errors_interval)
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(
|
||||
WandererApp.Character.Tracker,
|
||||
:check_ship_errors,
|
||||
[
|
||||
character_id
|
||||
]
|
||||
)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
error -> @logger.error("Error in check_ship_errors: #{inspect(error)}")
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] check_ship_errors => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:check_location_errors,
|
||||
%{
|
||||
characters: characters
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :check_location_errors, @check_location_errors_interval)
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(
|
||||
WandererApp.Character.Tracker,
|
||||
:check_location_errors,
|
||||
[
|
||||
character_id
|
||||
]
|
||||
)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
error -> @logger.error("Error in check_location_errors: #{inspect(error)}")
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] check_location_errors => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:update_location,
|
||||
%{
|
||||
@@ -389,26 +273,52 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
) do
|
||||
Process.send_after(self(), :update_location, @update_location_interval)
|
||||
|
||||
start_time = System.monotonic_time(:millisecond)
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_location(character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
max_concurrency: location_concurrency(),
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
|
||||
# Emit telemetry for location update performance
|
||||
duration = System.monotonic_time(:millisecond) - start_time
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :tracker_pool, :location_update],
|
||||
%{duration: duration, character_count: length(characters)},
|
||||
%{pool_uuid: state.uuid}
|
||||
)
|
||||
|
||||
# Warn if location updates are falling behind (taking > 800ms for 100 chars)
|
||||
if duration > 800 do
|
||||
Logger.warning(
|
||||
"[Tracker Pool] Location updates falling behind: #{duration}ms for #{length(characters)} chars (pool: #{state.uuid})"
|
||||
)
|
||||
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :tracker_pool, :location_lag],
|
||||
%{duration: duration, character_count: length(characters)},
|
||||
%{pool_uuid: state.uuid}
|
||||
)
|
||||
end
|
||||
|
||||
{:noreply, %{state | last_location_duration: duration}}
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] update_location => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
@@ -424,32 +334,48 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
:update_ship,
|
||||
%{
|
||||
characters: characters,
|
||||
server_online: true
|
||||
server_online: true,
|
||||
last_location_duration: location_duration
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :update_ship, @update_ship_interval)
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_ship(character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
# Backpressure: Skip ship updates if location updates are falling behind
|
||||
if location_duration > 1000 do
|
||||
Logger.debug(
|
||||
"[Tracker Pool] Skipping ship update due to location lag (#{location_duration}ms)"
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] update_ship => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :tracker_pool, :ship_skipped],
|
||||
%{count: 1},
|
||||
%{pool_uuid: state.uuid, reason: :location_lag}
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
else
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_ship(character_id)
|
||||
end,
|
||||
max_concurrency: @standard_concurrency,
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] update_ship => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
@@ -465,35 +391,51 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
:update_info,
|
||||
%{
|
||||
characters: characters,
|
||||
server_online: true
|
||||
server_online: true,
|
||||
last_location_duration: location_duration
|
||||
} =
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :update_info, @update_info_interval)
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_info(character_id)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
on_timeout: :kill_task
|
||||
# Backpressure: Skip info updates if location updates are severely falling behind
|
||||
if location_duration > 1500 do
|
||||
Logger.debug(
|
||||
"[Tracker Pool] Skipping info update due to location lag (#{location_duration}ms)"
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
error -> Logger.error("Error in update_info: #{inspect(error)}")
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] update_info => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :tracker_pool, :info_skipped],
|
||||
%{count: 1},
|
||||
%{pool_uuid: state.uuid, reason: :location_lag}
|
||||
)
|
||||
|
||||
{:noreply, state}
|
||||
else
|
||||
try do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_info(character_id)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: @standard_concurrency,
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
error -> Logger.error("Error in update_info: #{inspect(error)}")
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Tracker Pool] update_info => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
@@ -522,7 +464,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
WandererApp.Character.Tracker.update_wallet(character_id)
|
||||
end,
|
||||
timeout: :timer.minutes(5),
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
max_concurrency: @standard_concurrency,
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
@@ -581,8 +523,4 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
Logger.debug("Failed to monitor message queue: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp via_tuple(uuid) do
|
||||
{:via, Registry, {@unique_registry, Module.concat(__MODULE__, uuid)}}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -50,14 +50,9 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
|
||||
end
|
||||
end
|
||||
|
||||
def is_not_tracked?(tracked_id) do
|
||||
{:ok, tracked_ids} = Cachex.get(@cache, :tracked_characters)
|
||||
tracked_ids |> Enum.member?(tracked_id) |> Kernel.not()
|
||||
end
|
||||
|
||||
defp get_available_pool([]), do: nil
|
||||
|
||||
defp get_available_pool([{pid, uuid} | pools]) do
|
||||
defp get_available_pool([{_pid, uuid} | pools]) do
|
||||
case Registry.lookup(@unique_registry, Module.concat(WandererApp.Character.TrackerPool, uuid)) do
|
||||
[] ->
|
||||
nil
|
||||
@@ -67,8 +62,8 @@ defmodule WandererApp.Character.TrackerPoolDynamicSupervisor do
|
||||
nil ->
|
||||
get_available_pool(pools)
|
||||
|
||||
pid ->
|
||||
pid
|
||||
pool_pid ->
|
||||
pool_pid
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -173,12 +173,11 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
%{
|
||||
id: character_id,
|
||||
eve_id: eve_id
|
||||
},
|
||||
} = _character,
|
||||
map_id,
|
||||
is_track_allowed,
|
||||
caller_pid
|
||||
)
|
||||
when not is_nil(caller_pid) do
|
||||
) do
|
||||
WandererAppWeb.Presence.update(caller_pid, map_id, character_id, %{
|
||||
tracked: is_track_allowed,
|
||||
from: DateTime.utc_now()
|
||||
@@ -217,13 +216,16 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
|
||||
defp track_character(
|
||||
_character,
|
||||
character,
|
||||
_map_id,
|
||||
_is_track_allowed,
|
||||
_caller_pid
|
||||
) do
|
||||
Logger.error("caller_pid is required for tracking characters")
|
||||
{:error, "caller_pid is required"}
|
||||
Logger.error(
|
||||
"Invalid character data for tracking - character must have :id and :eve_id fields, got: #{inspect(character)}"
|
||||
)
|
||||
|
||||
{:error, "Invalid character data"}
|
||||
end
|
||||
|
||||
def untrack(characters, map_id, caller_pid) do
|
||||
@@ -238,30 +240,14 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
})
|
||||
end)
|
||||
|
||||
# WandererApp.Map.Server.untrack_characters(map_id, character_ids)
|
||||
|
||||
:ok
|
||||
else
|
||||
true ->
|
||||
Logger.error("caller_pid is required for untracking characters")
|
||||
Logger.error("caller_pid is required for untracking characters 2")
|
||||
{:error, "caller_pid is required"}
|
||||
end
|
||||
end
|
||||
|
||||
# def add_characters([], _map_id, _track_character), do: :ok
|
||||
|
||||
# def add_characters([character | characters], map_id, track_character) do
|
||||
# :ok = WandererApp.Map.Server.add_character(map_id, character, track_character)
|
||||
# add_characters(characters, map_id, track_character)
|
||||
# end
|
||||
|
||||
# def remove_characters([], _map_id), do: :ok
|
||||
|
||||
# def remove_characters([character | characters], map_id) do
|
||||
# :ok = WandererApp.Map.Server.remove_character(map_id, character.id)
|
||||
# remove_characters(characters, map_id)
|
||||
# end
|
||||
|
||||
def get_main_character(
|
||||
nil,
|
||||
current_user_characters,
|
||||
|
||||
@@ -12,7 +12,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
total_balance: 0,
|
||||
transactions: [],
|
||||
retries: 5,
|
||||
server_online: true,
|
||||
server_online: false,
|
||||
status: :started
|
||||
]
|
||||
|
||||
@@ -75,7 +75,7 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
|
||||
def handle_event(
|
||||
:update_corp_wallets,
|
||||
%{character: character} = state
|
||||
%{character: character, server_online: true} = state
|
||||
) do
|
||||
Process.send_after(self(), :update_corp_wallets, @update_interval)
|
||||
|
||||
@@ -88,26 +88,26 @@ defmodule WandererApp.Character.TransactionsTracker.Impl do
|
||||
:update_corp_wallets,
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :update_corp_wallets, :timer.seconds(15))
|
||||
Process.send_after(self(), :update_corp_wallets, @update_interval)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
:check_wallets,
|
||||
%{wallets: []} = state
|
||||
%{character: character, wallets: wallets, server_online: true} = state
|
||||
) do
|
||||
Process.send_after(self(), :check_wallets, :timer.seconds(5))
|
||||
Process.send_after(self(), :check_wallets, @update_interval)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
:check_wallets,
|
||||
%{character: character, wallets: wallets} = state
|
||||
) do
|
||||
check_wallets(wallets, character)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_event(
|
||||
:check_wallets,
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :check_wallets, @update_interval)
|
||||
|
||||
state
|
||||
|
||||
@@ -14,8 +14,6 @@ defmodule WandererApp.DatabaseSetup do
|
||||
alias WandererApp.Repo
|
||||
alias Ecto.Adapters.SQL
|
||||
|
||||
@test_db_name "wanderer_test"
|
||||
|
||||
@doc """
|
||||
Sets up the test database from scratch.
|
||||
Creates the database, runs migrations, and sets up initial data.
|
||||
|
||||
@@ -2,6 +2,8 @@ defmodule WandererApp.Esi do
|
||||
@moduledoc group: :esi
|
||||
|
||||
defdelegate get_server_status, to: WandererApp.Esi.ApiClient
|
||||
defdelegate get_group_info(group_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
defdelegate get_type_info(type_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
defdelegate get_alliance_info(eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
defdelegate get_corporation_info(eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
defdelegate get_character_info(eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
@@ -21,7 +23,8 @@ defmodule WandererApp.Esi do
|
||||
defdelegate get_character_location(character_eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
defdelegate get_character_online(character_eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
defdelegate get_character_ship(character_eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
defdelegate find_routes(map_id, origin, hubs, routes_settings), to: WandererApp.Esi.ApiClient
|
||||
defdelegate get_routes_custom(hubs, origin, params), to: WandererApp.Esi.ApiClient
|
||||
defdelegate get_routes_eve(hubs, origin, params, opts), to: WandererApp.Esi.ApiClient
|
||||
defdelegate search(character_eve_id, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
|
||||
defdelegate get_killmail(killmail_id, killmail_hash, opts \\ []), to: WandererApp.Esi.ApiClient
|
||||
|
||||
@@ -6,35 +6,9 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
alias WandererApp.Cache
|
||||
|
||||
@ttl :timer.hours(1)
|
||||
@routes_ttl :timer.minutes(15)
|
||||
|
||||
@base_url "https://esi.evetech.net/latest"
|
||||
@wanderrer_user_agent "(wanderer-industries@proton.me; +https://github.com/wanderer-industries/wanderer)"
|
||||
|
||||
@req_esi Req.new(base_url: @base_url, finch: WandererApp.Finch)
|
||||
|
||||
@get_link_pairs_advanced_params [
|
||||
:include_mass_crit,
|
||||
:include_eol,
|
||||
:include_frig
|
||||
]
|
||||
|
||||
@default_routes_settings %{
|
||||
path_type: "shortest",
|
||||
include_mass_crit: true,
|
||||
include_eol: false,
|
||||
include_frig: true,
|
||||
include_cruise: true,
|
||||
avoid_wormholes: false,
|
||||
avoid_pochven: false,
|
||||
avoid_edencom: false,
|
||||
avoid_triglavian: false,
|
||||
include_thera: true,
|
||||
avoid: []
|
||||
}
|
||||
|
||||
@zarzakh_system 30_100_000
|
||||
@default_avoid_systems [@zarzakh_system]
|
||||
@req_esi_options [base_url: "https://esi.evetech.net", finch: WandererApp.Finch]
|
||||
|
||||
@cache_opts [cache: true]
|
||||
@retry_opts [retry: false, retry_log_level: :warning]
|
||||
@@ -43,11 +17,22 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
|
||||
def get_server_status, do: get("/status")
|
||||
# Pool selection for different operation types
|
||||
# Character tracking operations use dedicated high-capacity pool
|
||||
@character_tracking_pool WandererApp.Finch.ESI.CharacterTracking
|
||||
# General ESI operations use standard pool
|
||||
@general_pool WandererApp.Finch.ESI.General
|
||||
|
||||
# Helper function to get Req options with appropriate Finch pool
|
||||
defp req_options_for_pool(pool) do
|
||||
[base_url: "https://esi.evetech.net", finch: pool]
|
||||
end
|
||||
|
||||
def get_server_status, do: do_get("/status", [], @cache_opts)
|
||||
|
||||
def set_autopilot_waypoint(add_to_beginning, clear_other_waypoints, destination_id, opts \\ []),
|
||||
do:
|
||||
post_esi(
|
||||
do_post_esi(
|
||||
"/ui/autopilot/waypoint",
|
||||
get_auth_opts(opts)
|
||||
|> Keyword.merge(
|
||||
@@ -62,176 +47,20 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
def post_characters_affiliation(character_eve_ids, _opts)
|
||||
when is_list(character_eve_ids),
|
||||
do:
|
||||
post_esi(
|
||||
do_post_esi(
|
||||
"/characters/affiliation/",
|
||||
json: character_eve_ids,
|
||||
params: %{
|
||||
datasource: "tranquility"
|
||||
}
|
||||
[
|
||||
json: character_eve_ids,
|
||||
params: %{
|
||||
datasource: "tranquility"
|
||||
}
|
||||
],
|
||||
@character_tracking_pool
|
||||
)
|
||||
|
||||
def find_routes(map_id, origin, hubs, routes_settings) do
|
||||
origin = origin |> String.to_integer()
|
||||
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
|
||||
|
||||
routes_settings = @default_routes_settings |> Map.merge(routes_settings)
|
||||
|
||||
connections =
|
||||
case routes_settings.avoid_wormholes do
|
||||
false ->
|
||||
map_chains =
|
||||
routes_settings
|
||||
|> Map.take(@get_link_pairs_advanced_params)
|
||||
|> Map.put_new(:map_id, map_id)
|
||||
|> WandererApp.Api.MapConnection.get_link_pairs_advanced!()
|
||||
|> Enum.map(fn %{
|
||||
solar_system_source: solar_system_source,
|
||||
solar_system_target: solar_system_target
|
||||
} ->
|
||||
%{
|
||||
first: solar_system_source,
|
||||
second: solar_system_target
|
||||
}
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|
||||
{:ok, thera_chains} =
|
||||
case routes_settings.include_thera do
|
||||
true ->
|
||||
WandererApp.Server.TheraDataFetcher.get_chain_pairs(routes_settings)
|
||||
|
||||
false ->
|
||||
{:ok, []}
|
||||
end
|
||||
|
||||
chains = remove_intersection([map_chains | thera_chains] |> List.flatten())
|
||||
|
||||
chains =
|
||||
case routes_settings.include_cruise do
|
||||
false ->
|
||||
{:ok, wh_class_a_systems} = WandererApp.CachedInfo.get_wh_class_a_systems()
|
||||
|
||||
chains
|
||||
|> Enum.filter(fn x ->
|
||||
not Enum.member?(wh_class_a_systems, x.first) and
|
||||
not Enum.member?(wh_class_a_systems, x.second)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
chains
|
||||
end
|
||||
|
||||
chains
|
||||
|> Enum.map(fn chain ->
|
||||
["#{chain.first}|#{chain.second}", "#{chain.second}|#{chain.first}"]
|
||||
end)
|
||||
|> List.flatten()
|
||||
|
||||
true ->
|
||||
[]
|
||||
end
|
||||
|
||||
{:ok, trig_systems} = WandererApp.CachedInfo.get_trig_systems()
|
||||
|
||||
pochven_solar_systems =
|
||||
trig_systems
|
||||
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Final" end)
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
triglavian_solar_systems =
|
||||
trig_systems
|
||||
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Triglavian" end)
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
edencom_solar_systems =
|
||||
trig_systems
|
||||
|> Enum.filter(fn s -> s.triglavian_invasion_status == "Edencom" end)
|
||||
|> Enum.map(& &1.solar_system_id)
|
||||
|
||||
avoidance_list =
|
||||
case routes_settings.avoid_edencom do
|
||||
true ->
|
||||
edencom_solar_systems
|
||||
|
||||
false ->
|
||||
[]
|
||||
end
|
||||
|
||||
avoidance_list =
|
||||
case routes_settings.avoid_triglavian do
|
||||
true ->
|
||||
[avoidance_list | triglavian_solar_systems]
|
||||
|
||||
false ->
|
||||
avoidance_list
|
||||
end
|
||||
|
||||
avoidance_list =
|
||||
case routes_settings.avoid_pochven do
|
||||
true ->
|
||||
[avoidance_list | pochven_solar_systems]
|
||||
|
||||
false ->
|
||||
avoidance_list
|
||||
end
|
||||
|
||||
avoidance_list =
|
||||
(@default_avoid_systems ++ [routes_settings.avoid | avoidance_list])
|
||||
|> List.flatten()
|
||||
|> Enum.uniq()
|
||||
|
||||
params =
|
||||
%{
|
||||
datasource: "tranquility",
|
||||
flag: routes_settings.path_type,
|
||||
connections: connections,
|
||||
avoid: avoidance_list
|
||||
}
|
||||
|
||||
{:ok, all_routes} = get_all_routes(hubs, origin, params)
|
||||
|
||||
routes =
|
||||
all_routes
|
||||
|> Enum.map(fn route_info ->
|
||||
map_route_info(route_info)
|
||||
end)
|
||||
|> Enum.filter(fn route_info -> not is_nil(route_info) end)
|
||||
|
||||
{:ok, routes}
|
||||
end
|
||||
|
||||
def get_all_routes(hubs, origin, params, opts \\ []) do
|
||||
cache_key =
|
||||
"routes-#{origin}-#{hubs |> Enum.join("-")}-#{:crypto.hash(:sha, :erlang.term_to_binary(params))}"
|
||||
|
||||
case WandererApp.Cache.lookup(cache_key) do
|
||||
{:ok, result} when not is_nil(result) ->
|
||||
{:ok, result}
|
||||
|
||||
_ ->
|
||||
case get_all_routes_custom(hubs, origin, params) do
|
||||
{:ok, result} ->
|
||||
WandererApp.Cache.insert(
|
||||
cache_key,
|
||||
result,
|
||||
ttl: @routes_ttl
|
||||
)
|
||||
|
||||
{:ok, result}
|
||||
|
||||
{:error, _error} ->
|
||||
@logger.error(
|
||||
"Error getting custom routes for #{inspect(origin)}: #{inspect(params)}"
|
||||
)
|
||||
|
||||
get_all_routes_eve(hubs, origin, params, opts)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp get_all_routes_custom(hubs, origin, params),
|
||||
def get_routes_custom(hubs, origin, params),
|
||||
do:
|
||||
post(
|
||||
do_post(
|
||||
"#{get_custom_route_base_url()}/route/multiple",
|
||||
[
|
||||
json: %{
|
||||
@@ -245,13 +74,20 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
)
|
||||
|
||||
def get_all_routes_eve(hubs, origin, params, opts),
|
||||
def get_routes_eve(hubs, origin, params, opts),
|
||||
do:
|
||||
{:ok,
|
||||
hubs
|
||||
|> Task.async_stream(
|
||||
fn destination ->
|
||||
get_routes(origin, destination, params, opts)
|
||||
%{
|
||||
"origin" => origin,
|
||||
"destination" => destination,
|
||||
"systems" => [],
|
||||
"success" => false
|
||||
}
|
||||
|
||||
# do_get_routes_eve(origin, destination, params, opts)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online() * 4,
|
||||
timeout: :timer.seconds(30),
|
||||
@@ -265,8 +101,19 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end)}
|
||||
|
||||
def get_routes(origin, destination, params, opts) do
|
||||
case _get_routes(origin, destination, params, opts) do
|
||||
defp do_get_routes_eve(origin, destination, params, opts) do
|
||||
esi_params =
|
||||
Map.merge(params, %{
|
||||
connections: params.connections |> Enum.join(","),
|
||||
avoid: params.avoid |> Enum.join(",")
|
||||
})
|
||||
|
||||
do_get(
|
||||
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|> case do
|
||||
{:ok, result} ->
|
||||
%{
|
||||
"origin" => origin,
|
||||
@@ -283,7 +130,33 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "info-#{eve_id}",
|
||||
key: "group-info-#{group_id}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_group_info(group_id, opts),
|
||||
do:
|
||||
do_get(
|
||||
"/universe/groups/#{group_id}/",
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "type-info-#{type_id}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_type_info(type_id, opts),
|
||||
do:
|
||||
do_get(
|
||||
"/universe/types/#{type_id}/",
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "alliance-info-#{eve_id}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_alliance_info(eve_id, opts \\ []) do
|
||||
@@ -299,13 +172,12 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
key: "killmail-#{killmail_id}-#{killmail_hash}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_killmail(killmail_id, killmail_hash, opts \\ []) do
|
||||
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts, @cache_opts)
|
||||
end
|
||||
def get_killmail(killmail_id, killmail_hash, opts \\ []),
|
||||
do: do_get("/killmails/#{killmail_id}/#{killmail_hash}/", opts, @cache_opts)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "info-#{eve_id}",
|
||||
key: "corporation-info-#{eve_id}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_corporation_info(eve_id, opts \\ []) do
|
||||
@@ -318,11 +190,11 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "info-#{eve_id}",
|
||||
key: "character-info-#{eve_id}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_character_info(eve_id, opts \\ []) do
|
||||
case get(
|
||||
case do_get(
|
||||
"/characters/#{eve_id}/",
|
||||
opts,
|
||||
@cache_opts
|
||||
@@ -371,8 +243,17 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
do: get_character_auth_data(character_eve_id, "ship", opts ++ @cache_opts)
|
||||
|
||||
def search(character_eve_id, opts \\ []) do
|
||||
search_val = to_string(opts[:params][:search] || "")
|
||||
categories_val = to_string(opts[:params][:categories] || "character,alliance,corporation")
|
||||
params = Keyword.get(opts, :params, %{}) |> Map.new()
|
||||
|
||||
search_val =
|
||||
to_string(Map.get(params, :search) || Map.get(params, "search") || "")
|
||||
|
||||
categories_val =
|
||||
to_string(
|
||||
Map.get(params, :categories) ||
|
||||
Map.get(params, "categories") ||
|
||||
"character,alliance,corporation"
|
||||
)
|
||||
|
||||
query_params = [
|
||||
{"search", search_val},
|
||||
@@ -388,55 +269,18 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
@decorate cacheable(
|
||||
cache: Cache,
|
||||
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
|
||||
key: "search-#{character_eve_id}-#{categories_val}-#{Base.encode64(search_val)}",
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
|
||||
get_character_auth_data(character_eve_id, "search", merged_opts)
|
||||
end
|
||||
|
||||
defp remove_intersection(pairs_arr) do
|
||||
tuples = pairs_arr |> Enum.map(fn x -> {x.first, x.second} end)
|
||||
|
||||
tuples
|
||||
|> Enum.reduce([], fn {first, second} = x, acc ->
|
||||
if Enum.member?(tuples, {second, first}) do
|
||||
acc
|
||||
else
|
||||
[x | acc]
|
||||
end
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.map(fn {first, second} ->
|
||||
%{
|
||||
first: first,
|
||||
second: second
|
||||
}
|
||||
end)
|
||||
end
|
||||
|
||||
defp _get_routes(origin, destination, params, opts),
|
||||
do: get_routes_eve(origin, destination, params, opts)
|
||||
|
||||
defp get_routes_eve(origin, destination, params, opts) do
|
||||
esi_params =
|
||||
Map.merge(params, %{
|
||||
connections: params.connections |> Enum.join(","),
|
||||
avoid: params.avoid |> Enum.join(",")
|
||||
})
|
||||
|
||||
get(
|
||||
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
|
||||
opts,
|
||||
@cache_opts
|
||||
)
|
||||
end
|
||||
|
||||
defp get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
|
||||
|
||||
defp get_alliance_info(alliance_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
do_get(
|
||||
"/alliances/#{alliance_eve_id}/#{info_path}",
|
||||
opts,
|
||||
@cache_opts
|
||||
@@ -444,7 +288,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp get_corporation_info(corporation_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
do_get(
|
||||
"/corporations/#{corporation_eve_id}/#{info_path}",
|
||||
opts,
|
||||
@cache_opts
|
||||
@@ -459,14 +303,18 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
character_id = opts |> Keyword.get(:character_id, nil)
|
||||
|
||||
# Use character tracking pool for character operations
|
||||
pool = @character_tracking_pool
|
||||
|
||||
if not is_access_token_expired?(character_id) do
|
||||
get(
|
||||
do_get(
|
||||
path,
|
||||
auth_opts,
|
||||
opts |> with_refresh_token()
|
||||
opts |> with_refresh_token(),
|
||||
pool
|
||||
)
|
||||
else
|
||||
get_retry(path, auth_opts, opts |> with_refresh_token())
|
||||
do_get_retry(path, auth_opts, opts |> with_refresh_token(), :forbidden, pool)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -481,49 +329,48 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp get_corporation_auth_data(corporation_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
do_get(
|
||||
"/corporations/#{corporation_eve_id}/#{info_path}",
|
||||
[params: opts[:params] || []] ++
|
||||
(opts |> get_auth_opts()),
|
||||
(opts |> with_refresh_token()) ++ @cache_opts
|
||||
)
|
||||
|
||||
defp with_user_agent_opts(opts) do
|
||||
opts
|
||||
|> Keyword.merge(
|
||||
headers: [{:user_agent, "Wanderer/#{WandererApp.Env.vsn()} #{@wanderrer_user_agent}"}]
|
||||
)
|
||||
end
|
||||
defp with_user_agent_opts(opts),
|
||||
do:
|
||||
opts
|
||||
|> Keyword.merge(
|
||||
headers: [{:user_agent, "Wanderer/#{WandererApp.Env.vsn()} #{@wanderrer_user_agent}"}]
|
||||
)
|
||||
|
||||
defp with_refresh_token(opts) do
|
||||
opts |> Keyword.merge(refresh_token?: true)
|
||||
end
|
||||
defp with_refresh_token(opts), do: opts |> Keyword.merge(refresh_token?: true)
|
||||
|
||||
defp with_cache_opts(opts) do
|
||||
opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
|
||||
end
|
||||
defp with_cache_opts(opts),
|
||||
do: opts |> Keyword.merge(@cache_opts) |> Keyword.merge(cache_dir: System.tmp_dir!())
|
||||
|
||||
defp get(path, api_opts \\ [], opts \\ []) do
|
||||
defp do_get(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
case Cachex.get(:api_cache, path) do
|
||||
{:ok, cached_data} when not is_nil(cached_data) ->
|
||||
{:ok, cached_data}
|
||||
|
||||
_ ->
|
||||
do_get_request(path, api_opts, opts)
|
||||
do_get_request(path, api_opts, opts, pool)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_request(path, api_opts \\ [], opts \\ []) do
|
||||
defp do_get_request(path, api_opts \\ [], opts \\ [], pool \\ @general_pool) do
|
||||
try do
|
||||
case Req.get(
|
||||
@req_esi,
|
||||
api_opts
|
||||
|> Keyword.merge(url: path)
|
||||
|> with_user_agent_opts()
|
||||
|> with_cache_opts()
|
||||
|> Keyword.merge(@retry_opts)
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
) do
|
||||
req_options_for_pool(pool)
|
||||
|> Req.new()
|
||||
|> Req.get(
|
||||
api_opts
|
||||
|> Keyword.merge(url: path)
|
||||
|> with_user_agent_opts()
|
||||
|> with_cache_opts()
|
||||
|> Keyword.merge(@retry_opts)
|
||||
|> Keyword.merge(@timeout_opts)
|
||||
)
|
||||
|> case do
|
||||
{:ok, %{status: 200, body: body, headers: headers}} ->
|
||||
maybe_cache_response(path, body, headers, opts)
|
||||
|
||||
@@ -537,8 +384,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
{:ok, %{status: 420, headers: headers} = _error} ->
|
||||
# Extract rate limit information from headers
|
||||
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["0"]) |> List.first()
|
||||
remaining = Map.get(headers, "x-esi-error-limit-remain", ["0"]) |> List.first()
|
||||
|
||||
# Emit telemetry for rate limiting
|
||||
:telemetry.execute(
|
||||
@@ -568,24 +415,90 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
{:error, :error_limited, headers}
|
||||
|
||||
{:ok, %{status: status} = _error} when status in [401, 403] ->
|
||||
get_retry(path, api_opts, opts)
|
||||
{:ok, %{status: 429, headers: headers} = _error} ->
|
||||
# Extract rate limit information from headers
|
||||
reset_seconds = Map.get(headers, "retry-after", ["0"]) |> List.first()
|
||||
|
||||
{:ok, %{status: status}} ->
|
||||
# Emit telemetry for rate limiting
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
count: 1,
|
||||
reset_duration:
|
||||
case Integer.parse(reset_seconds || "0") do
|
||||
{seconds, _} -> seconds * 1000
|
||||
_ -> 0
|
||||
end
|
||||
},
|
||||
%{
|
||||
method: "GET",
|
||||
path: path,
|
||||
reset_seconds: reset_seconds
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: GET request rate limited",
|
||||
method: "GET",
|
||||
path: path,
|
||||
reset_seconds: reset_seconds
|
||||
)
|
||||
|
||||
{:error, :error_limited, headers}
|
||||
|
||||
{:ok, %{status: status} = _error} when status in [401, 403] ->
|
||||
do_get_retry(path, api_opts, opts)
|
||||
|
||||
{:ok, %{status: status, headers: headers}} ->
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
|
||||
{:error, _reason} ->
|
||||
{:error, %Mint.TransportError{reason: :timeout}} ->
|
||||
# Emit telemetry for pool timeout
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :finch, :pool_timeout],
|
||||
%{count: 1},
|
||||
%{method: "GET", path: path, pool: pool}
|
||||
)
|
||||
|
||||
{:error, :pool_timeout}
|
||||
|
||||
{:error, reason} ->
|
||||
# Check if this is a Finch pool error
|
||||
if is_exception(reason) and Exception.message(reason) =~ "unable to provide a connection" do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :finch, :pool_exhausted],
|
||||
%{count: 1},
|
||||
%{method: "GET", path: path, pool: pool}
|
||||
)
|
||||
end
|
||||
|
||||
{:error, "Request failed"}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error(Exception.message(e))
|
||||
error_msg = Exception.message(e)
|
||||
|
||||
# Emit telemetry for pool exhaustion errors
|
||||
if error_msg =~ "unable to provide a connection" do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :finch, :pool_exhausted],
|
||||
%{count: 1},
|
||||
%{method: "GET", path: path, pool: pool}
|
||||
)
|
||||
|
||||
Logger.error("FINCH_POOL_EXHAUSTED: #{error_msg}",
|
||||
method: "GET",
|
||||
path: path,
|
||||
pool: inspect(pool)
|
||||
)
|
||||
else
|
||||
Logger.error(error_msg)
|
||||
end
|
||||
|
||||
{:error, "Request failed"}
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_cache_response(path, body, %{"expires" => [expires]}, opts)
|
||||
defp maybe_cache_response(path, body, %{"expires" => [expires]} = _headers, opts)
|
||||
when is_binary(path) and not is_nil(expires) do
|
||||
try do
|
||||
if opts |> Keyword.get(:cache, false) do
|
||||
@@ -609,7 +522,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp maybe_cache_response(_path, _body, _headers, _opts), do: :ok
|
||||
|
||||
defp post(url, opts) do
|
||||
defp do_post(url, opts) do
|
||||
try do
|
||||
case Req.post("#{url}", opts |> with_user_agent_opts()) do
|
||||
{:ok, %{status: status, body: body}} when status in [200, 201] ->
|
||||
@@ -623,8 +536,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
{:ok, %{status: 420, headers: headers} = _error} ->
|
||||
# Extract rate limit information from headers
|
||||
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["0"]) |> List.first()
|
||||
remaining = Map.get(headers, "x-esi-error-limit-remain", ["0"]) |> List.first()
|
||||
|
||||
# Emit telemetry for rate limiting
|
||||
:telemetry.execute(
|
||||
@@ -668,16 +581,13 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
end
|
||||
end
|
||||
|
||||
defp post_esi(url, opts) do
|
||||
defp do_post_esi(url, opts, pool \\ @general_pool) do
|
||||
try do
|
||||
req_opts =
|
||||
(opts |> with_user_agent_opts() |> Keyword.merge(@retry_opts)) ++
|
||||
[params: opts[:params] || []]
|
||||
|
||||
Req.new(
|
||||
[base_url: @base_url, finch: WandererApp.Finch] ++
|
||||
req_opts
|
||||
)
|
||||
Req.new(req_options_for_pool(pool) ++ req_opts)
|
||||
|> Req.post(url: url)
|
||||
|> case do
|
||||
{:ok, %{status: status, body: body}} when status in [200, 201] ->
|
||||
@@ -691,8 +601,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
{:ok, %{status: 420, headers: headers} = _error} ->
|
||||
# Extract rate limit information from headers
|
||||
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
reset_seconds = Map.get(headers, "x-esi-error-limit-reset", ["0"]) |> List.first()
|
||||
remaining = Map.get(headers, "x-esi-error-limit-remain", ["0"]) |> List.first()
|
||||
|
||||
# Emit telemetry for rate limiting
|
||||
:telemetry.execute(
|
||||
@@ -722,21 +632,87 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
{:error, :error_limited, headers}
|
||||
|
||||
{:ok, %{status: 429, headers: headers} = _error} ->
|
||||
# Extract rate limit information from headers
|
||||
reset_seconds = Map.get(headers, "retry-after", ["0"]) |> List.first()
|
||||
|
||||
# Emit telemetry for rate limiting
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
count: 1,
|
||||
reset_duration:
|
||||
case Integer.parse(reset_seconds || "0") do
|
||||
{seconds, _} -> seconds * 1000
|
||||
_ -> 0
|
||||
end
|
||||
},
|
||||
%{
|
||||
method: "POST_ESI",
|
||||
path: url,
|
||||
reset_seconds: reset_seconds
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: POST request rate limited",
|
||||
method: "POST_ESI",
|
||||
path: url,
|
||||
reset_seconds: reset_seconds
|
||||
)
|
||||
|
||||
{:error, :error_limited, headers}
|
||||
|
||||
{:ok, %{status: status}} ->
|
||||
{:error, "Unexpected status: #{status}"}
|
||||
|
||||
{:error, %Mint.TransportError{reason: :timeout}} ->
|
||||
# Emit telemetry for pool timeout
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :finch, :pool_timeout],
|
||||
%{count: 1},
|
||||
%{method: "POST_ESI", path: url, pool: pool}
|
||||
)
|
||||
|
||||
{:error, :pool_timeout}
|
||||
|
||||
{:error, reason} ->
|
||||
# Check if this is a Finch pool error
|
||||
if is_exception(reason) and Exception.message(reason) =~ "unable to provide a connection" do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :finch, :pool_exhausted],
|
||||
%{count: 1},
|
||||
%{method: "POST_ESI", path: url, pool: pool}
|
||||
)
|
||||
end
|
||||
|
||||
{:error, reason}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@logger.error(Exception.message(e))
|
||||
error_msg = Exception.message(e)
|
||||
|
||||
# Emit telemetry for pool exhaustion errors
|
||||
if error_msg =~ "unable to provide a connection" do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :finch, :pool_exhausted],
|
||||
%{count: 1},
|
||||
%{method: "POST_ESI", path: url, pool: pool}
|
||||
)
|
||||
|
||||
@logger.error("FINCH_POOL_EXHAUSTED: #{error_msg}",
|
||||
method: "POST_ESI",
|
||||
path: url,
|
||||
pool: inspect(pool)
|
||||
)
|
||||
else
|
||||
@logger.error(error_msg)
|
||||
end
|
||||
|
||||
{:error, "Request failed"}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_retry(path, api_opts, opts, status \\ :forbidden) do
|
||||
defp do_get_retry(path, api_opts, opts, status \\ :forbidden, pool \\ @general_pool) do
|
||||
refresh_token? = opts |> Keyword.get(:refresh_token?, false)
|
||||
retry_count = opts |> Keyword.get(:retry_count, 0)
|
||||
character_id = opts |> Keyword.get(:character_id, nil)
|
||||
@@ -748,10 +724,11 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
{:ok, token} ->
|
||||
auth_opts = [access_token: token.access_token] |> get_auth_opts()
|
||||
|
||||
get(
|
||||
do_get(
|
||||
path,
|
||||
api_opts |> Keyword.merge(auth_opts),
|
||||
opts |> Keyword.merge(retry_count: retry_count + 1)
|
||||
opts |> Keyword.merge(retry_count: retry_count + 1),
|
||||
pool
|
||||
)
|
||||
|
||||
{:error, _error} ->
|
||||
@@ -913,44 +890,4 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
:character_token_invalid
|
||||
)
|
||||
end
|
||||
|
||||
defp map_route_info(
|
||||
%{
|
||||
"origin" => origin,
|
||||
"destination" => destination,
|
||||
"systems" => result_systems,
|
||||
"success" => success
|
||||
} = _route_info
|
||||
),
|
||||
do:
|
||||
map_route_info(%{
|
||||
origin: origin,
|
||||
destination: destination,
|
||||
systems: result_systems,
|
||||
success: success
|
||||
})
|
||||
|
||||
defp map_route_info(
|
||||
%{origin: origin, destination: destination, systems: result_systems, success: success} =
|
||||
_route_info
|
||||
) do
|
||||
systems =
|
||||
case result_systems do
|
||||
[] ->
|
||||
[]
|
||||
|
||||
_ ->
|
||||
result_systems |> Enum.reject(fn system_id -> system_id == origin end)
|
||||
end
|
||||
|
||||
%{
|
||||
has_connection: result_systems != [],
|
||||
systems: systems,
|
||||
origin: origin,
|
||||
destination: destination,
|
||||
success: success
|
||||
}
|
||||
end
|
||||
|
||||
defp map_route_info(_), do: nil
|
||||
end
|
||||
|
||||
@@ -38,32 +38,8 @@ defmodule WandererApp.EveDataService do
|
||||
|> Ash.bulk_create(WandererApp.Api.MapSolarSystemJumps, :create)
|
||||
|
||||
Logger.info("MapSolarSystemJumps updated!")
|
||||
end
|
||||
|
||||
def download_files() do
|
||||
tasks =
|
||||
@dump_file_names
|
||||
|> Enum.map(fn file_name ->
|
||||
Task.async(fn ->
|
||||
download_file(file_name)
|
||||
end)
|
||||
end)
|
||||
|
||||
Task.await_many(tasks, :timer.minutes(30))
|
||||
end
|
||||
|
||||
def download_file(file_name) do
|
||||
url = "#{@eve_db_dump_url}/#{file_name}"
|
||||
Logger.info("Downloading file from #{url}")
|
||||
|
||||
download_path = Path.join([:code.priv_dir(:wanderer_app), "repo", "data", file_name])
|
||||
|
||||
Req.get!(url, raw: true, into: File.stream!(download_path, [:write])).body
|
||||
|> Stream.run()
|
||||
|
||||
Logger.info("File downloaded successfully to #{download_path}")
|
||||
|
||||
:ok
|
||||
cleanup_files()
|
||||
end
|
||||
|
||||
def load_wormhole_types() do
|
||||
@@ -163,7 +139,57 @@ defmodule WandererApp.EveDataService do
|
||||
data
|
||||
end
|
||||
|
||||
def load_map_constellations() do
|
||||
defp cleanup_files() do
|
||||
tasks =
|
||||
@dump_file_names
|
||||
|> Enum.map(fn file_name ->
|
||||
Task.async(fn ->
|
||||
cleanup_file(file_name)
|
||||
end)
|
||||
end)
|
||||
|
||||
Task.await_many(tasks, :timer.minutes(30))
|
||||
end
|
||||
|
||||
defp cleanup_file(file_name) do
|
||||
Logger.info("Cleaning file: #{file_name}")
|
||||
|
||||
download_path = Path.join([:code.priv_dir(:wanderer_app), "repo", "data", file_name])
|
||||
|
||||
:ok = File.rm(download_path)
|
||||
|
||||
Logger.info("File removed successfully to #{download_path}")
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp download_files() do
|
||||
tasks =
|
||||
@dump_file_names
|
||||
|> Enum.map(fn file_name ->
|
||||
Task.async(fn ->
|
||||
download_file(file_name)
|
||||
end)
|
||||
end)
|
||||
|
||||
Task.await_many(tasks, :timer.minutes(30))
|
||||
end
|
||||
|
||||
defp download_file(file_name) do
|
||||
url = "#{@eve_db_dump_url}/#{file_name}"
|
||||
Logger.info("Downloading file from #{url}")
|
||||
|
||||
download_path = Path.join([:code.priv_dir(:wanderer_app), "repo", "data", file_name])
|
||||
|
||||
Req.get!(url, raw: true, into: File.stream!(download_path, [:write])).body
|
||||
|> Stream.run()
|
||||
|
||||
Logger.info("File downloaded successfully to #{download_path}")
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp load_map_constellations() do
|
||||
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
|
||||
"#{:code.priv_dir(:wanderer_app)}/repo/data/mapConstellations.csv",
|
||||
fn row ->
|
||||
@@ -175,7 +201,7 @@ defmodule WandererApp.EveDataService do
|
||||
)
|
||||
end
|
||||
|
||||
def load_map_regions() do
|
||||
defp load_map_regions() do
|
||||
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
|
||||
"#{:code.priv_dir(:wanderer_app)}/repo/data/mapRegions.csv",
|
||||
fn row ->
|
||||
@@ -187,7 +213,7 @@ defmodule WandererApp.EveDataService do
|
||||
)
|
||||
end
|
||||
|
||||
def load_map_location_wormhole_classes() do
|
||||
defp load_map_location_wormhole_classes() do
|
||||
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
|
||||
"#{:code.priv_dir(:wanderer_app)}/repo/data/mapLocationWormholeClasses.csv",
|
||||
fn row ->
|
||||
@@ -199,7 +225,7 @@ defmodule WandererApp.EveDataService do
|
||||
)
|
||||
end
|
||||
|
||||
def load_inv_groups() do
|
||||
defp load_inv_groups() do
|
||||
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
|
||||
"#{:code.priv_dir(:wanderer_app)}/repo/data/invGroups.csv",
|
||||
fn row ->
|
||||
@@ -212,7 +238,7 @@ defmodule WandererApp.EveDataService do
|
||||
)
|
||||
end
|
||||
|
||||
def get_db_data() do
|
||||
defp get_db_data() do
|
||||
map_constellations = load_map_constellations()
|
||||
map_regions = load_map_regions()
|
||||
map_location_wormhole_classes = load_map_location_wormhole_classes()
|
||||
@@ -296,7 +322,7 @@ defmodule WandererApp.EveDataService do
|
||||
)
|
||||
end
|
||||
|
||||
def get_ship_types_data() do
|
||||
defp get_ship_types_data() do
|
||||
inv_groups = load_inv_groups()
|
||||
|
||||
ship_type_groups =
|
||||
@@ -331,7 +357,7 @@ defmodule WandererApp.EveDataService do
|
||||
|> Enum.filter(fn t -> t.group_id in ship_type_groups end)
|
||||
end
|
||||
|
||||
def get_solar_system_jumps_data() do
|
||||
defp get_solar_system_jumps_data() do
|
||||
WandererApp.Utils.CSVUtil.csv_row_to_table_record(
|
||||
"#{:code.priv_dir(:wanderer_app)}/repo/data/mapSolarSystemJumps.csv",
|
||||
fn row ->
|
||||
|
||||
@@ -2,7 +2,7 @@ defmodule WandererApp.ExternalEvents do
|
||||
@moduledoc """
|
||||
External event system for SSE and webhook delivery.
|
||||
|
||||
This system is completely separate from the internal Phoenix PubSub
|
||||
This system is completely separate from the internal Phoenix PubSub
|
||||
event system and does NOT modify any existing event flows.
|
||||
|
||||
External events are delivered to:
|
||||
@@ -72,20 +72,12 @@ defmodule WandererApp.ExternalEvents do
|
||||
|
||||
# Check if MapEventRelay is alive before sending
|
||||
if Process.whereis(MapEventRelay) do
|
||||
try do
|
||||
# Use call with timeout instead of cast for better error handling
|
||||
GenServer.call(MapEventRelay, {:deliver_event, event}, 5000)
|
||||
:ok
|
||||
catch
|
||||
:exit, {:timeout, _} ->
|
||||
Logger.error("Timeout delivering event to MapEventRelay for map #{map_id}")
|
||||
{:error, :timeout}
|
||||
|
||||
:exit, reason ->
|
||||
Logger.error("Failed to deliver event to MapEventRelay: #{inspect(reason)}")
|
||||
{:error, reason}
|
||||
end
|
||||
# Use cast for async delivery to avoid blocking the caller
|
||||
# This is critical for performance in hot paths (character updates)
|
||||
GenServer.cast(MapEventRelay, {:deliver_event, event})
|
||||
:ok
|
||||
else
|
||||
Logger.debug(fn -> "MapEventRelay not available for event delivery (map: #{map_id})" end)
|
||||
{:error, :relay_not_available}
|
||||
end
|
||||
else
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
| :character_added
|
||||
| :character_removed
|
||||
| :character_updated
|
||||
| :characters_updated
|
||||
| :map_kill
|
||||
| :acl_member_added
|
||||
| :acl_member_removed
|
||||
@@ -42,50 +43,6 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
|
||||
defstruct [:id, :map_id, :type, :payload, :timestamp]
|
||||
|
||||
@doc """
|
||||
Creates a new external event with ULID for ordering.
|
||||
|
||||
Validates that the event_type is supported before creating the event.
|
||||
"""
|
||||
@spec new(String.t(), event_type(), map()) :: t() | {:error, :invalid_event_type}
|
||||
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: Ecto.ULID.generate(System.system_time(:millisecond)),
|
||||
map_id: map_id,
|
||||
type: event_type,
|
||||
payload: payload,
|
||||
timestamp: DateTime.utc_now()
|
||||
}
|
||||
else
|
||||
raise ArgumentError,
|
||||
"Invalid event type: #{inspect(event_type)}. Must be one of: #{supported_event_types() |> Enum.map(&to_string/1) |> Enum.join(", ")}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an event to JSON format for delivery.
|
||||
"""
|
||||
@spec to_json(t()) :: map()
|
||||
def to_json(%__MODULE__{} = event) do
|
||||
%{
|
||||
"id" => event.id,
|
||||
"type" => to_string(event.type),
|
||||
"map_id" => event.map_id,
|
||||
"timestamp" => DateTime.to_iso8601(event.timestamp),
|
||||
"payload" => serialize_payload(event.payload)
|
||||
}
|
||||
end
|
||||
|
||||
# Convert Ash structs and other complex types to plain maps
|
||||
defp serialize_payload(payload) when is_struct(payload) do
|
||||
serialize_payload(payload, MapSet.new())
|
||||
end
|
||||
|
||||
defp serialize_payload(payload) when is_map(payload) do
|
||||
serialize_payload(payload, MapSet.new())
|
||||
end
|
||||
|
||||
# Define allowlisted fields for different struct types
|
||||
@system_fields [
|
||||
:id,
|
||||
@@ -133,6 +90,73 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
]
|
||||
@signature_fields [:id, :signature_id, :name, :type, :group]
|
||||
|
||||
@supported_event_types [
|
||||
:add_system,
|
||||
:deleted_system,
|
||||
:system_renamed,
|
||||
:system_metadata_changed,
|
||||
:signatures_updated,
|
||||
:signature_added,
|
||||
:signature_removed,
|
||||
:connection_added,
|
||||
:connection_removed,
|
||||
:connection_updated,
|
||||
:character_added,
|
||||
:character_removed,
|
||||
:character_updated,
|
||||
:characters_updated,
|
||||
:map_kill,
|
||||
:acl_member_added,
|
||||
:acl_member_removed,
|
||||
:acl_member_updated,
|
||||
:rally_point_added,
|
||||
:rally_point_removed
|
||||
]
|
||||
|
||||
@doc """
|
||||
Creates a new external event with ULID for ordering.
|
||||
|
||||
Validates that the event_type is supported before creating the event.
|
||||
"""
|
||||
@spec new(String.t(), event_type(), map()) :: t() | {:error, :invalid_event_type}
|
||||
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: Ecto.ULID.generate(System.system_time(:millisecond)),
|
||||
map_id: map_id,
|
||||
type: event_type,
|
||||
payload: payload,
|
||||
timestamp: DateTime.utc_now()
|
||||
}
|
||||
else
|
||||
raise ArgumentError,
|
||||
"Invalid event type: #{inspect(event_type)}. Must be one of: #{supported_event_types() |> Enum.map(&to_string/1) |> Enum.join(", ")}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts an event to JSON format for delivery.
|
||||
"""
|
||||
@spec to_json(t()) :: map()
|
||||
def to_json(%__MODULE__{} = event) do
|
||||
%{
|
||||
"id" => event.id,
|
||||
"type" => to_string(event.type),
|
||||
"map_id" => event.map_id,
|
||||
"timestamp" => DateTime.to_iso8601(event.timestamp),
|
||||
"payload" => serialize_payload(event.payload)
|
||||
}
|
||||
end
|
||||
|
||||
# Convert Ash structs and other complex types to plain maps
|
||||
defp serialize_payload(payload) when is_struct(payload) do
|
||||
serialize_payload(payload, MapSet.new())
|
||||
end
|
||||
|
||||
defp serialize_payload(payload) when is_map(payload) do
|
||||
serialize_payload(payload, MapSet.new())
|
||||
end
|
||||
|
||||
# Overloaded versions with visited tracking
|
||||
defp serialize_payload(payload, visited) when is_struct(payload) do
|
||||
# Check for circular reference
|
||||
@@ -193,29 +217,7 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
Returns all supported event types.
|
||||
"""
|
||||
@spec supported_event_types() :: [event_type()]
|
||||
def supported_event_types do
|
||||
[
|
||||
:add_system,
|
||||
:deleted_system,
|
||||
:system_renamed,
|
||||
:system_metadata_changed,
|
||||
:signatures_updated,
|
||||
:signature_added,
|
||||
:signature_removed,
|
||||
:connection_added,
|
||||
:connection_removed,
|
||||
:connection_updated,
|
||||
:character_added,
|
||||
:character_removed,
|
||||
:character_updated,
|
||||
:map_kill,
|
||||
:acl_member_added,
|
||||
:acl_member_removed,
|
||||
:acl_member_updated,
|
||||
:rally_point_added,
|
||||
:rally_point_removed
|
||||
]
|
||||
end
|
||||
def supported_event_types, do: @supported_event_types
|
||||
|
||||
@doc """
|
||||
Validates an event type.
|
||||
|
||||
@@ -212,6 +212,7 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
|
||||
"time_status" => payload["time_status"] || payload[:time_status],
|
||||
"mass_status" => payload["mass_status"] || payload[:mass_status],
|
||||
"ship_size_type" => payload["ship_size_type"] || payload[:ship_size_type],
|
||||
"locked" => payload["locked"] || payload[:locked],
|
||||
"updated_at" => event.timestamp
|
||||
},
|
||||
"relationships" => %{
|
||||
|
||||
@@ -82,16 +82,9 @@ defmodule WandererApp.ExternalEvents.MapEventRelay do
|
||||
|
||||
@impl true
|
||||
def handle_call({:deliver_event, %Event{} = event}, _from, state) do
|
||||
# Log ACL events at info level for debugging
|
||||
if event.type in [:acl_member_added, :acl_member_removed, :acl_member_updated] do
|
||||
Logger.debug(fn ->
|
||||
"MapEventRelay received :deliver_event (call) for map #{event.map_id}, type: #{event.type}"
|
||||
end)
|
||||
else
|
||||
Logger.debug(fn ->
|
||||
"MapEventRelay received :deliver_event (call) for map #{event.map_id}, type: #{event.type}"
|
||||
end)
|
||||
end
|
||||
Logger.debug(fn ->
|
||||
"MapEventRelay received :deliver_event (call) for map #{event.map_id}, type: #{event.type}"
|
||||
end)
|
||||
|
||||
new_state = deliver_single_event(event, state)
|
||||
{:reply, :ok, new_state}
|
||||
|
||||
@@ -90,7 +90,9 @@ defmodule WandererApp.ExternalEvents.WebhookDispatcher do
|
||||
|
||||
@impl true
|
||||
def handle_cast({:dispatch_events, map_id, events}, state) do
|
||||
Logger.debug(fn -> "WebhookDispatcher received #{length(events)} events for map #{map_id}" end)
|
||||
Logger.debug(fn ->
|
||||
"WebhookDispatcher received #{length(events)} events for map #{map_id}"
|
||||
end)
|
||||
|
||||
# Emit telemetry for batch events
|
||||
:telemetry.execute(
|
||||
@@ -290,7 +292,7 @@ defmodule WandererApp.ExternalEvents.WebhookDispatcher do
|
||||
|
||||
request = Finch.build(:post, url, headers, payload)
|
||||
|
||||
case Finch.request(request, WandererApp.Finch, timeout: 30_000) do
|
||||
case Finch.request(request, WandererApp.Finch.Webhooks, timeout: 30_000) do
|
||||
{:ok, %Finch.Response{status: status}} ->
|
||||
{:ok, status}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ defmodule WandererApp.StartCorpWalletTrackerTask do
|
||||
|
||||
if not is_nil(admin_character) do
|
||||
:ok =
|
||||
WandererApp.Character.TrackerManager.start_tracking(admin_character.id, keep_alive: true)
|
||||
WandererApp.Character.TrackerManager.start_tracking(admin_character.id)
|
||||
|
||||
{:ok, _pid} =
|
||||
WandererApp.Character.TrackerManager.start_transaction_tracker(admin_character.id)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user