Compare commits

..

1 Commits

Author SHA1 Message Date
Dmitry Popov
d2f7e4a892 feat(Core): Added unsplashed systems support 2025-10-16 22:11:19 +02:00
329 changed files with 136275 additions and 19569 deletions

View File

@@ -1,7 +1,5 @@
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>"

View File

@@ -4,6 +4,7 @@ on:
push:
branches:
- main
- develop
env:
MIX_ENV: prod
@@ -21,7 +22,7 @@ jobs:
build:
name: 🛠 Build
runs-on: ubuntu-22.04
if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop') && github.event_name == 'push' }}
permissions:
checks: write
contents: write
@@ -36,7 +37,7 @@ jobs:
elixir: ["1.17"]
node-version: ["18.x"]
outputs:
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash }}
commit_hash: ${{ steps.generate-changelog.outputs.commit_hash || steps.set-commit-develop.outputs.commit_hash }}
steps:
- name: Prepare
run: |
@@ -90,6 +91,7 @@ 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'
@@ -100,16 +102,15 @@ 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
@@ -136,17 +137,6 @@ 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
@@ -195,24 +185,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:
@@ -243,9 +215,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
@@ -288,14 +259,3 @@ 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 Normal file
View File

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

View File

@@ -1,9 +1,9 @@
name: Build Develop
name: Build Docker Image
on:
push:
branches:
- develop
tags:
- '**'
env:
MIX_ENV: prod
@@ -18,85 +18,12 @@ 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
@@ -110,7 +37,6 @@ jobs:
matrix:
platform:
- linux/amd64
- linux/arm64
steps:
- name: Prepare
run: |
@@ -120,9 +46,25 @@ 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
@@ -171,6 +113,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:
@@ -201,8 +161,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
@@ -215,20 +176,12 @@ jobs:
docker buildx imagetools inspect ${{ env.REGISTRY_IMAGE }}:${{ steps.meta.outputs.version }}
notify:
name: 🏷 Notify about develop release
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_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.
webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL }}
content: ${{ needs.docker.outputs.release-notes }}

View File

@@ -2,531 +2,6 @@
<!-- changelog -->
## [v1.88.1](https://github.com/wanderer-industries/wanderer/compare/v1.88.0...v1.88.1) (2025-11-26)
### Bug Fixes:
* sse enable checkbox, and kills ticker
* apiv1 token auth and structure fixes
* removed ipv6 distribution env settings
* tests: updated tests
* tests: updated tests
* clean up id generation
* resolve issue with async event processing
## [v1.88.0](https://github.com/wanderer-industries/wanderer/compare/v1.87.0...v1.88.0) (2025-11-25)
### Features:
* Add zkb and eve who links for characters where it possibly was add
## [v1.87.0](https://github.com/wanderer-industries/wanderer/compare/v1.86.1...v1.87.0) (2025-11-25)
### Features:
* Add support markdown for system description
## [v1.86.1](https://github.com/wanderer-industries/wanderer/compare/v1.86.0...v1.86.1) (2025-11-25)
### Bug Fixes:
* Map: Add ability to see character passage direction in list of passages
## [v1.86.0](https://github.com/wanderer-industries/wanderer/compare/v1.85.5...v1.86.0) (2025-11-25)
### Features:
* add date filter for character activity
## [v1.85.5](https://github.com/wanderer-industries/wanderer/compare/v1.85.4...v1.85.5) (2025-11-24)
### Bug Fixes:
* core: fixed connections cleanup and rally points delete issues
## [v1.85.4](https://github.com/wanderer-industries/wanderer/compare/v1.85.3...v1.85.4) (2025-11-22)
### Bug Fixes:
* core: invalidate map characters every 1 hour for any missing/revoked permissions
## [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)

View File

@@ -30,10 +30,10 @@ format f:
mix format
test t:
MIX_ENV=test mix test
mix test
coverage cover co:
MIX_ENV=test mix test --cover
mix test --cover
unit-tests ut:
@echo "Running unit tests..."
@@ -45,3 +45,4 @@ versions v:
@cat .tool-versions
@cat Aptfile
@echo

View File

@@ -73,9 +73,7 @@ body > div:first-of-type {
}
.maps_bg {
/* 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-image: url('../images/maps_bg.webp');
background-size: cover;
background-position: center;
width: 100%;

View File

@@ -51,8 +51,20 @@ export const Characters = ({ data }: CharactersProps) => {
['border-lime-600/70']: character.online,
},
)}
title={character.name}
title={character.tracking_paused ? `${character.name} - Tracking Paused (click to resume)` : 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(

View File

@@ -118,11 +118,7 @@ export const useContextMenuSystemItems = ({
});
if (isShowPingBtn) {
return (
<WdMenuItem icon={iconClasses} className="!ml-[-2px]">
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
);
return <WdMenuItem icon={iconClasses}>{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}</WdMenuItem>;
}
return (
@@ -130,7 +126,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} className="!ml-[-2px]">
<WdMenuItem disabled icon={iconClasses}>
{!hasPing ? 'Ping: RALLY' : 'Cancel: RALLY'}
</WdMenuItem>
</MenuItemWithInfo>

View File

@@ -2,10 +2,6 @@ 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>;
@@ -18,44 +14,20 @@ export const ContextMenuSystemMultiple: React.FC<ContextMenuSystemMultipleProps>
onDeleteSystems,
onCopySystems,
}) => {
const {
data: { options, userPermissions },
} = useMapRootState();
const items: MenuItem[] = useMemo(() => {
const allowCopy = checkPermissions(userPermissions, options.allowed_copy_for);
return [
{
label: 'Delete',
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 dont 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>
);
},
},
{
label: 'Delete',
icon: PrimeIcons.TRASH,
command: onDeleteSystems,
},
];
}, [onCopySystems, onDeleteSystems, options, userPermissions]);
}, [onCopySystems, onDeleteSystems]);
return (
<>

View File

@@ -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'),
[],
);

View File

@@ -3,10 +3,6 @@ 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>;
@@ -21,13 +17,7 @@ export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
onPasteSystemsAnsConnections,
pasteSystemsAndConnections,
}) => {
const {
data: { options, userPermissions },
} = useMapState();
const items: MenuItem[] = useMemo(() => {
const allowPaste = checkPermissions(userPermissions, options.allowed_paste_for);
return [
{
label: 'Add System',
@@ -37,35 +27,14 @@ export const ContextMenuRoot: React.FC<ContextMenuRootProps> = ({
...(pasteSystemsAndConnections != null
? [
{
label: 'Paste',
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 dont 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>
);
},
},
]
: []),
];
}, [userPermissions, options, onAddSystem, pasteSystemsAndConnections, onPasteSystemsAnsConnections]);
}, [onAddSystem, onPasteSystemsAnsConnections, pasteSystemsAndConnections]);
return (
<>

View File

@@ -1,6 +1,6 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
@import '@/hooks/Mapper/components/map/styles/solar-system-node';
@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(v.$dark-bg, 0.5);
border: 1px solid color.adjust(v.$pastel-blue, $lightness: -10%);
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
border: 1px solid color.adjust($pastel-blue, $lightness: -10%);
border-radius: 5px;
position: relative;
z-index: 3;
@@ -99,7 +99,7 @@
}
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
@@ -113,11 +113,11 @@
bottom: 0;
z-index: -1;
border-color: v.$neon-color-1;
border-color: $neon-color-1;
background: repeating-linear-gradient(
45deg,
v.$neon-color-3 0px,
v.$neon-color-3 8px,
$neon-color-3 0px,
$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: v.$pastel-pink;
border-color: $pastel-pink;
}
}
@@ -347,13 +347,13 @@
.Handle {
min-width: initial;
min-height: initial;
border: 1px solid v.$pastel-blue;
border: 1px solid $pastel-blue;
width: 5px;
height: 5px;
pointer-events: auto;
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
}
&.HandleTop {

View File

@@ -1,5 +0,0 @@
import { UserPermission, UserPermissions } from '@/hooks/Mapper/types';
export const checkPermissions = (permissions: Partial<UserPermissions>, targetPermission: UserPermission) => {
return targetPermission != null && permissions[targetPermission];
};

View File

@@ -4,4 +4,3 @@ export * from './getSystemClassStyles';
export * from './getShapeClass';
export * from './getBackgroundClass';
export * from './prepareUnsplashedChunks';
export * from './checkPermissions';

View File

@@ -14,27 +14,8 @@ export const useCommandsCharacters = () => {
const ref = useRef({ update });
ref.current = { update };
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 charactersUpdated = useCallback((characters: CommandCharactersUpdated) => {
ref.current.update(() => ({ characters: characters.slice() }));
}, []);
const characterAdded = useCallback((value: CommandCharacterAdded) => {

View File

@@ -38,8 +38,6 @@ export const useMapInit = () => {
user_characters,
present_characters,
hubs,
options,
user_permissions,
}: CommandInit) => {
const { update } = ref.current;
@@ -65,14 +63,6 @@ export const useMapInit = () => {
updateData.hubs = hubs;
}
if (options) {
updateData.options = options;
}
if (options) {
updateData.userPermissions = user_permissions;
}
if (systems) {
updateData.systems = systems;
}

View File

@@ -49,91 +49,87 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } =
useCommandsCharacters();
useImperativeHandle(
ref,
() => {
return {
command(type, data) {
switch (type) {
case Commands.init:
mapInit(data as CommandInit);
break;
case Commands.addSystems:
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
break;
case Commands.updateSystems:
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
break;
case Commands.addConnections:
setTimeout(() => addConnections(data as CommandAddConnections), 100);
break;
case Commands.removeConnections:
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
break;
case Commands.charactersUpdated:
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded:
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved:
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated:
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters:
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.updateConnection:
updateConnection(data as CommandUpdateConnection);
break;
case Commands.mapUpdated:
mapUpdated(data as CommandMapUpdated);
break;
case Commands.killsUpdated:
killsUpdated(data as CommandKillsUpdated);
break;
useImperativeHandle(ref, () => {
return {
command(type, data) {
switch (type) {
case Commands.init:
mapInit(data as CommandInit);
break;
case Commands.addSystems:
setTimeout(() => mapAddSystems(data as CommandAddSystems), 100);
break;
case Commands.updateSystems:
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
setTimeout(() => removeSystems(data as CommandRemoveSystems), 100);
break;
case Commands.addConnections:
setTimeout(() => addConnections(data as CommandAddConnections), 100);
break;
case Commands.removeConnections:
setTimeout(() => removeConnections(data as CommandRemoveConnections), 100);
break;
case Commands.charactersUpdated:
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded:
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved:
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated:
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters:
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.updateConnection:
updateConnection(data as CommandUpdateConnection);
break;
case Commands.mapUpdated:
mapUpdated(data as CommandMapUpdated);
break;
case Commands.killsUpdated:
killsUpdated(data as CommandKillsUpdated);
break;
case Commands.centerSystem:
setTimeout(() => {
const systemId = `${data}`;
centerSystem(systemId as CommandSelectSystem);
}, 100);
break;
case Commands.centerSystem:
setTimeout(() => {
const systemId = `${data}`;
centerSystem(systemId as CommandSelectSystem);
}, 100);
break;
case Commands.selectSystem:
selectSystems({ systems: [data as string], delay: 500 });
break;
case Commands.selectSystem:
selectSystems({ systems: [data as string], delay: 500 });
break;
case Commands.selectSystems:
selectSystems(data as CommandSelectSystems);
break;
case Commands.selectSystems:
selectSystems(data as CommandSelectSystems);
break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes:
case Commands.signaturesUpdated:
case Commands.linkSignatureToSystem:
case Commands.detailedKillsUpdated:
case Commands.characterActivityData:
case Commands.trackingCharactersData:
case Commands.updateActivity:
case Commands.updateTracking:
case Commands.userSettingsUpdated:
// do nothing
break;
case Commands.pingAdded:
case Commands.pingCancelled:
case Commands.routes:
case Commands.signaturesUpdated:
case Commands.linkSignatureToSystem:
case Commands.detailedKillsUpdated:
case Commands.characterActivityData:
case Commands.trackingCharactersData:
case Commands.updateActivity:
case Commands.updateTracking:
case Commands.userSettingsUpdated:
// do nothing
break;
default:
console.warn(`Map handlers: Unknown command: ${type}`, data);
break;
}
},
};
},
[],
);
default:
console.warn(`Map handlers: Unknown command: ${type}`, data);
break;
}
},
};
}, []);
};

View File

@@ -4,13 +4,10 @@ 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;

View File

@@ -1,7 +1,7 @@
import { MarkdownComment } from '@/hooks/Mapper/components/mapInterface/components/Comments/components';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useEffect, useRef, useState } from 'react';
import { CommentType } from '@/hooks/Mapper/types';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
export interface CommentsProps {}
@@ -14,9 +14,7 @@ export const Comments = ({}: CommentsProps) => {
comments: { loadComments, comments, lastUpdateKey },
} = useMapRootState();
const systemId = useMemo(() => {
return +selectedSystems[0];
}, [selectedSystems]);
const [systemId] = selectedSystems;
const ref = useRef({ loadComments, systemId });
ref.current = { loadComments, systemId };

View File

@@ -1,5 +1,8 @@
.MarkdownTextViewer {
.MarkdownCommentRoot {
border-left-width: 3px;
@apply text-[12px] leading-[1.2] text-stone-300 break-words;
@apply bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0;
.h1 {
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
@@ -53,10 +56,6 @@
@apply font-bold text-green-400 break-words whitespace-normal;
}
strong {
font-weight: bold;
}
i, em {
@apply italic text-pink-400 break-words whitespace-normal;
}

View File

@@ -1,3 +1,4 @@
import classes from './MarkdownComment.module.scss';
import clsx from 'clsx';
import {
InfoDrawer,
@@ -48,11 +49,7 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
<>
<InfoDrawer
labelClassName="mb-[3px]"
className={clsx(
'p-1 bg-stone-700/20',
'text-[12px] leading-[1.2] text-stone-300 break-words',
'bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0',
)}
className={clsx(classes.MarkdownCommentRoot, 'p-1 bg-stone-700/20 ')}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
title={

View File

@@ -1,9 +0,0 @@
.CERoot {
@apply border border-stone-400/30 rounded-[2px];
:global {
.cm-content {
@apply bg-stone-600/40;
}
}
}

View File

@@ -3,10 +3,9 @@ import clsx from 'clsx';
import { PrimeIcons } from 'primereact/api';
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useCallback, useRef, useState } from 'react';
import { OutCommand } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import classes from './CommentsEditor.module.scss';
export interface CommentsEditorProps {}
@@ -19,9 +18,7 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
outCommand,
} = useMapRootState();
const systemId = useMemo(() => {
return +selectedSystems[0];
}, [selectedSystems]);
const [systemId] = selectedSystems;
const ref = useRef({ outCommand, systemId, textVal });
ref.current = { outCommand, systemId, textVal };
@@ -51,7 +48,6 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
return (
<MarkdownEditor
className={classes.CERoot}
value={textVal}
onChange={setTextVal}
overlayContent={

View File

@@ -1,9 +1,9 @@
.CERoot {
@apply border border-stone-500/30 rounded-[2px];
@apply border border-stone-400/30 rounded-[2px];
:global {
.cm-content {
@apply bg-stone-950/70;
@apply bg-stone-600/40;
}
.cm-scroller {

View File

@@ -44,17 +44,9 @@ export interface MarkdownEditorProps {
overlayContent?: ReactNode;
value: string;
onChange: (value: string) => void;
height?: string;
className?: string;
}
export const MarkdownEditor = ({
value,
onChange,
overlayContent,
height = '70px',
className,
}: MarkdownEditorProps) => {
export const MarkdownEditor = ({ value, onChange, overlayContent }: MarkdownEditorProps) => {
const [hasShift, setHasShift] = useState(false);
const refData = useRef({ onChange });
@@ -74,9 +66,9 @@ export const MarkdownEditor = ({
<div className={clsx(classes.MarkdownEditor, 'relative')}>
<CodeMirror
value={value}
height={height}
height="70px"
extensions={CODE_MIRROR_EXTENSIONS}
className={clsx(classes.CERoot, className)}
className={classes.CERoot}
theme={oneDark}
onChange={handleOnChange}
placeholder="Start typing..."

View File

@@ -8,8 +8,8 @@ import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
import { Dialog } from 'primereact/dialog';
import { IconField } from 'primereact/iconfield';
import { InputText } from 'primereact/inputtext';
import { InputTextarea } from 'primereact/inputtextarea';
import { useCallback, useEffect, useRef, useState } from 'react';
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
interface SystemSettingsDialog {
systemId: string;
@@ -214,9 +214,13 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
<div className="flex flex-col gap-1">
<label htmlFor="username">Description</label>
<div className="h-[200px]">
<MarkdownEditor value={description} onChange={e => setDescription(e)} height="180px" />
</div>
<InputTextarea
autoResize
rows={5}
cols={30}
value={description}
onChange={e => setDescription(e.target.value)}
/>
</div>
</div>

View File

@@ -1,12 +1,12 @@
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSettingsDialog } from '@/hooks/Mapper/components/mapInterface/components/SystemSettingsDialog/SystemSettingsDialog.tsx';
import { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
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 { LayoutEventBlocker, SystemView, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { SystemInfoContent } from './SystemInfoContent';
import { PrimeIcons } from 'primereact/api';
import { useCallback, useState } from 'react';
import { SystemInfoContent } from './SystemInfoContent';
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';
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">

View File

@@ -2,7 +2,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
import { useMemo } from 'react';
import { getSystemById, sortWHClasses } from '@/hooks/Mapper/helpers';
import { InfoDrawer, MarkdownTextViewer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { InfoDrawer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
interface SystemInfoContentProps {
@@ -51,7 +51,7 @@ export const SystemInfoContent = ({ systemId }: SystemInfoContentProps) => {
</div>
}
>
<MarkdownTextViewer>{description}</MarkdownTextViewer>
<div className="break-words">{description}</div>
</InfoDrawer>
)}
</div>

View File

@@ -19,7 +19,13 @@ import {
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { SignatureSettings } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings';
import { TooltipPosition, WdTooltip, WdTooltipHandlers, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
import { ExtendedSystemSignature, SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
import {
ExtendedSystemSignature,
OutCommand,
SignatureGroup,
SignatureKind,
SystemSignature,
} from '@/hooks/Mapper/types';
import {
renderAddedTimeLeft,
@@ -73,6 +79,7 @@ export const SystemSignaturesContent = ({
const [hoveredSignature, setHoveredSignature] = useState<SystemSignature | null>(null);
const {
outCommand,
storedSettings: { settingsSignatures, settingsSignaturesUpdate },
} = useMapRootState();
@@ -238,6 +245,16 @@ export const SystemSignaturesContent = ({
});
}, []);
const handleUnsplash = useCallback(
async (row: SystemSignature) => {
await outCommand({
type: OutCommand.unsplashSignature,
data: { system_id: systemId, eve_id: row.eve_id },
});
},
[outCommand],
);
return (
<div ref={tableRef} className="h-full">
{filteredSignatures.length === 0 ? (
@@ -354,8 +371,11 @@ export const SystemSignaturesContent = ({
{!selectable && (
<Column
header=""
body={() => (
body={(row: SystemSignature) => (
<div className="flex justify-end items-center gap-2 mr-[4px]">
<WdTooltipWrapper content="Unsplash signature">
<span className={PrimeIcons.SHARE_ALT + ' text-[10px]'} onClick={() => handleUnsplash(row)} />
</WdTooltipWrapper>
<WdTooltipWrapper content="Double-click a row to edit signature">
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
</WdTooltipWrapper>

View File

@@ -30,14 +30,10 @@ 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);
},
[systemId, structures, handleUpdateStructures],
[structures, handleUpdateStructures],
);
const handlePaste = useCallback(

View File

@@ -30,6 +30,9 @@ 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);
@@ -43,24 +46,34 @@ export const SystemStructuresDialog: React.FC<StructuresEditDialogProps> = ({
// Searching corporation owners via auto-complete
const searchOwners = useCallback(
async (e: { query: string }) => {
const query = e.query.trim();
if (!query) {
const newQuery = e.query.trim();
if (!newQuery) {
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: query },
data: { search: newQuery },
});
setOwnerSuggestions(results);
setPrevQuery(newQuery);
setPrevResults(results);
} catch (err) {
console.error('Failed to fetch owners:', err);
setOwnerSuggestions([]);
}
},
[outCommand],
[prevQuery, prevResults, outCommand],
);
const handleChange = (field: keyof StructureItem, val: string | Date) => {
@@ -109,6 +122,7 @@ 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 },

View File

@@ -56,11 +56,6 @@ 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);

View File

@@ -1,7 +1,4 @@
import { Dialog } from 'primereact/dialog';
import { Menu } from 'primereact/menu';
import { MenuItem } from 'primereact/menuitem';
import { useState, useCallback, useRef, useMemo } from 'react';
import { CharacterActivityContent } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivityContent.tsx';
interface CharacterActivityProps {
@@ -9,69 +6,17 @@ interface CharacterActivityProps {
onHide: () => void;
}
const periodOptions = [
{ value: 30, label: '30 Days' },
{ value: 365, label: '1 Year' },
{ value: null, label: 'All Time' },
];
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
const [selectedPeriod, setSelectedPeriod] = useState<number | null>(30);
const menuRef = useRef<Menu>(null);
const handlePeriodChange = useCallback((days: number | null) => {
setSelectedPeriod(days);
}, []);
const menuItems: MenuItem[] = useMemo(
() => [
{
label: 'Period',
items: periodOptions.map(option => ({
label: option.label,
icon: selectedPeriod === option.value ? 'pi pi-check' : undefined,
command: () => handlePeriodChange(option.value),
})),
},
],
[selectedPeriod, handlePeriodChange],
);
const selectedPeriodLabel = useMemo(
() => periodOptions.find(opt => opt.value === selectedPeriod)?.label || 'All Time',
[selectedPeriod],
);
const headerIcons = (
<>
<button
type="button"
className="p-dialog-header-icon p-link"
onClick={e => menuRef.current?.toggle(e)}
aria-label="Filter options"
>
<span className="pi pi-bars" />
</button>
<Menu model={menuItems} popup ref={menuRef} />
</>
);
return (
<Dialog
header={
<div className="flex items-center gap-2">
<span>Character Activity</span>
<span className="text-xs text-stone-400">({selectedPeriodLabel})</span>
</div>
}
header="Character Activity"
visible={visible}
className="w-[550px] max-h-[90vh]"
onHide={onHide}
dismissableMask
contentClassName="p-0 h-full flex flex-col"
icons={headerIcons}
>
<CharacterActivityContent selectedPeriod={selectedPeriod} />
<CharacterActivityContent />
</Dialog>
);
};

View File

@@ -7,28 +7,16 @@ import {
} from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/helpers.tsx';
import { Column } from 'primereact/column';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMemo, useEffect } from 'react';
import { useCharacterActivityHandlers } from '@/hooks/Mapper/components/mapRootContent/hooks/useCharacterActivityHandlers';
import { useMemo } from 'react';
interface CharacterActivityContentProps {
selectedPeriod: number | null;
}
export const CharacterActivityContent = ({ selectedPeriod }: CharacterActivityContentProps) => {
export const CharacterActivityContent = () => {
const {
data: { characterActivityData },
} = useMapRootState();
const { handleShowActivity } = useCharacterActivityHandlers();
const activity = useMemo(() => characterActivityData?.activity || [], [characterActivityData]);
const loading = useMemo(() => characterActivityData?.loading !== false, [characterActivityData]);
// Reload activity data when period changes
useEffect(() => {
handleShowActivity(selectedPeriod);
}, [selectedPeriod, handleShowActivity]);
if (loading) {
return (
<div className="flex flex-col items-center justify-center h-full w-full">

View File

@@ -3,7 +3,7 @@
}
.SidebarOnTheMap {
width: 500px;
width: 400px;
padding: 0 !important;
:global {

View File

@@ -5,7 +5,6 @@ import {
ConnectionType,
OutCommand,
Passage,
PassageWithSourceTarget,
SolarSystemConnection,
} from '@/hooks/Mapper/types';
import clsx from 'clsx';
@@ -20,7 +19,7 @@ import { PassageCard } from './PassageCard';
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
const itemTemplate = (item: PassageWithSourceTarget, options: VirtualScrollerTemplateOptions) => {
const itemTemplate = (item: Passage, options: VirtualScrollerTemplateOptions) => {
return (
<div
className={clsx(classes.CharacterRow, 'w-full box-border', {
@@ -36,7 +35,7 @@ const itemTemplate = (item: PassageWithSourceTarget, options: VirtualScrollerTem
};
export interface ConnectionPassagesContentProps {
passages: PassageWithSourceTarget[];
passages: Passage[];
}
export const ConnectionPassages = ({ passages = [] }: ConnectionPassagesContentProps) => {
@@ -114,20 +113,6 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
[outCommand],
);
const preparedPassages = useMemo(() => {
if (!cnInfo) {
return [];
}
return passages
.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at))
.map<PassageWithSourceTarget>(x => ({
...x,
source: x.from ? cnInfo.target : cnInfo.source,
target: x.from ? cnInfo.source : cnInfo.target,
}));
}, [cnInfo, passages]);
useEffect(() => {
if (!selectedConnection) {
return;
@@ -160,14 +145,12 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
<InfoDrawer title="Connection" rightSide>
<div className="flex justify-end gap-2 items-center">
<SystemView
showCustomName
systemId={cnInfo.source}
className={clsx(classes.InfoTextSize, 'select-none text-center')}
hideRegion
/>
<span className="pi pi-angle-double-right text-stone-500 text-[15px]"></span>
<SystemView
showCustomName
systemId={cnInfo.target}
className={clsx(classes.InfoTextSize, 'select-none text-center')}
hideRegion
@@ -201,7 +184,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
{/* separator */}
<div className="w-full h-px bg-neutral-800 px-0.5"></div>
<ConnectionPassages passages={preparedPassages} />
<ConnectionPassages passages={passages} />
</div>
</Sidebar>
);

View File

@@ -35,10 +35,6 @@
&.ThreeColumns {
grid-template-columns: auto 1fr auto;
}
&.FourColumns {
grid-template-columns: auto auto 1fr auto;
}
}
.CardBorderLeftIsOwn {

View File

@@ -1,19 +1,17 @@
import clsx from 'clsx';
import classes from './PassageCard.module.scss';
import { PassageWithSourceTarget } from '@/hooks/Mapper/types';
import { SystemView, TimeAgo, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
import { Passage } from '@/hooks/Mapper/types';
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
import { useCallback, useMemo } from 'react';
import { ZKB_ICON } from '@/hooks/Mapper/icons';
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
import { useMemo } from 'react';
type PassageCardType = {
// compact?: boolean;
showShipName?: boolean;
// showSystem?: boolean;
// useSystemsCache?: boolean;
} & PassageWithSourceTarget;
} & Passage;
const SHIP_NAME_RX = /u'|'/g;
export const getShipName = (name: string) => {
@@ -27,7 +25,7 @@ export const getShipName = (name: string) => {
});
};
export const PassageCard = ({ inserted_at, character: char, ship, source, target, from }: PassageCardType) => {
export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardType) => {
const isOwn = false;
const insertedAt = useMemo(() => {
@@ -35,46 +33,11 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
return date.toLocaleString();
}, [inserted_at]);
const handleOpenZKB = useCallback(() => window.open(charZKBLink(char.eve_id), '_blank'), [char]);
const handleOpenEveWho = useCallback(() => window.open(charEveWhoLink(char.eve_id), '_blank'), [char]);
return (
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}>
<div className="flex flex-col justify-between px-2 py-1 gap-1">
{/*here icon and other*/}
<div className={clsx(classes.CharRow, classes.FourColumns)}>
<WdTooltipWrapper
position={TooltipPosition.top}
content={
<div className="flex justify-between gap-2 items-center">
<SystemView
showCustomName
systemId={source}
className="select-none text-center !text-[12px]"
hideRegion
/>
<span className="pi pi-angle-double-right text-stone-500 text-[15px]"></span>
<SystemView
showCustomName
systemId={target}
className="select-none text-center !text-[12px]"
hideRegion
/>
</div>
}
>
<div
className={clsx(
'transition-all transform ease-in duration-200',
'pi text-stone-500 text-[15px] w-[35px] h-[33px] !flex items-center justify-center border rounded-[6px]',
{
['pi-angle-double-right !text-orange-400 border-orange-400 hover:bg-orange-400/30']: from,
['pi-angle-double-left !text-stone-500/70 border-stone-500/70 hover:bg-stone-500/30']: !from,
},
)}
/>
</WdTooltipWrapper>
<div className={clsx(classes.CharRow, classes.ThreeColumns)}>
{/*portrait*/}
<span
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
@@ -86,7 +49,7 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
{/*here name and ship name*/}
<div className="grid gap-1 justify-between grid-cols-[max-content_1fr]">
{/*char name*/}
<div className="grid gap-1 grid-cols-[auto_1px_1fr_auto]">
<div className="grid gap-1 grid-cols-[auto_1px_1fr]">
<span
className={clsx(classes.MaxWidth, 'text-ellipsis overflow-hidden whitespace-nowrap', {
[classes.CardBorderLeftIsOwn]: isOwn,
@@ -99,21 +62,6 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
<div className="h-3 border-r border-neutral-500 my-0.5"></div>
{char.alliance_ticker && <span className="text-neutral-400">{char.alliance_ticker}</span>}
{!char.alliance_ticker && <span className="text-neutral-400">{char.corporation_ticker}</span>}
<div className={clsx('flex gap-1 items-center h-full ml-[2px]')}>
<WdImgButton
width={16}
height={16}
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
source={ZKB_ICON}
onClick={handleOpenZKB}
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Eve Who' }}
className={clsx('pi pi-user', '!text-[12px] relative top-[-1px]')}
onClick={handleOpenEveWho}
/>
</div>
</div>
{/*ship name*/}

View File

@@ -10,14 +10,9 @@ 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(
@@ -27,7 +22,7 @@ export const CommonSettings = () => {
[renderSettingItem],
);
const handleResetSettings = useCallback(() => resetSettings(), [resetSettings]);
const handleResetSettings = () => {};
return (
<div className="flex flex-col h-full gap-1">

View File

@@ -1,6 +1,6 @@
@use "sass:color";
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
@use '@/hooks/Mapper/components/map/styles/solar-system-node' as v;
@import '@/hooks/Mapper/components/map/styles/solar-system-node';
:root {
--rf-has-user-characters: #ffc75d;
@@ -108,7 +108,7 @@
}
&.selected {
border-color: v.$pastel-pink;
border-color: $pastel-pink;
box-shadow: 0 0 10px #9a1af1c2;
}
@@ -122,11 +122,11 @@
bottom: 0;
z-index: -1;
border-color: v.$neon-color-1;
border-color: $neon-color-1;
background: repeating-linear-gradient(
45deg,
v.$neon-color-3 0px,
v.$neon-color-3 8px,
$neon-color-3 0px,
$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: v.$pastel-pink;
border-color: $pastel-pink;
}
}

View File

@@ -23,17 +23,17 @@ export const useCharacterActivityHandlers = () => {
/**
* Handle showing the character activity dialog
*/
const handleShowActivity = useCallback((days?: number | null) => {
const handleShowActivity = useCallback(() => {
// Update local state to show the dialog
update(state => ({
...state,
showCharacterActivity: true,
}));
// Send the command to the server with optional days parameter
// Send the command to the server
outCommand({
type: OutCommand.showActivity,
data: days !== undefined ? { days } : {},
data: {},
});
}, [outCommand, update]);

View File

@@ -3,7 +3,6 @@ import {
WdEveEntityPortrait,
WdEveEntityPortraitSize,
WdEveEntityPortraitType,
WdImgButton,
WdTooltipWrapper,
} from '@/hooks/Mapper/components/ui-kit';
import { SystemView } from '@/hooks/Mapper/components/ui-kit/SystemView';
@@ -15,8 +14,6 @@ import { Commands } from '@/hooks/Mapper/types/mapHandlers';
import clsx from 'clsx';
import { useCallback } from 'react';
import classes from './CharacterCard.module.scss';
import { ZKB_ICON } from '@/hooks/Mapper/icons';
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
export type CharacterCardProps = {
compact?: boolean;
@@ -69,9 +66,6 @@ export const CharacterCard = ({
const shipType = char.ship?.ship_type_info?.name;
const locationShown = showSystem && char.location?.solar_system_id;
const handleOpenZKB = useCallback(() => window.open(charZKBLink(char.eve_id), '_blank'), [char]);
const handleOpenEveWho = useCallback(() => window.open(charEveWhoLink(char.eve_id), '_blank'), [char]);
// INFO: Simple mode show only name and icon of ally/corp. By default it compact view
if (simpleMode) {
return (
@@ -250,24 +244,7 @@ export const CharacterCard = ({
{char.name}
</span>
{showTicker && <span className="flex-shrink-0 text-gray-400 ml-1">[{tickerText}]</span>}
<div className={clsx('flex gap-1 items-center h-full ml-[6px]')}>
<WdImgButton
width={16}
height={16}
tooltip={{ position: TooltipPosition.top, content: 'Open zkillboard' }}
source={ZKB_ICON}
onClick={handleOpenZKB}
className="min-w-[16px]"
/>
<WdImgButton
tooltip={{ position: TooltipPosition.top, content: 'Open Eve Who' }}
className={clsx('pi pi-user', '!text-[12px] relative top-[-1px]')}
onClick={handleOpenEveWho}
/>
</div>
</div>
{locationShown ? (
<div className="text-gray-300 text-xs overflow-hidden text-ellipsis whitespace-nowrap">
<SystemView

View File

@@ -2,16 +2,10 @@ import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkBreaks from 'remark-breaks';
import classes from './MarkdownTextViewer.module.scss';
const REMARK_PLUGINS = [remarkGfm, remarkBreaks];
type MarkdownTextViewerProps = { children: string };
export const MarkdownTextViewer = ({ children }: MarkdownTextViewerProps) => {
return (
<div className={classes.MarkdownTextViewer}>
<Markdown remarkPlugins={REMARK_PLUGINS}>{children}</Markdown>
</div>
);
return <Markdown remarkPlugins={REMARK_PLUGINS}>{children}</Markdown>;
};

View File

@@ -4,17 +4,8 @@ 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;
tooltipWrapperClassName?: string;
} & WithChildren;
export const MenuItemWithInfo = ({
children,
infoClass,
infoTitle,
tooltipWrapperClassName,
}: MenuItemWithInfoProps) => {
type MenuItemWithInfoProps = { infoTitle: ReactNode; infoClass?: string } & WithChildren;
export const MenuItemWithInfo = ({ children, infoClass, infoTitle }: MenuItemWithInfoProps) => {
return (
<div className="flex justify-between w-full h-full items-center">
{children}
@@ -22,7 +13,6 @@ export const MenuItemWithInfo = ({
content={infoTitle}
position={TooltipPosition.top}
className="!opacity-100 !pointer-events-auto"
wrapperClassName={tooltipWrapperClassName}
>
<div className={clsx('pi text-orange-400', infoClass)} />
</WdTooltipWrapper>

View File

@@ -1,18 +1,13 @@
import { WithChildren, WithClassName } from '@/hooks/Mapper/types/common.ts';
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
import clsx from 'clsx';
type WdMenuItemProps = { icon?: string; disabled?: boolean } & WithChildren & WithClassName;
export const WdMenuItem = ({ children, icon, disabled, className }: WdMenuItemProps) => {
type WdMenuItemProps = { icon?: string; disabled?: boolean } & WithChildren;
export const WdMenuItem = ({ children, icon, disabled }: WdMenuItemProps) => {
return (
<a
className={clsx(
'flex gap-[6px] w-full h-full items-center px-[12px] !py-0',
'p-menuitem-link',
{
'p-disabled': disabled,
},
className,
)}
className={clsx('flex gap-[6px] w-full h-full items-center px-[12px] !py-0 ml-[-2px]', 'p-menuitem-link', {
'p-disabled': disabled,
})}
>
{icon && <div className={clsx('min-w-[20px]', icon)}></div>}
<div className="w-full">{children}</div>

View File

@@ -10,7 +10,6 @@ export type WdTooltipWrapperProps = {
interactive?: boolean;
smallPaddings?: boolean;
tooltipClassName?: string;
wrapperClassName?: string;
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
Omit<TooltipProps, 'content'>;
@@ -27,7 +26,6 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
smallPaddings,
size,
tooltipClassName,
wrapperClassName,
...props
},
forwardedRef,
@@ -38,7 +36,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
return (
<div className={clsx(classes.WdTooltipWrapperRoot, className)} {...props}>
{targetSelector ? <>{children}</> : <div className={clsx(autoClass, wrapperClassName)}>{children}</div>}
{targetSelector ? <>{children}</> : <div className={autoClass}>{children}</div>}
<WdTooltip
ref={forwardedRef}

View File

@@ -1,5 +1,12 @@
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';

View File

@@ -1,2 +0,0 @@
export const charZKBLink = (characterId: string) => `https://zkillboard.com/character/${characterId}/`;
export const charEveWhoLink = (characterId: string) => `https://evewho.com/character/${characterId}`;

View File

@@ -6,11 +6,9 @@ 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';
@@ -82,16 +80,7 @@ const INITIAL_DATA: MapRootData = {
selectedSystems: [],
selectedConnections: [],
userPermissions: {},
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',
},
options: {},
isSubscriptionActive: false,
linkSignatureToSystem: null,
mainCharacterEveId: null,
@@ -146,7 +135,7 @@ export interface MapRootContextProps {
hasOldSettings: boolean;
getSettingsForExport(): string | undefined;
applySettings(settings: MapUserSettings): boolean;
resetSettings(): void;
resetSettings(settings: MapUserSettings): void;
checkOldSettings(): void;
};
}

View File

@@ -12,7 +12,7 @@ export const useCommandComments = () => {
}, []);
const removeComment = useCallback((data: CommandCommentRemoved) => {
ref.current.removeComment(data.solarSystemId, data.commentId);
ref.current.removeComment(data.solarSystemId.toString(), data.commentId);
}, []);
return { addComment, removeComment };

View File

@@ -1,4 +1,5 @@
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useCallback, useRef } from 'react';
import {
CommandCharacterAdded,
CommandCharacterRemoved,
@@ -6,7 +7,6 @@ import {
CommandCharacterUpdated,
CommandPresentCharacters,
} from '@/hooks/Mapper/types';
import { useCallback, useRef } from 'react';
export const useCommandsCharacters = () => {
const { update } = useMapRootState();
@@ -14,27 +14,8 @@ export const useCommandsCharacters = () => {
const ref = useRef({ update });
ref.current = { update };
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 charactersUpdated = useCallback((characters: CommandCharactersUpdated) => {
ref.current.update(() => ({ characters: characters.slice() }));
}, []);
const characterAdded = useCallback((value: CommandCharacterAdded) => {

View File

@@ -1,5 +1,5 @@
import { CommentSystem, CommentType, OutCommand, OutCommandHandler, UseCommentsData } from '@/hooks/Mapper/types';
import { useCallback, useRef, useState } from 'react';
import { CommentSystem, CommentType, OutCommand, OutCommandHandler, UseCommentsData } from '@/hooks/Mapper/types';
interface UseCommentsProps {
outCommand: OutCommandHandler;
@@ -8,12 +8,12 @@ interface UseCommentsProps {
export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData => {
const [lastUpdateKey, setLastUpdateKey] = useState(0);
const commentBySystemsRef = useRef<Map<number, CommentSystem>>(new Map());
const commentBySystemsRef = useRef<Map<string, CommentSystem>>(new Map());
const ref = useRef({ outCommand });
ref.current = { outCommand };
const loadComments = useCallback(async (systemId: number) => {
const loadComments = useCallback(async (systemId: string) => {
let cSystem = commentBySystemsRef.current.get(systemId);
if (cSystem?.loading || cSystem?.loaded) {
return;
@@ -45,7 +45,7 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
setLastUpdateKey(x => x + 1);
}, []);
const addComment = useCallback((systemId: number, comment: CommentType) => {
const addComment = useCallback((systemId: string, comment: CommentType) => {
const cSystem = commentBySystemsRef.current.get(systemId);
if (cSystem) {
cSystem.comments.push(comment);
@@ -61,9 +61,8 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
setLastUpdateKey(x => x + 1);
}, []);
const removeComment = useCallback((systemId: number, commentId: string) => {
const removeComment = useCallback((systemId: string, commentId: string) => {
const cSystem = commentBySystemsRef.current.get(systemId);
console.log('cSystem', cSystem);
if (!cSystem) {
return;
}

View File

@@ -148,6 +148,10 @@ 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;
@@ -162,24 +166,6 @@ 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,

View File

@@ -33,6 +33,7 @@ export type CharacterTypeRaw = {
corporation_id: number;
corporation_name: string;
corporation_ticker: string;
tracking_paused: boolean;
};
export interface TrackingCharacter {
@@ -68,5 +69,4 @@ export interface ActivitySummary {
passages: number;
connections: number;
signatures: number;
timestamp?: string;
}

View File

@@ -13,9 +13,9 @@ export type CommentSystem = {
};
export interface UseCommentsData {
loadComments: (systemId: number) => Promise<void>;
addComment: (systemId: number, comment: CommentType) => void;
removeComment: (systemId: number, commentId: string) => void;
comments: Map<number, CommentSystem>;
loadComments: (systemId: string) => Promise<void>;
addComment: (systemId: string, comment: CommentType) => void;
removeComment: (systemId: string, commentId: string) => void;
comments: Map<string, CommentSystem>;
lastUpdateKey: number;
}

View File

@@ -6,17 +6,11 @@ export type PassageLimitedCharacterType = Pick<
>;
export type Passage = {
from: boolean;
inserted_at: string; // Date
ship: ShipTypeRaw;
character: PassageLimitedCharacterType;
};
export type PassageWithSourceTarget = {
source: string;
target: string;
} & Passage;
export type ConnectionInfoOutput = {
marl_eol_time: string;
};

View File

@@ -9,4 +9,3 @@ export * from './connectionPassages';
export * from './permissions';
export * from './comment';
export * from './ping';
export * from './options';

View File

@@ -1,4 +1,4 @@
import { CommentType, MapOptions, PingData, SystemSignature, UserPermissions } from '@/hooks/Mapper/types';
import { CommentType, 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: MapOptions;
options: Record<string, string | boolean>;
reset?: boolean;
is_subscription_active?: boolean;
main_character_eve_id?: string | null;
@@ -131,7 +131,7 @@ export type CommandLinkSignatureToSystem = {
};
export type CommandLinkSignaturesUpdated = number;
export type CommandCommentAdd = {
solarSystemId: number;
solarSystemId: string;
comment: CommentType;
};
export type CommandCommentRemoved = {
@@ -282,6 +282,7 @@ export enum OutCommand {
unlinkSignature = 'unlink_signature',
searchSystems = 'search_systems',
undoDeleteSignatures = 'undo_delete_signatures',
unsplashSignature = 'unsplash_signature',
}
export type OutCommandHandler = <T = unknown>(event: { type: OutCommand; data: unknown }) => Promise<T>;

View File

@@ -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 { MapOptions, PingData, UserPermissions } from '@/hooks/Mapper/types';
import { 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: MapOptions;
options: Record<string, string | boolean>;
isSubscriptionActive: boolean;
mainCharacterEveId: string | null;

View File

@@ -1,14 +0,0 @@
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;
};

View File

@@ -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.002;
const VELOCITY_INC = 1.01;
const VELOCITY_INIT_INC = 0.525;
const JUMP_VELOCITY_INC = 0.55;
const JUMP_SIZE_INC = 1.15;
const SIZE_INC = 1.002;
const SIZE_INC = 1.01;
const RAD = Math.PI / 180;
const WARP_COLORS = [
[197, 239, 247],

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -25,9 +25,13 @@ config :wanderer_app,
ecto_repos: [WandererApp.Repo],
ash_domains: [WandererApp.Api],
generators: [timestamp_type: :utc_datetime],
ddrt: WandererApp.Map.CacheRTree,
ddrt: DDRT,
logger: Logger,
pubsub_client: Phoenix.PubSub
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"
config :wanderer_app, WandererAppWeb.Endpoint,
adapter: Bandit.PhoenixAdapter,

View File

@@ -4,7 +4,7 @@ import Config
config :wanderer_app, WandererApp.Repo,
username: "postgres",
password: "postgres",
hostname: "localhost",
hostname: System.get_env("DB_HOST", "localhost"),
database: "wanderer_dev",
stacktrace: true,
show_sensitive_data_on_connection_error: true,

View File

@@ -177,34 +177,7 @@ 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: [
@@ -285,9 +258,7 @@ config :wanderer_app, WandererApp.Scheduler,
timezone: :utc,
jobs:
[
{"@daily", {WandererApp.Map.Audit, :archive, []}},
{"@daily", {WandererApp.Map.GarbageCollector, :cleanup_chain_passages, []}},
{"@daily", {WandererApp.Map.GarbageCollector, :cleanup_system_signatures, []}}
{"@daily", {WandererApp.Map.Audit, :archive, []}}
] ++ sheduler_jobs,
timeout: :infinity
@@ -432,7 +403,7 @@ config :wanderer_app, :license_manager,
config :wanderer_app, :sse,
enabled:
config_dir
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "false")
|> get_var_from_path_or_env("WANDERER_SSE_ENABLED", "true")
|> String.to_existing_atom(),
max_connections_total:
config_dir |> get_int_from_path_or_env("WANDERER_SSE_MAX_CONNECTIONS", 1000),
@@ -447,6 +418,6 @@ config :wanderer_app, :sse,
config :wanderer_app, :external_events,
webhooks_enabled:
config_dir
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "false")
|> get_var_from_path_or_env("WANDERER_WEBHOOKS_ENABLED", "true")
|> String.to_existing_atom(),
webhook_timeout_ms: config_dir |> get_int_from_path_or_env("WANDERER_WEBHOOK_TIMEOUT_MS", 15000)

View File

@@ -24,11 +24,7 @@ config :wanderer_app,
pubsub_client: Test.PubSubMock,
cached_info: WandererApp.CachedInfo.Mock,
character_api_disabled: false,
environment: :test,
map_subscriptions_enabled: false,
wanderer_kills_service_enabled: false,
sse: [enabled: false],
external_events: [webhooks_enabled: false]
environment: :test
# We don't run a server during test. If one is required,
# you can enable the server option below.

View File

@@ -60,17 +60,19 @@ defmodule WandererApp.Api.AccessList do
# Added :api_key to the accepted attributes
accept [:name, :description, :owner_id, :api_key]
primary?(true)
argument :owner_id, :uuid, allow_nil?: false
change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
end
update :update do
accept [:name, :description, :owner_id, :api_key]
primary?(true)
require_atomic? false
end
update :assign_owner do
accept [:owner_id]
require_atomic? false
end
end

View File

@@ -53,11 +53,7 @@ defmodule WandererApp.Api.AccessListMember do
:role
]
defaults [:create, :read, :destroy]
update :update do
require_atomic? false
end
defaults [:create, :read, :update, :destroy]
read :read_by_access_list do
argument(:access_list_id, :string, allow_nil?: false)
@@ -71,14 +67,12 @@ defmodule WandererApp.Api.AccessListMember do
update :block do
accept([])
require_atomic? false
change(set_attribute(:blocked, true))
end
update :unblock do
accept([])
require_atomic? false
change(set_attribute(:blocked, false))
end

View File

@@ -1,80 +0,0 @@
defmodule WandererApp.Api.ActorHelpers do
@moduledoc """
Utilities for extracting actor information from Ash contexts.
Provides helper functions for working with ActorWithMap and extracting
user, map, and character information from various context formats.
"""
alias WandererApp.Api.ActorWithMap
@doc """
Extract map from actor or context.
Handles various context formats:
- Direct ActorWithMap struct
- Context map with :actor key
- Context map with :map key
- Ash.Resource.Change.Context struct
"""
def get_map(%{actor: %ActorWithMap{map: %{} = map}}), do: map
def get_map(%{map: %{} = map}), do: map
# Handle Ash.Resource.Change.Context struct
def get_map(%Ash.Resource.Change.Context{actor: %ActorWithMap{map: %{} = map}}), do: map
def get_map(%Ash.Resource.Change.Context{actor: _}), do: nil
def get_map(context) when is_map(context) do
# For plain maps, check private.actor
with private when is_map(private) <- Map.get(context, :private),
%ActorWithMap{map: %{} = map} <- Map.get(private, :actor) do
map
else
_ -> nil
end
end
def get_map(_), do: nil
@doc """
Extract user from actor.
Handles:
- ActorWithMap struct
- Direct user struct with :id field
"""
def get_user(%ActorWithMap{user: user}), do: user
def get_user(%{id: _} = user), do: user
def get_user(_), do: nil
@doc """
Get character IDs for the actor.
Used for ACL filtering to determine which resources the user can access.
Returns {:ok, list} or {:ok, []} if no characters found.
"""
def get_character_ids(%ActorWithMap{user: user}), do: get_character_ids(user)
def get_character_ids(%{characters: characters}) when is_list(characters) do
{:ok, Enum.map(characters, & &1.id)}
end
def get_character_ids(%{characters: %Ecto.Association.NotLoaded{}, id: user_id}) do
# Load characters from database
load_characters_by_id(user_id)
end
def get_character_ids(%{id: user_id}) do
# Fallback: load user with characters
load_characters_by_id(user_id)
end
def get_character_ids(_), do: {:ok, []}
defp load_characters_by_id(user_id) do
case WandererApp.Api.User.by_id(user_id, load: [:characters]) do
{:ok, user} -> {:ok, Enum.map(user.characters, & &1.id)}
_ -> {:ok, []}
end
end
end

View File

@@ -1,15 +0,0 @@
defmodule WandererApp.Api.ActorWithMap do
@moduledoc """
Wraps a user and map together as an actor for token-based authentication.
When API requests use Bearer token auth, the token identifies both the user
(map owner) and the map. This struct allows passing both through Ash's actor system.
"""
@enforce_keys [:user, :map]
defstruct [:user, :map]
def new(user, map) do
%__MODULE__{user: user, map: map}
end
end

View File

@@ -1,39 +0,0 @@
defmodule WandererApp.Api.Changes.InjectMapFromActor do
@moduledoc """
Ash change that injects map_id from the authenticated actor.
For token-based auth, the map is determined by the API token.
This change automatically sets map_id, so clients don't need to provide it.
"""
use Ash.Resource.Change
alias WandererApp.Api.ActorHelpers
@impl true
def change(changeset, _opts, context) do
case ActorHelpers.get_map(context) do
%{id: map_id} ->
Ash.Changeset.force_change_attribute(changeset, :map_id, map_id)
_other ->
# nil or unexpected return shape - check for direct map_id
# Check params (input), arguments, and attributes (in that order)
map_id = Map.get(changeset.params, :map_id) ||
Ash.Changeset.get_argument(changeset, :map_id) ||
Ash.Changeset.get_attribute(changeset, :map_id)
case map_id do
nil ->
Ash.Changeset.add_error(changeset,
field: :map_id,
message: "map_id is required (provide via token or attribute)"
)
_map_id ->
# map_id provided directly (internal calls, tests)
changeset
end
end
end
end

View File

@@ -1,25 +1,7 @@
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()
@@ -30,95 +12,10 @@ defmodule WandererApp.Api.Changes.SlugifyName do
defp maybe_slugify_name(changeset) do
case Changeset.get_attribute(changeset, :slug) do
slug when is_binary(slug) ->
base_slug = Slug.slugify(slug)
unique_slug = ensure_unique_slug(changeset, base_slug)
Changeset.force_change_attribute(changeset, :slug, unique_slug)
Changeset.force_change_attribute(changeset, :slug, Slug.slugify(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

View File

@@ -69,6 +69,11 @@ defmodule WandererApp.Api.Character do
filter(expr(user_id == ^arg(:user_id) and deleted == false))
end
read :available_by_map do
argument(:map_id, :uuid, allow_nil?: false)
filter(expr(user_id == ^arg(:user_id) and deleted == false))
end
read :last_active do
argument(:from, :utc_datetime, allow_nil?: false)
@@ -95,7 +100,6 @@ defmodule WandererApp.Api.Character do
update :mark_as_deleted do
accept([])
require_atomic? false
change(atomic_update(:deleted, true))
change(atomic_update(:user_id, nil))
@@ -103,7 +107,6 @@ defmodule WandererApp.Api.Character do
update :update_online do
accept([:online])
require_atomic? false
end
update :update_location do

View File

@@ -33,11 +33,7 @@ defmodule WandererApp.Api.CorpWalletTransaction do
:ref_type
]
defaults [:create, :read, :destroy]
update :update do
require_atomic? false
end
defaults [:create, :read, :update, :destroy]
create :new do
accept [

View File

@@ -36,11 +36,7 @@ defmodule WandererApp.Api.License do
:expire_at
]
defaults [:read, :destroy]
update :update do
require_atomic? false
end
defaults [:read, :update, :destroy]
create :create do
primary? true
@@ -62,14 +58,12 @@ defmodule WandererApp.Api.License do
update :invalidate do
accept([])
require_atomic? false
change(set_attribute(:is_valid, false))
end
update :set_valid do
accept([])
require_atomic? false
change(set_attribute(:is_valid, true))
end

View File

@@ -8,8 +8,6 @@ defmodule WandererApp.Api.Map do
alias Ash.Resource.Change.Builtins
require Logger
postgres do
repo(WandererApp.Repo)
table("maps_v1")
@@ -32,21 +30,20 @@ defmodule WandererApp.Api.Map do
# Routes configuration
routes do
base("/maps")
get(:by_slug, route: "/:slug")
# index :read
get(:read)
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
code_interface do
define(:available, action: :available)
define(:get_map_by_slug, action: :by_slug, args: [:slug])
define(:by_api_key, action: :by_api_key, args: [:api_key])
define(:new, action: :new)
define(:create, action: :create)
define(:update, action: :update)
@@ -57,7 +54,6 @@ defmodule WandererApp.Api.Map do
define(:mark_as_deleted, action: :mark_as_deleted)
define(:update_api_key, action: :update_api_key)
define(:toggle_webhooks, action: :toggle_webhooks)
define(:toggle_sse, action: :toggle_sse)
define(:by_id,
get_by: [:id],
@@ -94,34 +90,22 @@ defmodule WandererApp.Api.Map do
filter expr(slug == ^arg(:slug))
end
read :by_api_key do
get? true
argument :api_key, :string, allow_nil?: false
prepare WandererApp.Api.Preparations.SecureApiKeyLookup
end
read :available do
prepare WandererApp.Api.Preparations.FilterMapsByRoles
end
create :new do
accept [
:name,
:slug,
:description,
:scope,
:only_tracked_characters,
:owner_id,
:sse_enabled
]
accept [:name, :slug, :description, :scope, :only_tracked_characters, :owner_id]
primary?(true)
argument :owner_id, :uuid, allow_nil?: false
argument :create_default_acl, :boolean, allow_nil?: true
argument :acls, {:array, :uuid}, allow_nil?: true
argument :acls_text_input, :string, allow_nil?: true
argument :scope_text_input, :string, allow_nil?: true
argument :acls_empty_selection, :string, allow_nil?: true
change manage_relationship(:owner_id, :owner, on_lookup: :relate, on_no_match: nil)
change manage_relationship(:acls, type: :append_and_remove)
change WandererApp.Api.Changes.SlugifyName
end
@@ -129,16 +113,7 @@ defmodule WandererApp.Api.Map do
update :update do
primary? true
require_atomic? false
accept [
:name,
:slug,
:description,
:scope,
:only_tracked_characters,
:owner_id,
:sse_enabled
]
accept [:name, :slug, :description, :scope, :only_tracked_characters, :owner_id]
argument :owner_id_text_input, :string, allow_nil?: true
argument :acls_text_input, :string, allow_nil?: true
@@ -153,9 +128,6 @@ defmodule WandererApp.Api.Map do
)
change WandererApp.Api.Changes.SlugifyName
# Validate subscription when enabling SSE
validate &validate_sse_subscription/2
end
update :update_acls do
@@ -170,46 +142,33 @@ defmodule WandererApp.Api.Map do
update :assign_owner do
accept [:owner_id]
require_atomic? false
end
update :update_hubs do
accept [:hubs]
require_atomic? false
end
update :update_options do
accept [:options]
require_atomic? false
end
update :mark_as_deleted do
accept([])
require_atomic? false
change(set_attribute(:deleted, true))
end
update :update_api_key do
accept [:public_api_key]
require_atomic? false
end
update :toggle_webhooks do
accept [:webhooks_enabled]
require_atomic? false
end
update :toggle_sse do
require_atomic? false
accept [:sse_enabled]
# Validate subscription when enabling SSE
validate &validate_sse_subscription/2
end
create :duplicate do
accept [:name, :description, :scope, :only_tracked_characters]
argument :source_map_id, :uuid, allow_nil?: false
argument :copy_acls, :boolean, default: true
argument :copy_user_settings, :boolean, default: true
@@ -353,19 +312,12 @@ defmodule WandererApp.Api.Map do
public?(true)
end
attribute :sse_enabled, :boolean do
default(false)
allow_nil?(false)
public?(true)
end
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end
identities do
identity :unique_slug, [:slug]
identity :unique_public_api_key, [:public_api_key]
end
relationships do
@@ -392,49 +344,4 @@ defmodule WandererApp.Api.Map do
public? false
end
end
# SSE Subscription Validation
#
# This validation ensures that SSE can only be enabled when:
# 1. SSE is being disabled (always allowed)
# 2. Map is being created (skip validation, will be checked on first update)
# 3. Community Edition mode (always allowed)
# 4. Enterprise mode with active subscription
defp validate_sse_subscription(changeset, _context) do
sse_enabled = Ash.Changeset.get_attribute(changeset, :sse_enabled)
map_id = changeset.data.id
subscriptions_enabled = WandererApp.Env.map_subscriptions_enabled?()
cond do
# Not enabling SSE - no validation needed
not sse_enabled ->
:ok
# Map creation (no ID yet) - skip validation
is_nil(map_id) ->
:ok
# Community Edition mode - always allow
not subscriptions_enabled ->
:ok
# Enterprise mode - check subscription
true ->
validate_active_subscription(map_id)
end
end
defp validate_active_subscription(map_id) do
case WandererApp.Map.is_subscription_active?(map_id) do
{:ok, true} ->
:ok
{:ok, false} ->
{:error, field: :sse_enabled, message: "Active subscription required to enable SSE"}
{:error, reason} ->
Logger.error("Error checking subscription status: #{inspect(reason)}")
{:error, field: :sse_enabled, message: "Unable to verify subscription status"}
end
end
end

View File

@@ -61,11 +61,7 @@ defmodule WandererApp.Api.MapAccessList do
:access_list_id
]
defaults [:create, :read, :destroy]
update :update do
require_atomic? false
end
defaults [:create, :read, :update, :destroy]
read :read_by_map do
argument(:map_id, :string, allow_nil?: false)

View File

@@ -27,11 +27,7 @@ defmodule WandererApp.Api.MapChainPassages do
:solar_system_target_id
]
defaults [:create, :read, :destroy]
update :update do
require_atomic? false
end
defaults [:create, :read, :update, :destroy]
create :new do
accept [
@@ -44,6 +40,12 @@ defmodule WandererApp.Api.MapChainPassages do
]
primary?(true)
argument :map_id, :uuid, allow_nil?: false
argument :character_id, :uuid, allow_nil?: false
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
end
action :by_map_id, {:array, :struct} do

View File

@@ -81,6 +81,12 @@ defmodule WandererApp.Api.MapCharacterSettings do
:character_id,
:tracked
]
argument :map_id, :uuid, allow_nil?: false
argument :character_id, :uuid, allow_nil?: false
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
end
read :by_map_filtered do
@@ -139,7 +145,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
update :track do
accept [:map_id, :character_id]
require_atomic? false
argument :map_id, :string, allow_nil?: false
argument :character_id, :uuid, allow_nil?: false
# Load the record first
load do
@@ -152,7 +159,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
update :untrack do
accept [:map_id, :character_id]
require_atomic? false
argument :map_id, :string, allow_nil?: false
argument :character_id, :uuid, allow_nil?: false
# Load the record first
load do
@@ -165,7 +173,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
update :follow do
accept [:map_id, :character_id]
require_atomic? false
argument :map_id, :string, allow_nil?: false
argument :character_id, :uuid, allow_nil?: false
# Load the record first
load do
@@ -178,7 +187,8 @@ defmodule WandererApp.Api.MapCharacterSettings do
update :unfollow do
accept [:map_id, :character_id]
require_atomic? false
argument :map_id, :string, allow_nil?: false
argument :character_id, :uuid, allow_nil?: false
# Load the record first
load do

View File

@@ -4,17 +4,11 @@ defmodule WandererApp.Api.MapConnection do
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer,
extensions: [AshJsonApi.Resource],
primary_read_warning?: false
extensions: [AshJsonApi.Resource]
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
@@ -74,56 +68,7 @@ defmodule WandererApp.Api.MapConnection do
:custom_info
]
create :create do
primary? true
accept [
:map_id,
:solar_system_source,
:solar_system_target,
:type,
:ship_size_type,
:mass_status,
:time_status,
:wormhole_type,
:count_of_passage,
:locked,
:custom_info
]
# Inject map_id from token
change WandererApp.Api.Changes.InjectMapFromActor
end
read :read do
primary? true
# Security: Filter to only connections from actor's map
prepare WandererApp.Api.Preparations.FilterConnectionsByActorMap
end
update :update do
primary? true
accept [
:solar_system_source,
:solar_system_target,
:type,
:ship_size_type,
:mass_status,
:time_status,
:wormhole_type,
:count_of_passage,
:locked,
:custom_info
]
require_atomic? false
end
destroy :destroy do
primary? true
end
defaults [:create, :read, :update, :destroy]
read :read_by_map do
argument(:map_id, :string, allow_nil?: false)
@@ -160,37 +105,30 @@ defmodule WandererApp.Api.MapConnection do
update :update_mass_status do
accept [:mass_status]
require_atomic? false
end
update :update_time_status do
accept [:time_status]
require_atomic? false
end
update :update_ship_size_type do
accept [:ship_size_type]
require_atomic? false
end
update :update_locked do
accept [:locked]
require_atomic? false
end
update :update_custom_info do
accept [:custom_info]
require_atomic? false
end
update :update_type do
accept [:type]
require_atomic? false
end
update :update_wormhole_type do
accept [:wormhole_type]
require_atomic? false
end
end

View File

@@ -30,11 +30,7 @@ defmodule WandererApp.Api.MapInvite do
:token
]
defaults [:read, :destroy]
update :update do
require_atomic? false
end
defaults [:read, :update, :destroy]
create :new do
accept [
@@ -45,6 +41,10 @@ defmodule WandererApp.Api.MapInvite do
]
primary?(true)
argument :map_id, :uuid, allow_nil?: true
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
end
read :by_map do

View File

@@ -3,8 +3,7 @@ defmodule WandererApp.Api.MapPing do
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer,
primary_read_warning?: false
data_layer: AshPostgres.DataLayer
postgres do
repo(WandererApp.Repo)
@@ -37,18 +36,7 @@ defmodule WandererApp.Api.MapPing do
:message
]
defaults [:destroy]
update :update do
require_atomic? false
end
read :read do
primary? true
# Security: Filter to only pings from actor's map
prepare WandererApp.Api.Preparations.FilterPingsByActorMap
end
defaults [:read, :update, :destroy]
create :new do
accept [
@@ -60,6 +48,14 @@ defmodule WandererApp.Api.MapPing do
]
primary?(true)
argument :map_id, :uuid, allow_nil?: false
argument :system_id, :uuid, allow_nil?: false
argument :character_id, :uuid, allow_nil?: false
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
change manage_relationship(:system_id, :system, on_lookup: :relate, on_no_match: nil)
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
end
read :by_map do

View File

@@ -65,11 +65,7 @@ defmodule WandererApp.Api.MapSolarSystem do
:sun_type_id
]
defaults [:read, :destroy]
update :update do
require_atomic? false
end
defaults [:read, :destroy, :update]
create :create do
primary? true

View File

@@ -24,11 +24,7 @@ defmodule WandererApp.Api.MapSolarSystemJumps do
:to_solar_system_id
]
defaults [:read, :destroy]
update :update do
require_atomic? false
end
defaults [:read, :destroy, :update]
create :create do
primary? true

View File

@@ -45,11 +45,7 @@ defmodule WandererApp.Api.MapState do
:connections_start_time
]
defaults [:read, :destroy]
update :update do
require_atomic? false
end
defaults [:read, :update, :destroy]
create :create do
primary? true

View File

@@ -62,14 +62,10 @@ defmodule WandererApp.Api.MapSubscription do
:auto_renew?
]
defaults [:create, :read, :destroy]
update :update do
require_atomic? false
end
defaults [:create, :read, :update, :destroy]
read :all_active do
prepare build(sort: [updated_at: :asc], load: [:map])
prepare build(sort: [updated_at: :asc])
filter(expr(status == :active))
end
@@ -92,39 +88,32 @@ defmodule WandererApp.Api.MapSubscription do
update :update_plan do
accept [:plan]
require_atomic? false
end
update :update_characters_limit do
accept [:characters_limit]
require_atomic? false
end
update :update_hubs_limit do
accept [:hubs_limit]
require_atomic? false
end
update :update_active_till do
accept [:active_till]
require_atomic? false
end
update :update_auto_renew do
accept [:auto_renew?]
require_atomic? false
end
update :cancel do
accept([])
require_atomic? false
change(set_attribute(:status, :cancelled))
end
update :expire do
accept([])
require_atomic? false
change(set_attribute(:status, :expired))
end

View File

@@ -1,31 +1,10 @@
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,
extensions: [AshJsonApi.Resource],
primary_read_warning?: false
extensions: [AshJsonApi.Resource]
postgres do
repo(WandererApp.Repo)
@@ -37,17 +16,6 @@ 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)
@@ -63,10 +31,12 @@ defmodule WandererApp.Api.MapSystem do
code_interface do
define(:create, action: :create)
define(:upsert, action: :upsert)
define(:destroy, action: :destroy)
define :by_id, action: :get_by_id, args: [:id], get?: true
define(:by_id,
get_by: [:id],
action: :read
)
define(:by_solar_system_id,
get_by: [:solar_system_id],
@@ -96,7 +66,6 @@ defmodule WandererApp.Api.MapSystem do
define(:update_status, action: :update_status)
define(:update_tag, action: :update_tag)
define(:update_temporary_name, action: :update_temporary_name)
define(:update_custom_name, action: :update_custom_name)
define(:update_labels, action: :update_labels)
define(:update_linked_sig_eve_id, action: :update_linked_sig_eve_id)
define(:update_position, action: :update_position)
@@ -122,88 +91,11 @@ defmodule WandererApp.Api.MapSystem do
:linked_sig_eve_id
]
create :create do
primary? true
accept [
:map_id,
:name,
:solar_system_id,
:position_x,
:position_y,
:status,
:visible,
:locked,
:custom_name,
:description,
:tag,
:temporary_name,
:labels,
:added_at,
:linked_sig_eve_id
]
# Inject map_id from token
change WandererApp.Api.Changes.InjectMapFromActor
end
update :update do
primary? true
require_atomic? false
# Note: name and solar_system_id are not in accept
# - solar_system_id should be immutable (identifier)
# - name has allow_nil? false which makes it required in JSON:API
accept [
:position_x,
:position_y,
:status,
:visible,
:locked,
:custom_name,
:description,
:tag,
:temporary_name,
:labels,
:linked_sig_eve_id
]
end
destroy :destroy do
primary? true
end
create :upsert do
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
defaults [:create, :update, :destroy]
read :read do
primary?(true)
# Security: Filter to only systems from actor's map
prepare WandererApp.Api.Preparations.FilterSystemsByActorMap
pagination offset?: true,
default_limit: 100,
max_page_size: 500,
@@ -211,11 +103,6 @@ defmodule WandererApp.Api.MapSystem do
required?: false
end
read :get_by_id do
argument(:id, :string, allow_nil?: false)
filter(expr(id == ^arg(:id)))
end
read :read_all_by_map do
argument(:map_id, :string, allow_nil?: false)
filter(expr(map_id == ^arg(:map_id)))
@@ -237,59 +124,44 @@ defmodule WandererApp.Api.MapSystem do
update :update_name do
accept [:name]
require_atomic? false
end
update :update_description do
accept [:description]
require_atomic? false
end
update :update_locked do
accept [:locked]
require_atomic? false
end
update :update_status do
accept [:status]
require_atomic? false
end
update :update_tag do
accept [:tag]
require_atomic? false
end
update :update_temporary_name do
accept [:temporary_name]
require_atomic? false
end
update :update_custom_name do
accept [:custom_name]
require_atomic? false
end
update :update_labels do
accept [:labels]
require_atomic? false
end
update :update_position do
accept [:position_x, :position_y]
require_atomic? false
change(set_attribute(:visible, true))
end
update :update_linked_sig_eve_id do
accept [:linked_sig_eve_id]
require_atomic? false
end
update :update_visible do
accept [:visible]
require_atomic? false
end
end

View File

@@ -59,6 +59,12 @@ defmodule WandererApp.Api.MapSystemComment do
:character_id,
:text
]
argument :system_id, :uuid, allow_nil?: false
argument :character_id, :uuid, allow_nil?: false
change manage_relationship(:system_id, :system, on_lookup: :relate, on_no_match: nil)
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
end
read :by_system_id do

View File

@@ -111,6 +111,10 @@ defmodule WandererApp.Api.MapSystemSignature do
:custom_info,
:deleted
]
argument :system_id, :uuid, allow_nil?: false
change manage_relationship(:system_id, :system, on_lookup: :relate, on_no_match: nil)
end
update :update do
@@ -135,17 +139,14 @@ defmodule WandererApp.Api.MapSystemSignature do
update :update_linked_system do
accept [:linked_system_id]
require_atomic? false
end
update :update_type do
accept [:type]
require_atomic? false
end
update :update_group do
accept [:group]
require_atomic? false
end
read :by_system_id do

View File

@@ -122,6 +122,13 @@ defmodule WandererApp.Api.MapSystemStructure do
:status,
:end_time
]
argument :system_id, :uuid, allow_nil?: false
change manage_relationship(:system_id, :system,
on_lookup: :relate,
on_no_match: nil
)
end
update :update do

View File

@@ -29,11 +29,7 @@ defmodule WandererApp.Api.MapTransaction do
:amount
]
defaults [:create, :read, :destroy]
update :update do
require_atomic? false
end
defaults [:create, :read, :update, :destroy]
read :by_map do
argument(:map_id, :string, allow_nil?: false)

View File

@@ -53,30 +53,22 @@ defmodule WandererApp.Api.MapUserSettings do
:settings
]
defaults [:create, :read, :destroy]
update :update do
require_atomic? false
end
defaults [:create, :read, :update, :destroy]
update :update_settings do
accept [:settings]
require_atomic? false
end
update :update_main_character do
accept [:main_character_eve_id]
require_atomic? false
end
update :update_following_character do
accept [:following_character_eve_id]
require_atomic? false
end
update :update_hubs do
accept [:hubs]
require_atomic? false
end
end

View File

@@ -58,8 +58,6 @@ defmodule WandererApp.Api.MapWebhookSubscription do
:consecutive_failures,
:secret
]
require_atomic? false
end
read :by_map do

View File

@@ -1,64 +0,0 @@
defmodule WandererApp.Api.Preparations.FilterByActorMap do
@moduledoc """
Shared filtering logic for actor map context.
Filters queries to only return resources belonging to the actor's map.
Used by preparations for MapSystem, MapConnection, and MapPing resources.
"""
require Ash.Query
alias WandererApp.Api.ActorHelpers
@doc """
Filter a query by the actor's map context.
If a map is found in the context, filters the query to only return
resources where map_id matches. If no map context exists, returns
a query that will return no results.
## Parameters
* `query` - The Ash query to filter
* `context` - The Ash context containing actor/map information
* `resource_name` - Name of the resource for telemetry (atom)
## Examples
iex> query = Ash.Query.new(WandererApp.Api.MapSystem)
iex> context = %{map: %{id: "map-123"}}
iex> result = FilterByActorMap.filter_by_map(query, context, :map_system)
# Returns query filtered by map_id == "map-123"
"""
def filter_by_map(query, context, resource_name) do
case ActorHelpers.get_map(context) do
%{id: map_id} ->
emit_telemetry(resource_name, map_id)
Ash.Query.filter(query, map_id == ^map_id)
nil ->
emit_telemetry_no_context(resource_name)
Ash.Query.filter(query, false)
_other ->
emit_telemetry_no_context(resource_name)
Ash.Query.filter(query, false)
end
end
defp emit_telemetry(resource_name, map_id) do
:telemetry.execute(
[:wanderer_app, :ash, :preparation, :filter_by_map],
%{count: 1},
%{resource: resource_name, map_id: map_id}
)
end
defp emit_telemetry_no_context(resource_name) do
:telemetry.execute(
[:wanderer_app, :ash, :preparation, :filter_by_map, :no_context],
%{count: 1},
%{resource: resource_name}
)
end
end

View File

@@ -1,17 +0,0 @@
defmodule WandererApp.Api.Preparations.FilterConnectionsByActorMap do
@moduledoc """
Ash preparation that filters connections to only those from the actor's map.
For token-based auth, this ensures the API only returns connections
from the map associated with the token.
"""
use Ash.Resource.Preparation
alias WandererApp.Api.Preparations.FilterByActorMap
@impl true
def prepare(query, _opts, context) do
FilterByActorMap.filter_by_map(query, context, :map_connection)
end
end

View File

@@ -1,17 +0,0 @@
defmodule WandererApp.Api.Preparations.FilterPingsByActorMap do
@moduledoc """
Ash preparation that filters pings to only those from the actor's map.
For token-based auth, this ensures the API only returns pings
from the map associated with the token.
"""
use Ash.Resource.Preparation
alias WandererApp.Api.Preparations.FilterByActorMap
@impl true
def prepare(query, _opts, context) do
FilterByActorMap.filter_by_map(query, context, :map_ping)
end
end

View File

@@ -1,17 +0,0 @@
defmodule WandererApp.Api.Preparations.FilterSystemsByActorMap do
@moduledoc """
Ash preparation that filters systems to only those from the actor's map.
For token-based auth, this ensures the API only returns systems
from the map associated with the token.
"""
use Ash.Resource.Preparation
alias WandererApp.Api.Preparations.FilterByActorMap
@impl true
def prepare(query, _opts, context) do
FilterByActorMap.filter_by_map(query, context, :map_system)
end
end

View File

@@ -1,62 +0,0 @@
defmodule WandererApp.Api.Preparations.SecureApiKeyLookup do
@moduledoc """
Preparation that performs secure API key lookup using constant-time comparison.
This preparation:
1. Queries for the map with the given API key using database index
2. Performs constant-time comparison to verify the key matches
3. Returns the map only if the secure comparison passes
The constant-time comparison prevents timing attacks where an attacker
could deduce information about valid API keys by measuring response times.
"""
use Ash.Resource.Preparation
require Ash.Query
@dummy_key "dummy_key_for_timing_consistency_00000000"
def prepare(query, _params, _context) do
api_key = Ash.Query.get_argument(query, :api_key)
if is_nil(api_key) or api_key == "" do
# Return empty result for invalid input
Ash.Query.filter(query, expr(false))
else
# First, do the database lookup using the index
# Then apply constant-time comparison in after_action
query
|> Ash.Query.filter(expr(public_api_key == ^api_key))
|> Ash.Query.after_action(fn _query, results ->
verify_results_with_secure_compare(results, api_key)
end)
end
end
defp verify_results_with_secure_compare(results, provided_key) do
case results do
[map] ->
# Map found - verify with constant-time comparison
stored_key = map.public_api_key || @dummy_key
if Plug.Crypto.secure_compare(stored_key, provided_key) do
{:ok, [map]}
else
# Keys don't match (shouldn't happen if DB returned it, but safety check)
{:ok, []}
end
[] ->
# No map found - still do a comparison to maintain consistent timing
# This prevents timing attacks from distinguishing "not found" from "found but wrong"
_result = Plug.Crypto.secure_compare(@dummy_key, provided_key)
{:ok, []}
_multiple ->
# Multiple results - shouldn't happen with unique constraint
# Do comparison for timing consistency and return error
_result = Plug.Crypto.secure_compare(@dummy_key, provided_key)
{:ok, []}
end
end
end

View File

@@ -49,11 +49,7 @@ defmodule WandererApp.Api.ShipTypeInfo do
:volume
]
defaults [:read, :destroy]
update :update do
require_atomic? false
end
defaults [:read, :destroy, :update]
create :create do
primary? true

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