mirror of
				https://github.com/wanderer-industries/wanderer
				synced 2025-11-04 00:14:52 +00:00 
			
		
		
		
	Compare commits
	
		
			95 Commits
		
	
	
		
			v1.75.13
			...
			tracking-f
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					46c1ccdfcc | ||
| 
						 | 
					8817536038 | ||
| 
						 | 
					c3bb23a6ee | ||
| 
						 | 
					7e9c4c575e | ||
| 
						 | 
					5a70eee91e | ||
| 
						 | 
					228f6990a1 | ||
| 
						 | 
					d80ed0e70e | ||
| 
						 | 
					4576c75737 | ||
| 
						 | 
					67764faaa7 | ||
| 
						 | 
					91dd0b27ae | ||
| 
						 | 
					99dcf49fbc | ||
| 
						 | 
					6fb3edbfd6 | ||
| 
						 | 
					26f13ce857 | ||
| 
						 | 
					e9b475c0a8 | ||
| 
						 | 
					7752010092 | ||
| 
						 | 
					d3705b3ed7 | ||
| 
						 | 
					1394e2897e | ||
| 
						 | 
					5117a1c5af | ||
| 
						 | 
					3c62403f33 | ||
| 
						 | 
					a4760f5162 | ||
| 
						 | 
					b071070431 | ||
| 
						 | 
					3bcb9628e7 | ||
| 
						 | 
					e62c4cf5bf | ||
| 
						 | 
					af46962ce4 | ||
| 
						 | 
					0b0967830b | ||
| 
						 | 
					172251a208 | ||
| 
						 | 
					8a6fb63d55 | ||
| 
						 | 
					9652959e5e | ||
| 
						 | 
					825ef46d41 | ||
| 
						 | 
					ad9f7c6b95 | ||
| 
						 | 
					b960b5c149 | ||
| 
						 | 
					0f092d21f9 | ||
| 
						 | 
					031576caa6 | ||
| 
						 | 
					7a97a96c42 | ||
| 
						 | 
					2efb2daba0 | ||
| 
						 | 
					4374c39924 | ||
| 
						 | 
					15711495c7 | ||
| 
						 | 
					236f803427 | ||
| 
						 | 
					6772130f2a | ||
| 
						 | 
					ddd72f3fac | ||
| 
						 | 
					6e262835ef | ||
| 
						 | 
					2f3b8ddc5f | ||
| 
						 | 
					cea3a74b34 | ||
| 
						 | 
					867941a233 | ||
| 
						 | 
					3ff388a16d | ||
| 
						 | 
					f4248e9ab9 | ||
| 
						 | 
					507b3289c7 | ||
| 
						 | 
					9e1dfc48d5 | ||
| 
						 | 
					518cbc7b5d | ||
| 
						 | 
					ccc8db0620 | ||
| 
						 | 
					7cfb663efd | ||
| 
						 | 
					e5103cc925 | ||
| 
						 | 
					26458f5a19 | ||
| 
						 | 
					79d5ec6caf | ||
| 
						 | 
					034d461ab6 | ||
| 
						 | 
					2e9c1c170c | ||
| 
						 | 
					24ad3b2c61 | ||
| 
						 | 
					288f55dc2f | ||
| 
						 | 
					78dbea6267 | ||
| 
						 | 
					6a9e53141d | ||
| 
						 | 
					05e6994520 | ||
| 
						 | 
					1a4dc67eb9 | ||
| 
						 | 
					31d87a116b | ||
| 
						 | 
					c47796d590 | ||
| 
						 | 
					c7138a41ee | ||
| 
						 | 
					96f04c70a9 | ||
| 
						 | 
					87a8bc09ab | ||
| 
						 | 
					5f5661d559 | ||
| 
						 | 
					35ca87790e | ||
| 
						 | 
					ae43e4a57c | ||
| 
						 | 
					b91712a01a | ||
| 
						 | 
					b20007b341 | ||
| 
						 | 
					6a24e1188b | ||
| 
						 | 
					5894efc1aa | ||
| 
						 | 
					a05612d243 | ||
| 
						 | 
					48de874d6b | ||
| 
						 | 
					91e6da316f | ||
| 
						 | 
					fa60bd81a1 | ||
| 
						 | 
					a08a69c5be | ||
| 
						 | 
					18d450a41a | ||
| 
						 | 
					36cdee61c0 | ||
| 
						 | 
					797e188259 | ||
| 
						 | 
					91b581668a | ||
| 
						 | 
					ad01fec28f | ||
| 
						 | 
					357d3a0df6 | ||
| 
						 | 
					5ce6022761 | ||
| 
						 | 
					235a0c5aea | ||
| 
						 | 
					9b81fa6ebb | ||
| 
						 | 
					8792d5ab0e | ||
| 
						 | 
					d46ed0c078 | ||
| 
						 | 
					73c433fcd2 | ||
| 
						 | 
					02b5239220 | ||
| 
						 | 
					74f7ad155d | ||
| 
						 | 
					f58ebad0ec | ||
| 
						 | 
					7ca4eb3b8f | 
							
								
								
									
										1
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/build.yml
									
									
									
									
										vendored
									
									
								
							@@ -96,6 +96,7 @@ jobs:
 | 
			
		||||
          git config --global user.name 'CI'
 | 
			
		||||
          git config --global user.email 'ci@users.noreply.github.com'
 | 
			
		||||
          mix git_ops.release --force-patch --yes
 | 
			
		||||
          git commit --allow-empty -m 'chore: [skip ci]'
 | 
			
		||||
          git push --follow-tags
 | 
			
		||||
          echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								.github/workflows/docker-arm.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										11
									
								
								.github/workflows/docker-arm.yml
									
									
									
									
										vendored
									
									
								
							@@ -43,6 +43,11 @@ jobs:
 | 
			
		||||
          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"
 | 
			
		||||
@@ -118,11 +123,9 @@ jobs:
 | 
			
		||||
        id: get-content
 | 
			
		||||
        with:
 | 
			
		||||
          stringToTruncate: |
 | 
			
		||||
            📣 Wanderer **ARM** release available 🎉
 | 
			
		||||
            📣 Wanderer **ARM**  release available 🎉
 | 
			
		||||
 | 
			
		||||
            [wandererltd/community-edition-arm:${{ steps.get-latest-tag.outputs.tag }}](https://hub.docker.com/r/wandererltd/community-edition-arm/tags)
 | 
			
		||||
 | 
			
		||||
            **Version**: ${{ steps.get-latest-tag.outputs.tag }}
 | 
			
		||||
            **Version**: :${{ steps.get-latest-tag.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
            ${{ steps.extract-changelog.outputs.body }}
 | 
			
		||||
          maxLength: 500
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -43,6 +43,11 @@ jobs:
 | 
			
		||||
          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"
 | 
			
		||||
@@ -120,8 +125,6 @@ jobs:
 | 
			
		||||
          stringToTruncate: |
 | 
			
		||||
            📣 Wanderer new release available 🎉
 | 
			
		||||
 | 
			
		||||
            [wandererltd/community-edition:${{ steps.get-latest-tag.outputs.tag }}](https://hub.docker.com/r/wandererltd/community-edition/tags)
 | 
			
		||||
 | 
			
		||||
            **Version**: ${{ steps.get-latest-tag.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
            ${{ steps.extract-changelog.outputs.body }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1796
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										1796
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,18 +1,13 @@
 | 
			
		||||
// import './tailwind.css';
 | 
			
		||||
//@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css';
 | 
			
		||||
//@import 'primereact/resources/themes/lara-dark-purple/theme.css';
 | 
			
		||||
//@import "prime-fixes";
 | 
			
		||||
@import 'primereact/resources/primereact.min.css';
 | 
			
		||||
//@import 'primeflex/primeflex.css';
 | 
			
		||||
@import 'primeicons/primeicons.css';
 | 
			
		||||
//@import 'primereact/resources/primereact.css';
 | 
			
		||||
@use 'primereact/resources/primereact.min.css';
 | 
			
		||||
@use 'primeicons/primeicons.css';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@import "fixes";
 | 
			
		||||
@import "prime-fixes";
 | 
			
		||||
@import "custom-scrollbar";
 | 
			
		||||
@import "tooltip";
 | 
			
		||||
@import "context-menu";
 | 
			
		||||
@use "fixes";
 | 
			
		||||
@use "prime-fixes";
 | 
			
		||||
@use "custom-scrollbar";
 | 
			
		||||
@use "tooltip";
 | 
			
		||||
@use "context-menu";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.fixedImportant {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,3 @@
 | 
			
		||||
@import "fix-dialog";
 | 
			
		||||
@import "fix-popup";
 | 
			
		||||
@import "fix-tabs";
 | 
			
		||||
//@import "fix-input";
 | 
			
		||||
 | 
			
		||||
//@import "theme";
 | 
			
		||||
@use "fix-dialog";
 | 
			
		||||
@use "fix-popup";
 | 
			
		||||
@use "fix-tabs";
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
 | 
			
		||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
 | 
			
		||||
 | 
			
		||||
.ConnectionTimeEOL {
 | 
			
		||||
  background-image: linear-gradient(207deg, transparent, var(--conn-time-eol));
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
 | 
			
		||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
 | 
			
		||||
 | 
			
		||||
.EdgePathBack {
 | 
			
		||||
  fill: none;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
 | 
			
		||||
@use "sass:color";
 | 
			
		||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
 | 
			
		||||
 | 
			
		||||
$pastel-blue: #5a7d9a;
 | 
			
		||||
$pastel-pink: rgb(30, 161, 255);
 | 
			
		||||
@@ -34,7 +35,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
 | 
			
		||||
  color: var(--rf-text-color, #ffffff);
 | 
			
		||||
 | 
			
		||||
  box-shadow: 0 0 5px rgba($dark-bg, 0.5);
 | 
			
		||||
  border: 1px solid darken($pastel-blue, 10%);
 | 
			
		||||
  border: 1px solid color.adjust($pastel-blue, $lightness: -10%);
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  position: relative;
 | 
			
		||||
  z-index: 3;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
@import './SolarSystemNodeDefault.module.scss';
 | 
			
		||||
@use './SolarSystemNodeDefault.module.scss';
 | 
			
		||||
 | 
			
		||||
/* ---------------------------------------------
 | 
			
		||||
   Only override what's different from the base
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
 | 
			
		||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
 | 
			
		||||
 | 
			
		||||
.Signature {
 | 
			
		||||
  position: relative;
 | 
			
		||||
 
 | 
			
		||||
@@ -6,5 +6,5 @@ export * from './useCommandsCharacters';
 | 
			
		||||
export * from './useCommandsConnections';
 | 
			
		||||
export * from './useCommandsConnections';
 | 
			
		||||
export * from './useCenterSystem';
 | 
			
		||||
export * from './useSelectSystem';
 | 
			
		||||
export * from './useSelectSystems';
 | 
			
		||||
export * from './useMapCommands';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +0,0 @@
 | 
			
		||||
import { useReactFlow } from 'reactflow';
 | 
			
		||||
import { useCallback, useRef } from 'react';
 | 
			
		||||
import { CommandSelectSystem } from '@/hooks/Mapper/types';
 | 
			
		||||
 | 
			
		||||
export const useSelectSystem = () => {
 | 
			
		||||
  const rf = useReactFlow();
 | 
			
		||||
 | 
			
		||||
  const ref = useRef({ rf });
 | 
			
		||||
  ref.current = { rf };
 | 
			
		||||
 | 
			
		||||
  return useCallback((systemId: CommandSelectSystem) => {
 | 
			
		||||
    ref.current.rf.setNodes(nds =>
 | 
			
		||||
      nds.map(node => {
 | 
			
		||||
        return {
 | 
			
		||||
          ...node,
 | 
			
		||||
          selected: node.id === systemId,
 | 
			
		||||
        };
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  }, []);
 | 
			
		||||
};
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
import { useReactFlow } from 'reactflow';
 | 
			
		||||
import { useCallback, useRef } from 'react';
 | 
			
		||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
 | 
			
		||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
 | 
			
		||||
 | 
			
		||||
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
 | 
			
		||||
  const rf = useReactFlow();
 | 
			
		||||
 | 
			
		||||
  const ref = useRef({ rf, onSelectionChange });
 | 
			
		||||
  ref.current = { rf, onSelectionChange };
 | 
			
		||||
 | 
			
		||||
  return useCallback(({ systems, delay }: CommandSelectSystems) => {
 | 
			
		||||
    const run = () => {
 | 
			
		||||
      ref.current.rf.setNodes(nds =>
 | 
			
		||||
        nds.map(node => {
 | 
			
		||||
          return {
 | 
			
		||||
            ...node,
 | 
			
		||||
            selected: systems.includes(node.id),
 | 
			
		||||
          };
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (delay == null || delay === 0) {
 | 
			
		||||
      run();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    setTimeout(run, delay);
 | 
			
		||||
  }, []);
 | 
			
		||||
};
 | 
			
		||||
@@ -14,6 +14,7 @@ import {
 | 
			
		||||
  CommandRemoveSystems,
 | 
			
		||||
  Commands,
 | 
			
		||||
  CommandSelectSystem,
 | 
			
		||||
  CommandSelectSystems,
 | 
			
		||||
  CommandUpdateConnection,
 | 
			
		||||
  CommandUpdateSystems,
 | 
			
		||||
  MapHandlers,
 | 
			
		||||
@@ -28,7 +29,7 @@ import {
 | 
			
		||||
  useMapRemoveSystems,
 | 
			
		||||
  useMapUpdateSystems,
 | 
			
		||||
  useCenterSystem,
 | 
			
		||||
  useSelectSystem,
 | 
			
		||||
  useSelectSystems,
 | 
			
		||||
} from './api';
 | 
			
		||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
 | 
			
		||||
 | 
			
		||||
@@ -38,7 +39,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
 | 
			
		||||
  const mapUpdateSystems = useMapUpdateSystems();
 | 
			
		||||
  const removeSystems = useMapRemoveSystems(onSelectionChange);
 | 
			
		||||
  const centerSystem = useCenterSystem();
 | 
			
		||||
  const selectSystem = useSelectSystem();
 | 
			
		||||
  const selectSystems = useSelectSystems(onSelectionChange);
 | 
			
		||||
 | 
			
		||||
  const selectRef = useRef({ onSelectionChange });
 | 
			
		||||
  selectRef.current = { onSelectionChange };
 | 
			
		||||
@@ -105,14 +106,11 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            case Commands.selectSystem:
 | 
			
		||||
              setTimeout(() => {
 | 
			
		||||
                const systemId = `${data}`;
 | 
			
		||||
                selectRef.current.onSelectionChange({
 | 
			
		||||
                  systems: [systemId],
 | 
			
		||||
                  connections: [],
 | 
			
		||||
                });
 | 
			
		||||
                selectSystem(systemId as CommandSelectSystem);
 | 
			
		||||
              }, 500);
 | 
			
		||||
              selectSystems({ systems: [data as string], delay: 500 });
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            case Commands.selectSystems:
 | 
			
		||||
              selectSystems(data as CommandSelectSystems);
 | 
			
		||||
              break;
 | 
			
		||||
 | 
			
		||||
            case Commands.pingAdded:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
@import './eve-common-variables';
 | 
			
		||||
@import './eve-common';
 | 
			
		||||
@use './eve-common-variables';
 | 
			
		||||
@use './eve-common';
 | 
			
		||||
 | 
			
		||||
.default-theme {
 | 
			
		||||
  --rf-bg-color: #0C0A09;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,19 @@
 | 
			
		||||
@use "sass:color";
 | 
			
		||||
 | 
			
		||||
$friendlyBase: #3bbd39;
 | 
			
		||||
$friendlyAlpha: #3bbd3952;
 | 
			
		||||
$friendlyDark20: darken($friendlyBase, 20%);
 | 
			
		||||
$friendlyDark30: darken($friendlyBase, 30%);
 | 
			
		||||
$friendlyDark5:  darken($friendlyBase, 5%);
 | 
			
		||||
$friendlyDark20: color.adjust($friendlyBase, $lightness: -20%);
 | 
			
		||||
$friendlyDark30: color.adjust($friendlyBase, $lightness: -30%);
 | 
			
		||||
$friendlyDark5:  color.adjust($friendlyBase, $lightness: -5%);
 | 
			
		||||
 | 
			
		||||
$lookingForBase: #43c2fd;
 | 
			
		||||
$lookingForAlpha: rgba(67, 176, 253, 0.48);
 | 
			
		||||
$lookingForDark15: darken($lookingForBase, 15%);
 | 
			
		||||
$lookingForDark15: color.adjust($lookingForBase, $lightness: -15%);
 | 
			
		||||
 | 
			
		||||
$homeBase: rgb(179, 253, 67);
 | 
			
		||||
$homeAlpha: rgba(186, 248, 48, 0.32);
 | 
			
		||||
$homeBackground: #a0fa5636;
 | 
			
		||||
$homeDark30: darken($homeBase, 30%);
 | 
			
		||||
$homeDark30: color.adjust($homeBase, $lightness: -30%);
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
  --pastel-blue: #5a7d9a;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
@import './eve-common-variables';
 | 
			
		||||
@use './eve-common-variables';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
.eve-wh-effect-color-pulsar {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,2 +1,2 @@
 | 
			
		||||
@import './default-theme.scss'; 
 | 
			
		||||
@import './pathfinder-theme.scss'; 
 | 
			
		||||
@use './default-theme.scss'; 
 | 
			
		||||
@use './pathfinder-theme.scss'; 
 | 
			
		||||
@@ -1,10 +1,11 @@
 | 
			
		||||
@import './eve-common-variables';
 | 
			
		||||
@import './eve-common';
 | 
			
		||||
@use "sass:color";
 | 
			
		||||
@use './eve-common-variables';
 | 
			
		||||
@use './eve-common';
 | 
			
		||||
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
 | 
			
		||||
 | 
			
		||||
$homeBase: rgb(197, 253, 67);
 | 
			
		||||
$homeAlpha: rgba(197, 253, 67, 0.32);
 | 
			
		||||
$homeDark30: darken($homeBase, 30%);
 | 
			
		||||
$homeDark30: color.adjust($homeBase, $lightness: -30%);
 | 
			
		||||
 | 
			
		||||
.pathfinder-theme {
 | 
			
		||||
  /* -- Override values from the default theme -- */
 | 
			
		||||
 
 | 
			
		||||
@@ -28,12 +28,12 @@ import {
 | 
			
		||||
  renderInfoColumn,
 | 
			
		||||
  renderUpdatedTimeLeft,
 | 
			
		||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
 | 
			
		||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
 | 
			
		||||
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
 | 
			
		||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
 | 
			
		||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
 | 
			
		||||
import { getSignatureRowClass } from '../helpers/rowStyles';
 | 
			
		||||
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
 | 
			
		||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
 | 
			
		||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
 | 
			
		||||
 | 
			
		||||
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
 | 
			
		||||
 | 
			
		||||
@@ -157,9 +157,18 @@ export const SystemSignaturesContent = ({
 | 
			
		||||
    [onSelect, selectable, setSelectedSignatures, deletedSignatures],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
 | 
			
		||||
  const {
 | 
			
		||||
    showGroupColumn,
 | 
			
		||||
    showDescriptionColumn,
 | 
			
		||||
    showAddedColumn,
 | 
			
		||||
    showUpdatedColumn,
 | 
			
		||||
    showCharacterColumn,
 | 
			
		||||
    showCharacterPortrait,
 | 
			
		||||
  } = useMemo(
 | 
			
		||||
    () => ({
 | 
			
		||||
      showGroupColumn: settings[SETTINGS_KEYS.SHOW_GROUP_COLUMN] as boolean,
 | 
			
		||||
      showDescriptionColumn: settings[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN] as boolean,
 | 
			
		||||
      showAddedColumn: settings[SETTINGS_KEYS.SHOW_ADDED_COLUMN] as boolean,
 | 
			
		||||
      showUpdatedColumn: settings[SETTINGS_KEYS.SHOW_UPDATED_COLUMN] as boolean,
 | 
			
		||||
      showCharacterColumn: settings[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN] as boolean,
 | 
			
		||||
      showCharacterPortrait: settings[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT] as boolean,
 | 
			
		||||
@@ -309,15 +318,17 @@ export const SystemSignaturesContent = ({
 | 
			
		||||
              style={{ maxWidth: 72, minWidth: 72, width: 72 }}
 | 
			
		||||
              sortable
 | 
			
		||||
            />
 | 
			
		||||
            <Column
 | 
			
		||||
              field="group"
 | 
			
		||||
              header="Group"
 | 
			
		||||
              bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
 | 
			
		||||
              style={{ maxWidth: 110, minWidth: 110, width: 110 }}
 | 
			
		||||
              body={sig => sig.group ?? ''}
 | 
			
		||||
              hidden={isCompact}
 | 
			
		||||
              sortable
 | 
			
		||||
            />
 | 
			
		||||
            {showGroupColumn && (
 | 
			
		||||
              <Column
 | 
			
		||||
                field="group"
 | 
			
		||||
                header="Group"
 | 
			
		||||
                bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
 | 
			
		||||
                style={{ maxWidth: 110, minWidth: 110, width: 110 }}
 | 
			
		||||
                body={sig => sig.group ?? ''}
 | 
			
		||||
                hidden={isCompact}
 | 
			
		||||
                sortable
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <Column
 | 
			
		||||
              field="info"
 | 
			
		||||
              header="Info"
 | 
			
		||||
@@ -336,15 +347,17 @@ export const SystemSignaturesContent = ({
 | 
			
		||||
                sortable
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <Column
 | 
			
		||||
              field="inserted_at"
 | 
			
		||||
              header="Added"
 | 
			
		||||
              dataType="date"
 | 
			
		||||
              body={renderAddedTimeLeft}
 | 
			
		||||
              style={{ minWidth: 70, maxWidth: 80 }}
 | 
			
		||||
              bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
 | 
			
		||||
              sortable
 | 
			
		||||
            />
 | 
			
		||||
            {showAddedColumn && (
 | 
			
		||||
              <Column
 | 
			
		||||
                field="inserted_at"
 | 
			
		||||
                header="Added"
 | 
			
		||||
                dataType="date"
 | 
			
		||||
                body={renderAddedTimeLeft}
 | 
			
		||||
                style={{ minWidth: 70, maxWidth: 80 }}
 | 
			
		||||
                bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
 | 
			
		||||
                sortable
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            {showUpdatedColumn && (
 | 
			
		||||
              <Column
 | 
			
		||||
                field="updated_at"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
 | 
			
		||||
import {
 | 
			
		||||
  GroupType,
 | 
			
		||||
  SignatureGroup,
 | 
			
		||||
@@ -11,7 +12,6 @@ import {
 | 
			
		||||
  SignatureKindFR,
 | 
			
		||||
  SignatureKindRU,
 | 
			
		||||
} from '@/hooks/Mapper/types';
 | 
			
		||||
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
 | 
			
		||||
 | 
			
		||||
export const TIME_ONE_MINUTE = 1000 * 60;
 | 
			
		||||
export const TIME_TEN_MINUTES = TIME_ONE_MINUTE * 10;
 | 
			
		||||
@@ -130,6 +130,8 @@ export const SIGNATURE_SETTINGS = {
 | 
			
		||||
    { type: SettingsTypes.flag, key: SETTINGS_KEYS.COMBAT_SITE, name: 'Show Combat Sites' },
 | 
			
		||||
  ],
 | 
			
		||||
  uiFlags: [
 | 
			
		||||
    { type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_GROUP_COLUMN, name: 'Show Group Column' },
 | 
			
		||||
    { type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_ADDED_COLUMN, name: 'Show Added Column' },
 | 
			
		||||
    { type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_UPDATED_COLUMN, name: 'Show Updated Column' },
 | 
			
		||||
    { type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN, name: 'Show Description Column' },
 | 
			
		||||
    { type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_CHARACTER_COLUMN, name: 'Show Character Column' },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,6 @@
 | 
			
		||||
import { Dialog } from 'primereact/dialog';
 | 
			
		||||
import { Button } from 'primereact/button';
 | 
			
		||||
import { ConfirmPopup } from 'primereact/confirmpopup';
 | 
			
		||||
import { useCallback, useRef } from 'react';
 | 
			
		||||
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
 | 
			
		||||
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
 | 
			
		||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
 | 
			
		||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
 | 
			
		||||
import {
 | 
			
		||||
  DEFAULT_KILLS_WIDGET_SETTINGS,
 | 
			
		||||
  DEFAULT_ON_THE_MAP_SETTINGS,
 | 
			
		||||
@@ -11,11 +9,13 @@ import {
 | 
			
		||||
  getDefaultWidgetProps,
 | 
			
		||||
  STORED_INTERFACE_DEFAULT_VALUES,
 | 
			
		||||
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
 | 
			
		||||
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
 | 
			
		||||
import { Toast } from 'primereact/toast';
 | 
			
		||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
 | 
			
		||||
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
 | 
			
		||||
import { saveTextFile } from '@/hooks/Mapper/utils';
 | 
			
		||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
 | 
			
		||||
import { Button } from 'primereact/button';
 | 
			
		||||
import { ConfirmPopup } from 'primereact/confirmpopup';
 | 
			
		||||
import { Dialog } from 'primereact/dialog';
 | 
			
		||||
import { Toast } from 'primereact/toast';
 | 
			
		||||
import { useCallback, useRef } from 'react';
 | 
			
		||||
 | 
			
		||||
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
 | 
			
		||||
  return {
 | 
			
		||||
@@ -41,7 +41,7 @@ export const OldSettingsDialog = () => {
 | 
			
		||||
      const widgetKills = localStorage.getItem('kills:widget:settings');
 | 
			
		||||
      const onTheMapOld = localStorage.getItem('window:onTheMap:settings');
 | 
			
		||||
      const widgetsOld = localStorage.getItem('windows:settings:v2');
 | 
			
		||||
      const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_5');
 | 
			
		||||
      const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_6');
 | 
			
		||||
 | 
			
		||||
      const out: MapUserSettings = {
 | 
			
		||||
        killsWidget: createSettings(widgetKills, DEFAULT_KILLS_WIDGET_SETTINGS),
 | 
			
		||||
@@ -118,7 +118,7 @@ export const OldSettingsDialog = () => {
 | 
			
		||||
    localStorage.removeItem('kills:widget:settings');
 | 
			
		||||
    localStorage.removeItem('window:onTheMap:settings');
 | 
			
		||||
    localStorage.removeItem('windows:settings:v2');
 | 
			
		||||
    localStorage.removeItem('wanderer_system_signature_settings_v6_5');
 | 
			
		||||
    localStorage.removeItem('wanderer_system_signature_settings_v6_6');
 | 
			
		||||
 | 
			
		||||
    checkOldSettings();
 | 
			
		||||
  }, [checkOldSettings]);
 | 
			
		||||
 
 | 
			
		||||
@@ -94,6 +94,10 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
 | 
			
		||||
            out = { ...out, type: values.type };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (values.temporary_name != null) {
 | 
			
		||||
            out = { ...out, temporary_name: values.temporary_name };
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (signatureData.group !== SignatureGroup.Wormhole) {
 | 
			
		||||
            out = { ...out, name: '' };
 | 
			
		||||
          }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import { SignatureWormholeTypeSelect } from '@/hooks/Mapper/components/mapRootCo
 | 
			
		||||
import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
 | 
			
		||||
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
 | 
			
		||||
import { SignatureEOLCheckbox } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureEOLCheckbox';
 | 
			
		||||
import { SignatureTempName } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureTempName.tsx';
 | 
			
		||||
 | 
			
		||||
export const SignatureGroupContentWormholes = () => {
 | 
			
		||||
  const { watch } = useFormContext<SystemSignature>();
 | 
			
		||||
@@ -32,6 +33,11 @@ export const SignatureGroupContentWormholes = () => {
 | 
			
		||||
        <span>EOL:</span>
 | 
			
		||||
        <SignatureEOLCheckbox name="isEOL" />
 | 
			
		||||
      </label>
 | 
			
		||||
 | 
			
		||||
      <label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
 | 
			
		||||
        <span>Temp. Name:</span>
 | 
			
		||||
        <SignatureTempName />
 | 
			
		||||
      </label>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
import { Controller, useFormContext } from 'react-hook-form';
 | 
			
		||||
import { InputText } from 'primereact/inputtext';
 | 
			
		||||
import { SystemSignature } from '@/hooks/Mapper/types';
 | 
			
		||||
 | 
			
		||||
export const SignatureTempName = () => {
 | 
			
		||||
  const { control } = useFormContext<SystemSignature>();
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Controller
 | 
			
		||||
      name="temporary_name"
 | 
			
		||||
      control={control}
 | 
			
		||||
      render={({ field }) => <InputText placeholder="Temporary Name" value={field.value} onChange={field.onChange} />}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
 | 
			
		||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 | 
			
		||||
import { OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
 | 
			
		||||
import { CommandSelectSystems, OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
 | 
			
		||||
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
 | 
			
		||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
 | 
			
		||||
import isEqual from 'lodash.isequal';
 | 
			
		||||
@@ -88,6 +88,18 @@ export const MapWrapper = () => {
 | 
			
		||||
 | 
			
		||||
  useMapEventListener(event => {
 | 
			
		||||
    runCommand(event);
 | 
			
		||||
 | 
			
		||||
    if (event.name === Commands.init) {
 | 
			
		||||
      const { selectedSystems } = ref.current;
 | 
			
		||||
      if (selectedSystems.length === 0) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      runCommand({
 | 
			
		||||
        name: Commands.selectSystems,
 | 
			
		||||
        data: { systems: selectedSystems } as CommandSelectSystems,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const onSelectionChange: OnMapSelectionChange = useCallback(
 | 
			
		||||
 
 | 
			
		||||
@@ -12,14 +12,16 @@ export enum SETTINGS_KEYS {
 | 
			
		||||
  SORT_FIELD = 'sortField',
 | 
			
		||||
  SORT_ORDER = 'sortOrder',
 | 
			
		||||
 | 
			
		||||
  SHOW_DESCRIPTION_COLUMN = 'show_description_column',
 | 
			
		||||
  SHOW_UPDATED_COLUMN = 'show_updated_column',
 | 
			
		||||
  SHOW_ADDED_COLUMN = 'show_added_column',
 | 
			
		||||
  SHOW_CHARACTER_COLUMN = 'show_character_column',
 | 
			
		||||
  SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
 | 
			
		||||
  SHOW_DESCRIPTION_COLUMN = 'show_description_column',
 | 
			
		||||
  SHOW_GROUP_COLUMN = 'show_group_column',
 | 
			
		||||
  SHOW_UPDATED_COLUMN = 'show_updated_column',
 | 
			
		||||
  LAZY_DELETE_SIGNATURES = 'lazy_delete_signatures',
 | 
			
		||||
  KEEP_LAZY_DELETE = 'keep_lazy_delete_enabled',
 | 
			
		||||
  DELETION_TIMING = 'deletion_timing',
 | 
			
		||||
  COLOR_BY_TYPE = 'color_by_type',
 | 
			
		||||
  SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
 | 
			
		||||
 | 
			
		||||
  // From SignatureKind
 | 
			
		||||
  COSMIC_ANOMALY = SignatureKind.CosmicAnomaly,
 | 
			
		||||
@@ -45,6 +47,8 @@ export const DEFAULT_SIGNATURE_SETTINGS: SignatureSettingsType = {
 | 
			
		||||
  [SETTINGS_KEYS.SORT_FIELD]: 'inserted_at',
 | 
			
		||||
  [SETTINGS_KEYS.SORT_ORDER]: -1,
 | 
			
		||||
 | 
			
		||||
  [SETTINGS_KEYS.SHOW_GROUP_COLUMN]: true,
 | 
			
		||||
  [SETTINGS_KEYS.SHOW_ADDED_COLUMN]: true,
 | 
			
		||||
  [SETTINGS_KEYS.SHOW_UPDATED_COLUMN]: true,
 | 
			
		||||
  [SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
 | 
			
		||||
  [SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ export enum Commands {
 | 
			
		||||
  userRoutes = 'user_routes',
 | 
			
		||||
  centerSystem = 'center_system',
 | 
			
		||||
  selectSystem = 'select_system',
 | 
			
		||||
  selectSystems = 'select_systems',
 | 
			
		||||
  linkSignatureToSystem = 'link_signature_to_system',
 | 
			
		||||
  signaturesUpdated = 'signatures_updated',
 | 
			
		||||
  systemCommentAdded = 'system_comment_added',
 | 
			
		||||
@@ -60,6 +61,7 @@ export type Command =
 | 
			
		||||
  | Commands.routes
 | 
			
		||||
  | Commands.userRoutes
 | 
			
		||||
  | Commands.selectSystem
 | 
			
		||||
  | Commands.selectSystems
 | 
			
		||||
  | Commands.centerSystem
 | 
			
		||||
  | Commands.linkSignatureToSystem
 | 
			
		||||
  | Commands.signaturesUpdated
 | 
			
		||||
@@ -118,6 +120,10 @@ export type CommandUserRoutes = RoutesList;
 | 
			
		||||
export type CommandKillsUpdated = Kill[];
 | 
			
		||||
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
 | 
			
		||||
export type CommandSelectSystem = string | undefined;
 | 
			
		||||
export type CommandSelectSystems = {
 | 
			
		||||
  systems: string[];
 | 
			
		||||
  delay?: number;
 | 
			
		||||
};
 | 
			
		||||
export type CommandCenterSystem = string | undefined;
 | 
			
		||||
export type CommandLinkSignatureToSystem = {
 | 
			
		||||
  solar_system_source: number;
 | 
			
		||||
@@ -187,6 +193,7 @@ export interface CommandData {
 | 
			
		||||
  [Commands.killsUpdated]: CommandKillsUpdated;
 | 
			
		||||
  [Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
 | 
			
		||||
  [Commands.selectSystem]: CommandSelectSystem;
 | 
			
		||||
  [Commands.selectSystems]: CommandSelectSystems;
 | 
			
		||||
  [Commands.centerSystem]: CommandCenterSystem;
 | 
			
		||||
  [Commands.linkSignatureToSystem]: CommandLinkSignatureToSystem;
 | 
			
		||||
  [Commands.signaturesUpdated]: CommandLinkSignaturesUpdated;
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,7 @@ export type SystemSignature = {
 | 
			
		||||
  inserted_at?: string;
 | 
			
		||||
  updated_at?: string;
 | 
			
		||||
  deleted?: boolean;
 | 
			
		||||
  temporary_name?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface ExtendedSystemSignature extends SystemSignature {
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,7 @@
 | 
			
		||||
    "sass-loader": "^14.2.1",
 | 
			
		||||
    "ts-jest": "^29.1.2",
 | 
			
		||||
    "typescript": "^5.2.2",
 | 
			
		||||
    "vite": "^5.0.5",
 | 
			
		||||
    "vite": "^6.3.5",
 | 
			
		||||
    "vite-plugin-cdn-import": "^1.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "peerDependencies": {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5377
									
								
								assets/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										5377
									
								
								assets/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										82
									
								
								clean_changelog.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								clean_changelog.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,82 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
"""
 | 
			
		||||
Script to clean up CHANGELOG.md by removing empty version entries.
 | 
			
		||||
An empty version entry has only a version header followed by empty lines,
 | 
			
		||||
without any actual content (### Bug Fixes: or ### Features: sections).
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
def clean_changelog():
 | 
			
		||||
    with open('./CHANGELOG.md', 'r') as f:
 | 
			
		||||
        content = f.read()
 | 
			
		||||
 | 
			
		||||
    # Split content into sections based on version headers
 | 
			
		||||
    version_pattern = r'^## \[v\d+\.\d+\.\d+\].*?\([^)]+\)$'
 | 
			
		||||
 | 
			
		||||
    # Find all version headers with their positions
 | 
			
		||||
    matches = list(re.finditer(version_pattern, content, re.MULTILINE))
 | 
			
		||||
 | 
			
		||||
    # Build new content by keeping only non-empty versions
 | 
			
		||||
    new_content = ""
 | 
			
		||||
 | 
			
		||||
    # Keep the header (everything before first version)
 | 
			
		||||
    if matches:
 | 
			
		||||
        new_content += content[:matches[0].start()]
 | 
			
		||||
    else:
 | 
			
		||||
        # No versions found, keep original
 | 
			
		||||
        return content
 | 
			
		||||
 | 
			
		||||
    for i, match in enumerate(matches):
 | 
			
		||||
        version_start = match.start()
 | 
			
		||||
 | 
			
		||||
        # Find the end of this version section (start of next version or end of file)
 | 
			
		||||
        if i + 1 < len(matches):
 | 
			
		||||
            version_end = matches[i + 1].start()
 | 
			
		||||
        else:
 | 
			
		||||
            version_end = len(content)
 | 
			
		||||
 | 
			
		||||
        version_section = content[version_start:version_end]
 | 
			
		||||
 | 
			
		||||
        # Check if this version has actual content
 | 
			
		||||
        # Look for ### Bug Fixes: or ### Features: followed by actual content
 | 
			
		||||
        has_content = False
 | 
			
		||||
 | 
			
		||||
        # Split the section into lines
 | 
			
		||||
        lines = version_section.split('\n')
 | 
			
		||||
 | 
			
		||||
        # Look for content sections
 | 
			
		||||
        in_content_section = False
 | 
			
		||||
        for line in lines:
 | 
			
		||||
            line_stripped = line.strip()
 | 
			
		||||
 | 
			
		||||
            # Check if we're entering a content section
 | 
			
		||||
            if line_stripped.startswith('### Bug Fixes:') or line_stripped.startswith('### Features:'):
 | 
			
		||||
                in_content_section = True
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            # If we're in a content section and find non-empty content
 | 
			
		||||
            if in_content_section:
 | 
			
		||||
                if line_stripped and not line_stripped.startswith('###') and not line_stripped.startswith('##'):
 | 
			
		||||
                    # This is actual content (not just another header)
 | 
			
		||||
                    if line_stripped.startswith('*') or len(line_stripped) > 0:
 | 
			
		||||
                        has_content = True
 | 
			
		||||
                        break
 | 
			
		||||
                elif line_stripped.startswith('##'):
 | 
			
		||||
                    # We've reached the next version, stop looking
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
        # Only keep versions with actual content
 | 
			
		||||
        if has_content:
 | 
			
		||||
            new_content += version_section
 | 
			
		||||
 | 
			
		||||
    return new_content
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    cleaned_content = clean_changelog()
 | 
			
		||||
 | 
			
		||||
    # Write the cleaned content back to the file
 | 
			
		||||
    with open('./CHANGELOG.md', 'w') as f:
 | 
			
		||||
        f.write(cleaned_content)
 | 
			
		||||
 | 
			
		||||
    print("CHANGELOG.md has been cleaned up successfully!")
 | 
			
		||||
@@ -11,11 +11,13 @@ config :wanderer_app, WandererAppWeb.Endpoint,
 | 
			
		||||
config :wanderer_app, WandererApp.Repo,
 | 
			
		||||
  ssl: false,
 | 
			
		||||
  stacktrace: true,
 | 
			
		||||
  show_sensitive_data_on_connection_error: true,
 | 
			
		||||
  show_sensitive_data_on_connection_error: false,
 | 
			
		||||
  pool_size: 15,
 | 
			
		||||
  migration_timestamps: [type: :utc_datetime_usec],
 | 
			
		||||
  migration_lock: nil,
 | 
			
		||||
  queue_target: 5000
 | 
			
		||||
  queue_target: 5000,
 | 
			
		||||
  queue_interval: 1000,
 | 
			
		||||
  checkout_timeout: 15000
 | 
			
		||||
 | 
			
		||||
# Configures Swoosh API Client
 | 
			
		||||
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: WandererApp.Finch
 | 
			
		||||
 
 | 
			
		||||
@@ -129,6 +129,8 @@ config :wanderer_app,
 | 
			
		||||
  admin_username: System.get_env("WANDERER_ADMIN_USERNAME", "admin"),
 | 
			
		||||
  admin_password: System.get_env("WANDERER_ADMIN_PASSWORD"),
 | 
			
		||||
  admins: admins,
 | 
			
		||||
  base_metrics_only:
 | 
			
		||||
    System.get_env("WANDERER_BASE_METRICS_ONLY", "false") |> String.to_existing_atom(),
 | 
			
		||||
  corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
 | 
			
		||||
  corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
 | 
			
		||||
  corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1"),
 | 
			
		||||
 
 | 
			
		||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Api.Character do
 | 
			
		||||
    update :update_corporation do
 | 
			
		||||
      require_atomic? false
 | 
			
		||||
 | 
			
		||||
      accept([:corporation_id, :corporation_name, :corporation_ticker, :alliance_id])
 | 
			
		||||
      accept([:corporation_id, :corporation_name, :corporation_ticker])
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    update :update_alliance do
 | 
			
		||||
 
 | 
			
		||||
@@ -79,8 +79,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
 | 
			
		||||
      accept [
 | 
			
		||||
        :map_id,
 | 
			
		||||
        :character_id,
 | 
			
		||||
        :tracked,
 | 
			
		||||
        :followed
 | 
			
		||||
        :tracked
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
      argument :map_id, :uuid, allow_nil?: false
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@ defmodule WandererApp.Api.MapSystemSignature do
 | 
			
		||||
  code_interface do
 | 
			
		||||
    define(:all_active, action: :all_active)
 | 
			
		||||
    define(:create, action: :create)
 | 
			
		||||
    define(:destroy, action: :destroy)
 | 
			
		||||
    define(:update, action: :update)
 | 
			
		||||
    define(:update_linked_system, action: :update_linked_system)
 | 
			
		||||
    define(:update_type, action: :update_type)
 | 
			
		||||
@@ -62,6 +63,7 @@ defmodule WandererApp.Api.MapSystemSignature do
 | 
			
		||||
      :eve_id,
 | 
			
		||||
      :character_eve_id,
 | 
			
		||||
      :name,
 | 
			
		||||
      :temporary_name,
 | 
			
		||||
      :description,
 | 
			
		||||
      :kind,
 | 
			
		||||
      :group,
 | 
			
		||||
@@ -101,6 +103,7 @@ defmodule WandererApp.Api.MapSystemSignature do
 | 
			
		||||
        :eve_id,
 | 
			
		||||
        :character_eve_id,
 | 
			
		||||
        :name,
 | 
			
		||||
        :temporary_name,
 | 
			
		||||
        :description,
 | 
			
		||||
        :kind,
 | 
			
		||||
        :group,
 | 
			
		||||
@@ -120,6 +123,7 @@ defmodule WandererApp.Api.MapSystemSignature do
 | 
			
		||||
        :eve_id,
 | 
			
		||||
        :character_eve_id,
 | 
			
		||||
        :name,
 | 
			
		||||
        :temporary_name,
 | 
			
		||||
        :description,
 | 
			
		||||
        :kind,
 | 
			
		||||
        :group,
 | 
			
		||||
@@ -195,6 +199,10 @@ defmodule WandererApp.Api.MapSystemSignature do
 | 
			
		||||
      allow_nil? true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    attribute :temporary_name, :string do
 | 
			
		||||
      allow_nil? true
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    attribute :type, :string do
 | 
			
		||||
      allow_nil? true
 | 
			
		||||
    end
 | 
			
		||||
@@ -241,6 +249,7 @@ defmodule WandererApp.Api.MapSystemSignature do
 | 
			
		||||
             :eve_id,
 | 
			
		||||
             :character_eve_id,
 | 
			
		||||
             :name,
 | 
			
		||||
             :temporary_name,
 | 
			
		||||
             :description,
 | 
			
		||||
             :type,
 | 
			
		||||
             :linked_system_id,
 | 
			
		||||
 
 | 
			
		||||
@@ -113,6 +113,63 @@ defmodule WandererApp.CachedInfo do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_solar_system_jumps() do
 | 
			
		||||
    case WandererApp.Cache.lookup(:solar_system_jumps) do
 | 
			
		||||
      {:ok, nil} ->
 | 
			
		||||
        data = WandererApp.EveDataService.get_solar_system_jumps_data()
 | 
			
		||||
 | 
			
		||||
        cache_items(data, :solar_system_jumps)
 | 
			
		||||
 | 
			
		||||
        {:ok, data}
 | 
			
		||||
 | 
			
		||||
      {:ok, data} ->
 | 
			
		||||
        {:ok, data}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
 | 
			
		||||
    # Create normalized cache key (smaller ID first for bidirectional lookup)
 | 
			
		||||
    {id1, id2} =
 | 
			
		||||
      if from_solar_system_id < to_solar_system_id do
 | 
			
		||||
        {from_solar_system_id, to_solar_system_id}
 | 
			
		||||
      else
 | 
			
		||||
        {to_solar_system_id, from_solar_system_id}
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
    cache_key = "jump_#{id1}_#{id2}"
 | 
			
		||||
 | 
			
		||||
    case WandererApp.Cache.lookup(cache_key) do
 | 
			
		||||
      {:ok, nil} ->
 | 
			
		||||
        # Build jump index if not exists
 | 
			
		||||
        build_jump_index()
 | 
			
		||||
        WandererApp.Cache.lookup(cache_key)
 | 
			
		||||
 | 
			
		||||
      result ->
 | 
			
		||||
        result
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp build_jump_index() do
 | 
			
		||||
    case get_solar_system_jumps() do
 | 
			
		||||
      {:ok, jumps} ->
 | 
			
		||||
        jumps
 | 
			
		||||
        |> Enum.each(fn jump ->
 | 
			
		||||
          {id1, id2} =
 | 
			
		||||
            if jump.from_solar_system_id < jump.to_solar_system_id do
 | 
			
		||||
              {jump.from_solar_system_id, jump.to_solar_system_id}
 | 
			
		||||
            else
 | 
			
		||||
              {jump.to_solar_system_id, jump.from_solar_system_id}
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
          cache_key = "jump_#{id1}_#{id2}"
 | 
			
		||||
          WandererApp.Cache.put(cache_key, jump)
 | 
			
		||||
        end)
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        :error
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_wormhole_types!() do
 | 
			
		||||
    case get_wormhole_types() do
 | 
			
		||||
      {:ok, wormhole_types} ->
 | 
			
		||||
 
 | 
			
		||||
@@ -263,7 +263,7 @@ defmodule WandererApp.Character do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_merge_map_character_settings(%{id: character_id} = character, map_id, true) do
 | 
			
		||||
  defp maybe_merge_map_character_settings(%{id: character_id} = character, _map_id, true) do
 | 
			
		||||
    {:ok, tracking_paused} =
 | 
			
		||||
      WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -49,11 +49,13 @@ defmodule WandererApp.Character.Activity do
 | 
			
		||||
  """
 | 
			
		||||
  def process_character_activity(map_id, current_user) do
 | 
			
		||||
    with {:ok, map_user_settings} <- get_map_user_settings(map_id, current_user.id),
 | 
			
		||||
         raw_activity <- WandererApp.Map.get_character_activity(map_id),
 | 
			
		||||
         {:ok, raw_activity} <- WandererApp.Map.get_character_activity(map_id),
 | 
			
		||||
         {:ok, user_characters} <-
 | 
			
		||||
           WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do
 | 
			
		||||
      result = process_activity_data(raw_activity, map_user_settings, user_characters)
 | 
			
		||||
      result
 | 
			
		||||
      process_activity_data(raw_activity, map_user_settings, user_characters)
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
  defstruct [
 | 
			
		||||
    :character_id,
 | 
			
		||||
    :alliance_id,
 | 
			
		||||
    :corporation_id,
 | 
			
		||||
    :opts,
 | 
			
		||||
    server_online: true,
 | 
			
		||||
    start_time: nil,
 | 
			
		||||
@@ -21,6 +22,8 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
 | 
			
		||||
  @type t :: %__MODULE__{
 | 
			
		||||
          character_id: integer,
 | 
			
		||||
          alliance_id: integer,
 | 
			
		||||
          corporation_id: integer,
 | 
			
		||||
          opts: map,
 | 
			
		||||
          server_online: boolean,
 | 
			
		||||
          start_time: DateTime.t(),
 | 
			
		||||
@@ -34,10 +37,10 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
  @pause_tracking_timeout :timer.minutes(60 * 10)
 | 
			
		||||
  @offline_timeout :timer.minutes(5)
 | 
			
		||||
  @online_error_timeout :timer.minutes(2)
 | 
			
		||||
  @ship_error_timeout :timer.minutes(2)
 | 
			
		||||
  @location_error_timeout :timer.minutes(2)
 | 
			
		||||
  @offline_timeout :timer.minutes(10)
 | 
			
		||||
  @online_error_timeout :timer.minutes(10)
 | 
			
		||||
  @ship_error_timeout :timer.minutes(10)
 | 
			
		||||
  @location_error_timeout :timer.minutes(10)
 | 
			
		||||
  @online_forbidden_ttl :timer.seconds(7)
 | 
			
		||||
  @online_limit_ttl :timer.seconds(7)
 | 
			
		||||
  @forbidden_ttl :timer.seconds(5)
 | 
			
		||||
@@ -49,8 +52,15 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
  def new(args), do: __struct__(args)
 | 
			
		||||
 | 
			
		||||
  def init(args) do
 | 
			
		||||
    character_id = args[:character_id]
 | 
			
		||||
 | 
			
		||||
    {:ok, %{corporation_id: corporation_id, alliance_id: alliance_id}} =
 | 
			
		||||
      WandererApp.Character.get_character(character_id)
 | 
			
		||||
 | 
			
		||||
    %{
 | 
			
		||||
      character_id: args[:character_id],
 | 
			
		||||
      character_id: character_id,
 | 
			
		||||
      corporation_id: corporation_id,
 | 
			
		||||
      alliance_id: alliance_id,
 | 
			
		||||
      start_time: DateTime.utc_now(),
 | 
			
		||||
      opts: args
 | 
			
		||||
    }
 | 
			
		||||
@@ -101,6 +111,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
 | 
			
		||||
        if duration >= timeout do
 | 
			
		||||
          pause_tracking(character_id)
 | 
			
		||||
          WandererApp.Cache.delete("character:#{character_id}:#{type}_error_time")
 | 
			
		||||
 | 
			
		||||
          :ok
 | 
			
		||||
        else
 | 
			
		||||
@@ -113,15 +124,14 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
    if WandererApp.Character.can_pause_tracking?(character_id) &&
 | 
			
		||||
         not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
 | 
			
		||||
      # Log character tracking statistics before pausing
 | 
			
		||||
      {:ok, character_state} = WandererApp.Character.get_character_state(character_id)
 | 
			
		||||
      Logger.debug(fn ->
 | 
			
		||||
        {:ok, character_state} = WandererApp.Character.get_character_state(character_id)
 | 
			
		||||
 | 
			
		||||
      Logger.warning(
 | 
			
		||||
        "CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors",
 | 
			
		||||
        character_id: character_id,
 | 
			
		||||
        "CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors: #{inspect(character_id: character_id,
 | 
			
		||||
        active_maps: length(character_state.active_maps),
 | 
			
		||||
        is_online: character_state.is_online,
 | 
			
		||||
        tracking_duration_minutes: get_tracking_duration_minutes(character_id)
 | 
			
		||||
      )
 | 
			
		||||
        tracking_duration_minutes: get_tracking_duration_minutes(character_id))}"
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
      WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
 | 
			
		||||
      WandererApp.Cache.delete("character:#{character_id}:online_error_time")
 | 
			
		||||
@@ -193,7 +203,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
                   access_token: access_token,
 | 
			
		||||
                   character_id: character_id
 | 
			
		||||
                 ) do
 | 
			
		||||
              {:ok, online} ->
 | 
			
		||||
              {:ok, online} when is_map(online) ->
 | 
			
		||||
                online = get_online(online)
 | 
			
		||||
 | 
			
		||||
                if online.online == true do
 | 
			
		||||
@@ -258,7 +268,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
                  character_id: character_id
 | 
			
		||||
                })
 | 
			
		||||
 | 
			
		||||
                Logger.warning("ESI_ERROR: Character online tracking failed",
 | 
			
		||||
                Logger.warning("ESI_ERROR: Character online tracking failed #{inspect(error)}",
 | 
			
		||||
                  character_id: character_id,
 | 
			
		||||
                  tracking_pool: tracking_pool,
 | 
			
		||||
                  error_type: error,
 | 
			
		||||
@@ -388,12 +398,21 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
        {:ok, %{eve_id: eve_id, tracking_pool: tracking_pool}} =
 | 
			
		||||
          WandererApp.Character.get_character(character_id)
 | 
			
		||||
 | 
			
		||||
        case WandererApp.Esi.get_character_info(eve_id) do
 | 
			
		||||
          {:ok, _info} ->
 | 
			
		||||
        character_eve_id = eve_id |> String.to_integer()
 | 
			
		||||
 | 
			
		||||
        case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
 | 
			
		||||
          {:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
 | 
			
		||||
            {:ok, character_state} = WandererApp.Character.get_character_state(character_id)
 | 
			
		||||
 | 
			
		||||
            update = maybe_update_corporation(character_state, eve_id |> String.to_integer())
 | 
			
		||||
            WandererApp.Character.update_character_state(character_id, update)
 | 
			
		||||
            alliance_id = character_aff_info |> Map.get("alliance_id")
 | 
			
		||||
            corporation_id = character_aff_info |> Map.get("corporation_id")
 | 
			
		||||
 | 
			
		||||
            updated_state =
 | 
			
		||||
              character_state
 | 
			
		||||
              |> maybe_update_corporation(corporation_id)
 | 
			
		||||
              |> maybe_update_alliance(alliance_id)
 | 
			
		||||
 | 
			
		||||
            WandererApp.Character.update_character_state(character_id, updated_state)
 | 
			
		||||
 | 
			
		||||
            :ok
 | 
			
		||||
 | 
			
		||||
@@ -975,7 +994,38 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp update_alliance(%{character_id: character_id} = state, alliance_id) do
 | 
			
		||||
  defp maybe_update_alliance(
 | 
			
		||||
         %{character_id: character_id, alliance_id: old_alliance_id} = state,
 | 
			
		||||
         alliance_id
 | 
			
		||||
       )
 | 
			
		||||
       when old_alliance_id != alliance_id and is_nil(alliance_id) do
 | 
			
		||||
    {:ok, character} = WandererApp.Character.get_character(character_id)
 | 
			
		||||
 | 
			
		||||
    character_update = %{
 | 
			
		||||
      alliance_id: nil,
 | 
			
		||||
      alliance_name: nil,
 | 
			
		||||
      alliance_ticker: nil
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    {:ok, _character} =
 | 
			
		||||
      Character.update_alliance(character, character_update)
 | 
			
		||||
 | 
			
		||||
    WandererApp.Character.update_character(character_id, character_update)
 | 
			
		||||
 | 
			
		||||
    @pubsub_client.broadcast(
 | 
			
		||||
      WandererApp.PubSub,
 | 
			
		||||
      "character:#{character_id}:alliance",
 | 
			
		||||
      {:character_alliance, {character_id, character_update}}
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    state
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_update_alliance(
 | 
			
		||||
         %{character_id: character_id, alliance_id: old_alliance_id} = state,
 | 
			
		||||
         alliance_id
 | 
			
		||||
       )
 | 
			
		||||
       when old_alliance_id != alliance_id do
 | 
			
		||||
    (WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
 | 
			
		||||
       WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
 | 
			
		||||
    |> case do
 | 
			
		||||
@@ -1015,7 +1065,13 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp update_corporation(%{character_id: character_id} = state, corporation_id) do
 | 
			
		||||
  defp maybe_update_alliance(state, _alliance_id), do: state
 | 
			
		||||
 | 
			
		||||
  defp maybe_update_corporation(
 | 
			
		||||
         %{character_id: character_id, corporation_id: old_corporation_id} = state,
 | 
			
		||||
         corporation_id
 | 
			
		||||
       )
 | 
			
		||||
       when old_corporation_id != corporation_id do
 | 
			
		||||
    (WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
 | 
			
		||||
       WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
 | 
			
		||||
    |> case do
 | 
			
		||||
@@ -1027,16 +1083,13 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
        |> WandererApp.Esi.get_corporation_info()
 | 
			
		||||
        |> case do
 | 
			
		||||
          {:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
 | 
			
		||||
            alliance_id = Map.get(corporation_info, "alliance_id")
 | 
			
		||||
 | 
			
		||||
            {:ok, character} =
 | 
			
		||||
              WandererApp.Character.get_character(character_id)
 | 
			
		||||
 | 
			
		||||
            character_update = %{
 | 
			
		||||
              corporation_id: corporation_id,
 | 
			
		||||
              corporation_name: corporation_name,
 | 
			
		||||
              corporation_ticker: corporation_ticker,
 | 
			
		||||
              alliance_id: alliance_id
 | 
			
		||||
              corporation_ticker: corporation_ticker
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            {:ok, _character} =
 | 
			
		||||
@@ -1057,8 +1110,7 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            state
 | 
			
		||||
            |> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
 | 
			
		||||
            |> maybe_update_alliance()
 | 
			
		||||
            |> Map.merge(%{corporation_id: corporation_id})
 | 
			
		||||
 | 
			
		||||
          error ->
 | 
			
		||||
            Logger.warning(
 | 
			
		||||
@@ -1072,6 +1124,8 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_update_corporation(state, _corporation_id), do: state
 | 
			
		||||
 | 
			
		||||
  defp maybe_update_ship(
 | 
			
		||||
         %{
 | 
			
		||||
           character_id: character_id
 | 
			
		||||
@@ -1153,58 +1207,6 @@ defmodule WandererApp.Character.Tracker do
 | 
			
		||||
           structure_id != new_structure_id ||
 | 
			
		||||
           station_id != new_station_id
 | 
			
		||||
 | 
			
		||||
  defp maybe_update_corporation(
 | 
			
		||||
         state,
 | 
			
		||||
         character_eve_id
 | 
			
		||||
       )
 | 
			
		||||
       when not is_nil(character_eve_id) and is_integer(character_eve_id) do
 | 
			
		||||
    case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
 | 
			
		||||
      {:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
 | 
			
		||||
        update_corporation(state, character_aff_info |> Map.get("corporation_id"))
 | 
			
		||||
 | 
			
		||||
      _error ->
 | 
			
		||||
        state
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_update_corporation(
 | 
			
		||||
         state,
 | 
			
		||||
         _info
 | 
			
		||||
       ),
 | 
			
		||||
       do: state
 | 
			
		||||
 | 
			
		||||
  defp maybe_update_alliance(
 | 
			
		||||
         %{character_id: character_id, alliance_id: alliance_id} =
 | 
			
		||||
           state
 | 
			
		||||
       ) do
 | 
			
		||||
    case alliance_id do
 | 
			
		||||
      nil ->
 | 
			
		||||
        {:ok, character} = WandererApp.Character.get_character(character_id)
 | 
			
		||||
 | 
			
		||||
        character_update = %{
 | 
			
		||||
          alliance_id: nil,
 | 
			
		||||
          alliance_name: nil,
 | 
			
		||||
          alliance_ticker: nil
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        {:ok, _character} =
 | 
			
		||||
          Character.update_alliance(character, character_update)
 | 
			
		||||
 | 
			
		||||
        WandererApp.Character.update_character(character_id, character_update)
 | 
			
		||||
 | 
			
		||||
        @pubsub_client.broadcast(
 | 
			
		||||
          WandererApp.PubSub,
 | 
			
		||||
          "character:#{character_id}:alliance",
 | 
			
		||||
          {:character_alliance, {character_id, character_update}}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        state
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        update_alliance(state, alliance_id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_update_wallet(
 | 
			
		||||
         %{character_id: character_id} =
 | 
			
		||||
           state,
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
          opts: map
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
  @check_start_queue_interval :timer.seconds(1)
 | 
			
		||||
  @garbage_collection_interval :timer.minutes(15)
 | 
			
		||||
  @untrack_characters_interval :timer.minutes(1)
 | 
			
		||||
  @inactive_character_timeout :timer.minutes(10)
 | 
			
		||||
@@ -23,6 +24,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
  def new(args), do: __struct__(args)
 | 
			
		||||
 | 
			
		||||
  def init(args) do
 | 
			
		||||
    Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
 | 
			
		||||
    Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
 | 
			
		||||
    Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
 | 
			
		||||
 | 
			
		||||
@@ -46,25 +48,19 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def start_tracking(state, character_id, opts) do
 | 
			
		||||
    with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
 | 
			
		||||
         false <- Enum.member?(characters, character_id) do
 | 
			
		||||
      Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
 | 
			
		||||
    if not WandererApp.Cache.has_key?("#{character_id}:track_requested") do
 | 
			
		||||
      WandererApp.Cache.insert(
 | 
			
		||||
        "#{character_id}:track_requested",
 | 
			
		||||
        true
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      tracked_characters = [character_id | characters] |> Enum.uniq()
 | 
			
		||||
      WandererApp.Cache.insert("tracked_characters", tracked_characters)
 | 
			
		||||
 | 
			
		||||
      WandererApp.Character.update_character(character_id, %{online: false})
 | 
			
		||||
 | 
			
		||||
      WandererApp.Character.update_character_state(character_id, %{
 | 
			
		||||
        is_online: false
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
 | 
			
		||||
 | 
			
		||||
      WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
 | 
			
		||||
        character_id,
 | 
			
		||||
        %{opts: opts}
 | 
			
		||||
      ])
 | 
			
		||||
      WandererApp.Cache.insert_or_update(
 | 
			
		||||
        "track_characters_queue",
 | 
			
		||||
        [character_id],
 | 
			
		||||
        fn existing ->
 | 
			
		||||
          [character_id | existing] |> Enum.uniq()
 | 
			
		||||
        end
 | 
			
		||||
      )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    state
 | 
			
		||||
@@ -178,6 +174,21 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        :check_start_queue,
 | 
			
		||||
        state
 | 
			
		||||
      ) do
 | 
			
		||||
    Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
 | 
			
		||||
    {:ok, track_characters_queue} = WandererApp.Cache.lookup("track_characters_queue", [])
 | 
			
		||||
 | 
			
		||||
    track_characters_queue
 | 
			
		||||
    |> Enum.each(fn character_id ->
 | 
			
		||||
      track_character(character_id, %{})
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    state
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info(
 | 
			
		||||
        :garbage_collect,
 | 
			
		||||
        state
 | 
			
		||||
@@ -294,8 +305,56 @@ defmodule WandererApp.Character.TrackerManager.Impl do
 | 
			
		||||
    state
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_info(_event, state),
 | 
			
		||||
    do: state
 | 
			
		||||
  def track_character(character_id, opts) do
 | 
			
		||||
    with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
 | 
			
		||||
         false <- Enum.member?(characters, character_id) do
 | 
			
		||||
      Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
 | 
			
		||||
 | 
			
		||||
      WandererApp.Cache.insert_or_update(
 | 
			
		||||
        "tracked_characters",
 | 
			
		||||
        [character_id],
 | 
			
		||||
        fn existing ->
 | 
			
		||||
          [character_id | existing] |> Enum.uniq()
 | 
			
		||||
        end
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      WandererApp.Cache.insert_or_update(
 | 
			
		||||
        "track_characters_queue",
 | 
			
		||||
        [],
 | 
			
		||||
        fn existing ->
 | 
			
		||||
          existing
 | 
			
		||||
          |> Enum.reject(fn c_id -> c_id == character_id end)
 | 
			
		||||
        end
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      WandererApp.Cache.delete("#{character_id}:track_requested")
 | 
			
		||||
 | 
			
		||||
      WandererApp.Character.update_character(character_id, %{online: false})
 | 
			
		||||
 | 
			
		||||
      WandererApp.Character.update_character_state(character_id, %{
 | 
			
		||||
        is_online: false
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
 | 
			
		||||
 | 
			
		||||
      WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
 | 
			
		||||
        character_id,
 | 
			
		||||
        %{opts: opts}
 | 
			
		||||
      ])
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        WandererApp.Cache.insert_or_update(
 | 
			
		||||
          "track_characters_queue",
 | 
			
		||||
          [],
 | 
			
		||||
          fn existing ->
 | 
			
		||||
            existing
 | 
			
		||||
            |> Enum.reject(fn c_id -> c_id == character_id end)
 | 
			
		||||
          end
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        WandererApp.Cache.delete("#{character_id}:track_requested")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def character_is_present(map_id, character_id) do
 | 
			
		||||
    {:ok, presence_character_ids} =
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ defmodule WandererApp.Character.TrackerPool do
 | 
			
		||||
  @check_ship_errors_interval :timer.minutes(1)
 | 
			
		||||
  @check_location_errors_interval :timer.minutes(1)
 | 
			
		||||
  @update_ship_interval :timer.seconds(2)
 | 
			
		||||
  @update_info_interval :timer.minutes(1)
 | 
			
		||||
  @update_info_interval :timer.minutes(2)
 | 
			
		||||
  @update_wallet_interval :timer.minutes(1)
 | 
			
		||||
 | 
			
		||||
  @logger Application.compile_env(:wanderer_app, :logger)
 | 
			
		||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Character.TrackerPool do
 | 
			
		||||
    Process.send_after(self(), :check_online_errors, :timer.seconds(60))
 | 
			
		||||
    Process.send_after(self(), :check_ship_errors, :timer.seconds(90))
 | 
			
		||||
    Process.send_after(self(), :check_location_errors, :timer.seconds(120))
 | 
			
		||||
    Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
 | 
			
		||||
    # Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
 | 
			
		||||
    Process.send_after(self(), :update_location, 300)
 | 
			
		||||
    Process.send_after(self(), :update_ship, 500)
 | 
			
		||||
    Process.send_after(self(), :update_info, 1500)
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ defmodule WandererApp.Character.TrackingUtils do
 | 
			
		||||
      )
 | 
			
		||||
      when not is_nil(caller_pid) do
 | 
			
		||||
    with {:ok, character} <-
 | 
			
		||||
           WandererApp.Character.get_by_eve_id(character_eve_id),
 | 
			
		||||
           WandererApp.Character.get_by_eve_id("#{character_eve_id}"),
 | 
			
		||||
         {:ok, %{tracked: is_tracked}} <-
 | 
			
		||||
           do_update_character_tracking(character, map_id, track, caller_pid) do
 | 
			
		||||
      # Determine which event to send based on tracking mode and previous state
 | 
			
		||||
@@ -55,15 +55,19 @@ defmodule WandererApp.Character.TrackingUtils do
 | 
			
		||||
  Builds tracking data for all characters with access to a map.
 | 
			
		||||
  """
 | 
			
		||||
  def build_tracking_data(map_id, current_user_id) do
 | 
			
		||||
    with {:ok, map} <- WandererApp.MapRepo.get(map_id, [:acls]),
 | 
			
		||||
         {:ok, character_settings} <-
 | 
			
		||||
           WandererApp.Character.Activity.get_map_character_settings(map_id),
 | 
			
		||||
    with {:ok, map} <-
 | 
			
		||||
           WandererApp.MapRepo.get(map_id,
 | 
			
		||||
             acls: [
 | 
			
		||||
               :owner_id,
 | 
			
		||||
               members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
 | 
			
		||||
             ]
 | 
			
		||||
           ),
 | 
			
		||||
         {:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
 | 
			
		||||
         {:ok, %{characters: characters_with_access}} <-
 | 
			
		||||
           WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
 | 
			
		||||
           WandererApp.Maps.load_characters(map, current_user_id) do
 | 
			
		||||
      # Map characters to tracking data
 | 
			
		||||
      {:ok, characters_data} =
 | 
			
		||||
        build_character_tracking_data(characters_with_access, character_settings)
 | 
			
		||||
        build_character_tracking_data(characters_with_access)
 | 
			
		||||
 | 
			
		||||
      {:ok, main_character} =
 | 
			
		||||
        get_main_character(user_settings, characters_with_access, characters_with_access)
 | 
			
		||||
@@ -98,21 +102,19 @@ defmodule WandererApp.Character.TrackingUtils do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Helper to build tracking data for each character
 | 
			
		||||
  defp build_character_tracking_data(characters, character_settings) do
 | 
			
		||||
  defp build_character_tracking_data(characters) do
 | 
			
		||||
    {:ok,
 | 
			
		||||
     Enum.map(characters, fn char ->
 | 
			
		||||
       setting = Enum.find(character_settings, &(&1.character_id == char.id))
 | 
			
		||||
 | 
			
		||||
       %{
 | 
			
		||||
         character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
 | 
			
		||||
         tracked: (setting && setting.tracked) || false
 | 
			
		||||
         tracked: char.tracked
 | 
			
		||||
       }
 | 
			
		||||
     end)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Private implementation of update character tracking
 | 
			
		||||
  defp do_update_character_tracking(character, map_id, track, caller_pid) do
 | 
			
		||||
    WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id)
 | 
			
		||||
    WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
 | 
			
		||||
    |> case do
 | 
			
		||||
      # Untracking flow
 | 
			
		||||
      {:ok, %{tracked: true} = existing_settings} ->
 | 
			
		||||
 
 | 
			
		||||
@@ -11,49 +11,50 @@ defmodule WandererApp.Env do
 | 
			
		||||
  def vsn(), do: Application.spec(@app)[:vsn]
 | 
			
		||||
 | 
			
		||||
  def git_sha(), do: get_key(:git_sha, "<GIT_SHA>")
 | 
			
		||||
  def base_url, do: get_key(:web_app_url, "<BASE_URL>")
 | 
			
		||||
  def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
 | 
			
		||||
  def invites, do: get_key(:invites, false)
 | 
			
		||||
  def base_url(), do: get_key(:web_app_url, "<BASE_URL>")
 | 
			
		||||
  def base_metrics_only(), do: get_key(:base_metrics_only, false)
 | 
			
		||||
  def custom_route_base_url(), do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
 | 
			
		||||
  def invites(), do: get_key(:invites, false)
 | 
			
		||||
 | 
			
		||||
  def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
 | 
			
		||||
  def websocket_events_enabled?, do: get_key(:websocket_events_enabled, false)
 | 
			
		||||
  def public_api_disabled?, do: get_key(:public_api_disabled, false)
 | 
			
		||||
  def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
 | 
			
		||||
  def websocket_events_enabled?(), do: get_key(:websocket_events_enabled, false)
 | 
			
		||||
  def public_api_disabled?(), do: get_key(:public_api_disabled, false)
 | 
			
		||||
 | 
			
		||||
  @decorate cacheable(
 | 
			
		||||
              cache: WandererApp.Cache,
 | 
			
		||||
              key: "active_tracking_pool"
 | 
			
		||||
            )
 | 
			
		||||
  def active_tracking_pool, do: get_key(:active_tracking_pool, "default")
 | 
			
		||||
  def active_tracking_pool(), do: get_key(:active_tracking_pool, "default")
 | 
			
		||||
 | 
			
		||||
  @decorate cacheable(
 | 
			
		||||
              cache: WandererApp.Cache,
 | 
			
		||||
              key: "tracking_pool_max_size"
 | 
			
		||||
            )
 | 
			
		||||
  def tracking_pool_max_size, do: get_key(:tracking_pool_max_size, 300)
 | 
			
		||||
  def character_tracking_pause_disabled?, do: get_key(:character_tracking_pause_disabled, true)
 | 
			
		||||
  def character_api_disabled?, do: get_key(:character_api_disabled, false)
 | 
			
		||||
  def wanderer_kills_service_enabled?, do: get_key(:wanderer_kills_service_enabled, false)
 | 
			
		||||
  def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
 | 
			
		||||
  def admins, do: get_key(:admins, [])
 | 
			
		||||
  def admin_username, do: get_key(:admin_username)
 | 
			
		||||
  def admin_password, do: get_key(:admin_password)
 | 
			
		||||
  def corp_wallet, do: get_key(:corp_wallet, "")
 | 
			
		||||
  def corp_wallet_eve_id, do: get_key(:corp_wallet_eve_id, "-1")
 | 
			
		||||
  def corp_eve_id, do: get_key(:corp_id, -1)
 | 
			
		||||
  def subscription_settings, do: get_key(:subscription_settings)
 | 
			
		||||
  def tracking_pool_max_size(), do: get_key(:tracking_pool_max_size, 300)
 | 
			
		||||
  def character_tracking_pause_disabled?(), do: get_key(:character_tracking_pause_disabled, true)
 | 
			
		||||
  def character_api_disabled?(), do: get_key(:character_api_disabled, false)
 | 
			
		||||
  def wanderer_kills_service_enabled?(), do: get_key(:wanderer_kills_service_enabled, false)
 | 
			
		||||
  def wallet_tracking_enabled?(), do: get_key(:wallet_tracking_enabled, false)
 | 
			
		||||
  def admins(), do: get_key(:admins, [])
 | 
			
		||||
  def admin_username(), do: get_key(:admin_username)
 | 
			
		||||
  def admin_password(), do: get_key(:admin_password)
 | 
			
		||||
  def corp_wallet(), do: get_key(:corp_wallet, "")
 | 
			
		||||
  def corp_wallet_eve_id(), do: get_key(:corp_wallet_eve_id, "-1")
 | 
			
		||||
  def corp_eve_id(), do: get_key(:corp_id, -1)
 | 
			
		||||
  def subscription_settings(), do: get_key(:subscription_settings)
 | 
			
		||||
 | 
			
		||||
  @decorate cacheable(
 | 
			
		||||
              cache: WandererApp.Cache,
 | 
			
		||||
              key: "restrict_maps_creation"
 | 
			
		||||
            )
 | 
			
		||||
  def restrict_maps_creation?, do: get_key(:restrict_maps_creation, false)
 | 
			
		||||
  def restrict_maps_creation?(), do: get_key(:restrict_maps_creation, false)
 | 
			
		||||
 | 
			
		||||
  def sse_enabled? do
 | 
			
		||||
  def sse_enabled?() do
 | 
			
		||||
    Application.get_env(@app, :sse, [])
 | 
			
		||||
    |> Keyword.get(:enabled, false)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def webhooks_enabled? do
 | 
			
		||||
  def webhooks_enabled?() do
 | 
			
		||||
    Application.get_env(@app, :external_events, [])
 | 
			
		||||
    |> Keyword.get(:webhooks_enabled, false)
 | 
			
		||||
  end
 | 
			
		||||
@@ -62,19 +63,19 @@ defmodule WandererApp.Env do
 | 
			
		||||
              cache: WandererApp.Cache,
 | 
			
		||||
              key: "map-connection-auto-expire-hours"
 | 
			
		||||
            )
 | 
			
		||||
  def map_connection_auto_expire_hours, do: get_key(:map_connection_auto_expire_hours)
 | 
			
		||||
  def map_connection_auto_expire_hours(), do: get_key(:map_connection_auto_expire_hours)
 | 
			
		||||
 | 
			
		||||
  @decorate cacheable(
 | 
			
		||||
              cache: WandererApp.Cache,
 | 
			
		||||
              key: "map-connection-auto-eol-hours"
 | 
			
		||||
            )
 | 
			
		||||
  def map_connection_auto_eol_hours, do: get_key(:map_connection_auto_eol_hours)
 | 
			
		||||
  def map_connection_auto_eol_hours(), do: get_key(:map_connection_auto_eol_hours)
 | 
			
		||||
 | 
			
		||||
  @decorate cacheable(
 | 
			
		||||
              cache: WandererApp.Cache,
 | 
			
		||||
              key: "map-connection-eol-expire-timeout-mins"
 | 
			
		||||
            )
 | 
			
		||||
  def map_connection_eol_expire_timeout_mins,
 | 
			
		||||
  def map_connection_eol_expire_timeout_mins(),
 | 
			
		||||
    do: get_key(:map_connection_eol_expire_timeout_mins)
 | 
			
		||||
 | 
			
		||||
  def get_key(key, default \\ nil), do: Application.get_env(@app, key, default)
 | 
			
		||||
@@ -83,7 +84,7 @@ defmodule WandererApp.Env do
 | 
			
		||||
  A single map containing environment variables
 | 
			
		||||
  made available to react
 | 
			
		||||
  """
 | 
			
		||||
  def to_client_env do
 | 
			
		||||
  def to_client_env() do
 | 
			
		||||
    %{detailedKillsDisabled: not wanderer_kills_service_enabled?()}
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -287,8 +287,8 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
              opts: [ttl: @ttl]
 | 
			
		||||
            )
 | 
			
		||||
  def get_alliance_info(eve_id, opts \\ []) do
 | 
			
		||||
    case _get_alliance_info(eve_id, "", opts) do
 | 
			
		||||
      {:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
 | 
			
		||||
    case get_alliance_info(eve_id, "", opts) do
 | 
			
		||||
      {:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
 | 
			
		||||
      {:error, error} -> {:error, error}
 | 
			
		||||
      error -> error
 | 
			
		||||
    end
 | 
			
		||||
@@ -309,8 +309,8 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
              opts: [ttl: @ttl]
 | 
			
		||||
            )
 | 
			
		||||
  def get_corporation_info(eve_id, opts \\ []) do
 | 
			
		||||
    case _get_corporation_info(eve_id, "", opts) do
 | 
			
		||||
      {:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
 | 
			
		||||
    case get_corporation_info(eve_id, "", opts) do
 | 
			
		||||
      {:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
 | 
			
		||||
      {:error, error} -> {:error, error}
 | 
			
		||||
      error -> error
 | 
			
		||||
    end
 | 
			
		||||
@@ -327,7 +327,7 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
           opts,
 | 
			
		||||
           @cache_opts
 | 
			
		||||
         ) do
 | 
			
		||||
      {:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
 | 
			
		||||
      {:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
 | 
			
		||||
      {:error, error} -> {:error, error}
 | 
			
		||||
      error -> error
 | 
			
		||||
    end
 | 
			
		||||
@@ -434,7 +434,7 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
 | 
			
		||||
  defp get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
 | 
			
		||||
 | 
			
		||||
  defp _get_alliance_info(alliance_eve_id, info_path, opts),
 | 
			
		||||
  defp get_alliance_info(alliance_eve_id, info_path, opts),
 | 
			
		||||
    do:
 | 
			
		||||
      get(
 | 
			
		||||
        "/alliances/#{alliance_eve_id}/#{info_path}",
 | 
			
		||||
@@ -442,7 +442,7 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
        @cache_opts
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
  defp _get_corporation_info(corporation_eve_id, info_path, opts),
 | 
			
		||||
  defp get_corporation_info(corporation_eve_id, info_path, opts),
 | 
			
		||||
    do:
 | 
			
		||||
      get(
 | 
			
		||||
        "/corporations/#{corporation_eve_id}/#{info_path}",
 | 
			
		||||
@@ -830,7 +830,8 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
         expires_at,
 | 
			
		||||
         scopes
 | 
			
		||||
       ) do
 | 
			
		||||
    time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at, :second)
 | 
			
		||||
    expires_at_datetime = DateTime.from_unix!(expires_at)
 | 
			
		||||
    time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
 | 
			
		||||
 | 
			
		||||
    Logger.warning("TOKEN_REFRESH_FAILED: Invalid grant error during token refresh",
 | 
			
		||||
      character_id: character_id,
 | 
			
		||||
@@ -857,7 +858,8 @@ defmodule WandererApp.Esi.ApiClient do
 | 
			
		||||
         expires_at,
 | 
			
		||||
         scopes
 | 
			
		||||
       ) do
 | 
			
		||||
    time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at, :second)
 | 
			
		||||
    expires_at_datetime = DateTime.from_unix!(expires_at)
 | 
			
		||||
    time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
 | 
			
		||||
 | 
			
		||||
    Logger.warning("TOKEN_REFRESH_FAILED: Connection refused during token refresh",
 | 
			
		||||
      character_id: character_id,
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ defmodule WandererApp.ExternalEvents.Event do
 | 
			
		||||
  def new(map_id, event_type, payload) when is_binary(map_id) and is_map(payload) do
 | 
			
		||||
    if valid_event_type?(event_type) do
 | 
			
		||||
      %__MODULE__{
 | 
			
		||||
        id: Ulid.generate(System.system_time(:millisecond)),
 | 
			
		||||
        id: Ecto.ULID.generate(System.system_time(:millisecond)),
 | 
			
		||||
        map_id: map_id,
 | 
			
		||||
        type: event_type,
 | 
			
		||||
        payload: payload,
 | 
			
		||||
@@ -97,7 +97,7 @@ defmodule WandererApp.ExternalEvents.Event do
 | 
			
		||||
    :locked,
 | 
			
		||||
    # ADD
 | 
			
		||||
    :temporary_name,
 | 
			
		||||
    # ADD  
 | 
			
		||||
    # ADD
 | 
			
		||||
    :labels,
 | 
			
		||||
    # ADD
 | 
			
		||||
    :description,
 | 
			
		||||
 
 | 
			
		||||
@@ -448,7 +448,7 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
 | 
			
		||||
      "connected" ->
 | 
			
		||||
        %{
 | 
			
		||||
          "type" => "connection_status",
 | 
			
		||||
          "id" => event["id"] || Ulid.generate(),
 | 
			
		||||
          "id" => event["id"] || Ecto.ULID.generate(),
 | 
			
		||||
          "attributes" => %{
 | 
			
		||||
            "status" => "connected",
 | 
			
		||||
            "server_time" => payload["server_time"],
 | 
			
		||||
@@ -465,7 +465,7 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
 | 
			
		||||
        # Use existing payload structure but wrap it in JSON:API format
 | 
			
		||||
        %{
 | 
			
		||||
          "type" => "events",
 | 
			
		||||
          "id" => event["id"] || Ulid.generate(),
 | 
			
		||||
          "id" => event["id"] || Ecto.ULID.generate(),
 | 
			
		||||
          "attributes" => payload,
 | 
			
		||||
          "relationships" => %{
 | 
			
		||||
            "map" => %{
 | 
			
		||||
 
 | 
			
		||||
@@ -248,6 +248,6 @@ defmodule WandererApp.ExternalEvents.MapEventRelay do
 | 
			
		||||
  defp datetime_to_ulid(datetime) do
 | 
			
		||||
    timestamp = DateTime.to_unix(datetime, :millisecond)
 | 
			
		||||
    # Create a ULID with the timestamp (rest will be zeros for comparison)
 | 
			
		||||
    Ulid.generate(timestamp)
 | 
			
		||||
    Ecto.ULID.generate(timestamp)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,6 @@ defmodule WandererApp.Map.Audit do
 | 
			
		||||
  require Ash.Query
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  alias WandererApp.SecurityAudit
 | 
			
		||||
 | 
			
		||||
  @week_seconds :timer.hours(24 * 7)
 | 
			
		||||
  @month_seconds @week_seconds * 4
 | 
			
		||||
  @audit_expired_seconds @month_seconds * 3
 | 
			
		||||
@@ -38,17 +36,14 @@ defmodule WandererApp.Map.Audit do
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_activity_query(map_id, period, activity) do
 | 
			
		||||
    SecurityAudit.get_map_activity_query(map_id, period, activity)
 | 
			
		||||
  end
 | 
			
		||||
  defdelegate get_map_activity_query(map_id, period, activity),
 | 
			
		||||
    to: WandererApp.SecurityAudit
 | 
			
		||||
 | 
			
		||||
  def track_acl_event(event_type, metadata) do
 | 
			
		||||
    SecurityAudit.track_acl_event(event_type, metadata)
 | 
			
		||||
  end
 | 
			
		||||
  defdelegate track_acl_event(event_type, metadata),
 | 
			
		||||
    to: WandererApp.SecurityAudit
 | 
			
		||||
 | 
			
		||||
  def track_map_event(event_type, metadata) do
 | 
			
		||||
    SecurityAudit.track_map_event(event_type, metadata)
 | 
			
		||||
  end
 | 
			
		||||
  defdelegate track_map_event(event_type, metadata),
 | 
			
		||||
    to: WandererApp.SecurityAudit
 | 
			
		||||
 | 
			
		||||
  defp get_expired_at(), do: DateTime.utc_now() |> DateTime.add(-@audit_expired_seconds, :second)
 | 
			
		||||
end
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,19 @@ defmodule WandererApp.Map.Manager do
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  alias WandererApp.Map.Server
 | 
			
		||||
  alias WandererApp.Map.ServerSupervisor
 | 
			
		||||
  alias WandererApp.Api.MapSystemSignature
 | 
			
		||||
 | 
			
		||||
  @maps_start_per_second 10
 | 
			
		||||
  @maps_start_interval 1000
 | 
			
		||||
  @maps_queue :maps_queue
 | 
			
		||||
  @garbage_collection_interval :timer.hours(1)
 | 
			
		||||
  @check_maps_queue_interval :timer.seconds(1)
 | 
			
		||||
  @signatures_cleanup_interval :timer.minutes(30)
 | 
			
		||||
  @delete_after_minutes 30
 | 
			
		||||
 | 
			
		||||
  @pings_cleanup_interval :timer.minutes(10)
 | 
			
		||||
  @pings_expire_minutes 60
 | 
			
		||||
 | 
			
		||||
  # Test-aware async task runner
 | 
			
		||||
  defp safe_async_task(fun) do
 | 
			
		||||
@@ -25,20 +38,6 @@ defmodule WandererApp.Map.Manager do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  alias WandererApp.Map.ServerSupervisor
 | 
			
		||||
  alias WandererApp.Api.MapSystemSignature
 | 
			
		||||
 | 
			
		||||
  @maps_start_per_second 5
 | 
			
		||||
  @maps_start_interval 1000
 | 
			
		||||
  @maps_queue :maps_queue
 | 
			
		||||
  @garbage_collection_interval :timer.hours(1)
 | 
			
		||||
  @check_maps_queue_interval :timer.seconds(1)
 | 
			
		||||
  @signatures_cleanup_interval :timer.minutes(30)
 | 
			
		||||
  @delete_after_minutes 30
 | 
			
		||||
 | 
			
		||||
  @pings_cleanup_interval :timer.minutes(10)
 | 
			
		||||
  @pings_expire_minutes 60
 | 
			
		||||
 | 
			
		||||
  def start_map(map_id) when is_binary(map_id),
 | 
			
		||||
    do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
 | 
			
		||||
 | 
			
		||||
@@ -247,22 +246,29 @@ defmodule WandererApp.Map.Manager do
 | 
			
		||||
      Logger.debug(fn -> "All maps started" end)
 | 
			
		||||
    else
 | 
			
		||||
      # In production, run async as normal
 | 
			
		||||
      tasks =
 | 
			
		||||
        for chunk <- chunks do
 | 
			
		||||
          task =
 | 
			
		||||
            Task.async(fn ->
 | 
			
		||||
              chunk
 | 
			
		||||
              |> Enum.map(&start_map_server/1)
 | 
			
		||||
            end)
 | 
			
		||||
      chunks
 | 
			
		||||
      |> Task.async_stream(
 | 
			
		||||
        fn chunk ->
 | 
			
		||||
          chunk
 | 
			
		||||
          |> Enum.map(&start_map_server/1)
 | 
			
		||||
 | 
			
		||||
          :timer.sleep(@maps_start_interval)
 | 
			
		||||
        end,
 | 
			
		||||
        max_concurrency: System.schedulers_online(),
 | 
			
		||||
        on_timeout: :kill_task,
 | 
			
		||||
        timeout: :timer.seconds(60)
 | 
			
		||||
      )
 | 
			
		||||
      |> Enum.each(fn result ->
 | 
			
		||||
        case result do
 | 
			
		||||
          {:ok, _} ->
 | 
			
		||||
            :ok
 | 
			
		||||
 | 
			
		||||
          task
 | 
			
		||||
          _ ->
 | 
			
		||||
            :ok
 | 
			
		||||
        end
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
      Logger.debug(fn -> "Waiting for maps to start" end)
 | 
			
		||||
      Task.await_many(tasks)
 | 
			
		||||
      Logger.debug(fn -> "All maps started" end)
 | 
			
		||||
      Logger.info(fn -> "All maps started" end)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -88,11 +88,18 @@ defmodule WandererApp.Map.Server do
 | 
			
		||||
      |> map_pid!
 | 
			
		||||
      |> GenServer.cast({&Impl.remove_character/2, [character_id]})
 | 
			
		||||
 | 
			
		||||
  def untrack_characters(map_id, character_ids) when is_binary(map_id),
 | 
			
		||||
    do:
 | 
			
		||||
      map_id
 | 
			
		||||
      |> map_pid!
 | 
			
		||||
      |> GenServer.cast({&Impl.untrack_characters/2, [character_ids]})
 | 
			
		||||
  def untrack_characters(map_id, character_ids) when is_binary(map_id) do
 | 
			
		||||
    map_id
 | 
			
		||||
    |> map_pid()
 | 
			
		||||
    |> case do
 | 
			
		||||
      pid when is_pid(pid) ->
 | 
			
		||||
        GenServer.cast(pid, {&Impl.untrack_characters/2, [character_ids]})
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        WandererApp.Cache.insert("map_#{map_id}:started", false)
 | 
			
		||||
        :ok
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def add_system(map_id, system_info, user_id, character_id) when is_binary(map_id),
 | 
			
		||||
    do:
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,13 @@ defmodule WandererApp.Map.Operations.Signatures do
 | 
			
		||||
      systems
 | 
			
		||||
      |> Enum.flat_map(fn sys ->
 | 
			
		||||
        with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
 | 
			
		||||
          sigs
 | 
			
		||||
          # Add solar_system_id to each signature and remove system_id
 | 
			
		||||
          Enum.map(sigs, fn sig ->
 | 
			
		||||
            sig
 | 
			
		||||
            |> Map.from_struct()
 | 
			
		||||
            |> Map.put(:solar_system_id, sys.solar_system_id)
 | 
			
		||||
            |> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
 | 
			
		||||
          end)
 | 
			
		||||
        else
 | 
			
		||||
          err ->
 | 
			
		||||
            Logger.error("[list_signatures] error: #{inspect(err)}")
 | 
			
		||||
@@ -32,28 +38,70 @@ defmodule WandererApp.Map.Operations.Signatures do
 | 
			
		||||
  def create_signature(
 | 
			
		||||
        %{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
 | 
			
		||||
          _conn,
 | 
			
		||||
        %{"solar_system_id" => _solar_system_id} = params
 | 
			
		||||
      ) do
 | 
			
		||||
    attrs = Map.put(params, "character_eve_id", char_id)
 | 
			
		||||
        %{"solar_system_id" => solar_system_id} = params
 | 
			
		||||
      )
 | 
			
		||||
      when is_integer(solar_system_id) do
 | 
			
		||||
    # Convert solar_system_id to system_id for internal use
 | 
			
		||||
    with {:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
 | 
			
		||||
      attrs =
 | 
			
		||||
        params
 | 
			
		||||
        |> Map.put("character_eve_id", char_id)
 | 
			
		||||
        |> Map.put("system_id", system.id)
 | 
			
		||||
        |> Map.delete("solar_system_id")
 | 
			
		||||
 | 
			
		||||
    case Server.update_signatures(map_id, %{
 | 
			
		||||
           added_signatures: [attrs],
 | 
			
		||||
           updated_signatures: [],
 | 
			
		||||
           removed_signatures: [],
 | 
			
		||||
           solar_system_id: params["solar_system_id"],
 | 
			
		||||
           character_id: char_id,
 | 
			
		||||
           user_id: user_id,
 | 
			
		||||
           delete_connection_with_sigs: false
 | 
			
		||||
         }) do
 | 
			
		||||
      :ok ->
 | 
			
		||||
        {:ok, attrs}
 | 
			
		||||
      case Server.update_signatures(map_id, %{
 | 
			
		||||
             added_signatures: [attrs],
 | 
			
		||||
             updated_signatures: [],
 | 
			
		||||
             removed_signatures: [],
 | 
			
		||||
             solar_system_id: solar_system_id,
 | 
			
		||||
             character_id: char_id,
 | 
			
		||||
             user_id: user_id,
 | 
			
		||||
             delete_connection_with_sigs: false
 | 
			
		||||
           }) do
 | 
			
		||||
        :ok ->
 | 
			
		||||
          # Try to fetch the created signature to return with proper fields
 | 
			
		||||
          with {:ok, sigs} <-
 | 
			
		||||
                 MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
 | 
			
		||||
               sig when not is_nil(sig) <- List.first(sigs) do
 | 
			
		||||
            result =
 | 
			
		||||
              sig
 | 
			
		||||
              |> Map.from_struct()
 | 
			
		||||
              |> Map.put(:solar_system_id, system.solar_system_id)
 | 
			
		||||
              |> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
 | 
			
		||||
 | 
			
		||||
      err ->
 | 
			
		||||
        Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
 | 
			
		||||
        {:error, :unexpected_error}
 | 
			
		||||
            {:ok, result}
 | 
			
		||||
          else
 | 
			
		||||
            _ ->
 | 
			
		||||
              # Fallback: return attrs with solar_system_id added
 | 
			
		||||
              attrs_result =
 | 
			
		||||
                attrs
 | 
			
		||||
                |> Map.put(:solar_system_id, solar_system_id)
 | 
			
		||||
                |> Map.drop(["system_id"])
 | 
			
		||||
 | 
			
		||||
              {:ok, attrs_result}
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
        err ->
 | 
			
		||||
          Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
 | 
			
		||||
          {:error, :unexpected_error}
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        Logger.error(
 | 
			
		||||
          "[create_signature] System not found for solar_system_id: #{solar_system_id}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        {:error, :system_not_found}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def create_signature(
 | 
			
		||||
        %{assigns: %{map_id: _map_id, owner_character_id: _char_id, owner_user_id: _user_id}} =
 | 
			
		||||
          _conn,
 | 
			
		||||
        %{"solar_system_id" => _invalid} = _params
 | 
			
		||||
      ),
 | 
			
		||||
      do: {:error, :missing_params}
 | 
			
		||||
 | 
			
		||||
  def create_signature(_conn, _params), do: {:error, :missing_params}
 | 
			
		||||
 | 
			
		||||
  @spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
 | 
			
		||||
@@ -90,7 +138,18 @@ defmodule WandererApp.Map.Operations.Signatures do
 | 
			
		||||
          delete_connection_with_sigs: false
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
      {:ok, attrs}
 | 
			
		||||
      # Fetch the updated signature to return with proper fields
 | 
			
		||||
      with {:ok, updated_sig} <- MapSystemSignature.by_id(sig_id) do
 | 
			
		||||
        result =
 | 
			
		||||
          updated_sig
 | 
			
		||||
          |> Map.from_struct()
 | 
			
		||||
          |> Map.put(:solar_system_id, system.solar_system_id)
 | 
			
		||||
          |> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
 | 
			
		||||
 | 
			
		||||
        {:ok, result}
 | 
			
		||||
      else
 | 
			
		||||
        _ -> {:ok, attrs}
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      err ->
 | 
			
		||||
        Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
 | 
			
		||||
    map_update = %{acls: map.acls, scope: map.scope}
 | 
			
		||||
 | 
			
		||||
    WandererApp.Map.update_map(map_id, map_update)
 | 
			
		||||
    WandererApp.Cache.delete("map_characters-#{map_id}")
 | 
			
		||||
 | 
			
		||||
    broadcast_acl_updates({:ok, result}, map_id)
 | 
			
		||||
 | 
			
		||||
@@ -66,7 +67,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_acl_updated(map_id, acl_id) do
 | 
			
		||||
    {:ok, map} =
 | 
			
		||||
    {:ok, %{acls: acls}} =
 | 
			
		||||
      WandererApp.MapRepo.get(map_id,
 | 
			
		||||
        acls: [
 | 
			
		||||
          :owner_id,
 | 
			
		||||
@@ -74,8 +75,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
 | 
			
		||||
      WandererApp.Map.update_map(map_id, %{acls: map.acls})
 | 
			
		||||
    if acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
 | 
			
		||||
      WandererApp.Map.update_map(map_id, %{acls: acls})
 | 
			
		||||
      WandererApp.Cache.delete("map_characters-#{map_id}")
 | 
			
		||||
 | 
			
		||||
      :ok =
 | 
			
		||||
        acl_id
 | 
			
		||||
@@ -85,7 +87,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_acl_deleted(map_id, _acl_id) do
 | 
			
		||||
    {:ok, map} =
 | 
			
		||||
    {:ok, %{acls: acls}} =
 | 
			
		||||
      WandererApp.MapRepo.get(map_id,
 | 
			
		||||
        acls: [
 | 
			
		||||
          :owner_id,
 | 
			
		||||
@@ -93,7 +95,8 @@ defmodule WandererApp.Map.Server.AclsImpl do
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    WandererApp.Map.update_map(map_id, %{acls: map.acls})
 | 
			
		||||
    WandererApp.Map.update_map(map_id, %{acls: acls})
 | 
			
		||||
    WandererApp.Cache.delete("map_characters-#{map_id}")
 | 
			
		||||
 | 
			
		||||
    character_ids =
 | 
			
		||||
      map_id
 | 
			
		||||
 
 | 
			
		||||
@@ -78,15 +78,12 @@ defmodule WandererApp.Map.Server.CharactersImpl do
 | 
			
		||||
 | 
			
		||||
      characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
 | 
			
		||||
 | 
			
		||||
      {:ok, invalidate_character_ids} =
 | 
			
		||||
        WandererApp.Cache.lookup(
 | 
			
		||||
          "map_#{map_id}:invalidate_character_ids",
 | 
			
		||||
          []
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      WandererApp.Cache.insert(
 | 
			
		||||
      WandererApp.Cache.insert_or_update(
 | 
			
		||||
        "map_#{map_id}:invalidate_character_ids",
 | 
			
		||||
        (invalidate_character_ids ++ characters_to_remove) |> Enum.uniq()
 | 
			
		||||
        characters_to_remove,
 | 
			
		||||
        fn ids ->
 | 
			
		||||
          (ids ++ characters_to_remove) |> Enum.uniq()
 | 
			
		||||
        end
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
 | 
			
		||||
@@ -126,15 +123,18 @@ defmodule WandererApp.Map.Server.CharactersImpl do
 | 
			
		||||
 | 
			
		||||
  def cleanup_characters(map_id, owner_id) do
 | 
			
		||||
    {:ok, invalidate_character_ids} =
 | 
			
		||||
      WandererApp.Cache.lookup(
 | 
			
		||||
      WandererApp.Cache.get_and_remove(
 | 
			
		||||
        "map_#{map_id}:invalidate_character_ids",
 | 
			
		||||
        []
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    acls =
 | 
			
		||||
      map_id
 | 
			
		||||
      |> WandererApp.Map.get_map!()
 | 
			
		||||
      |> Map.get(:acls, [])
 | 
			
		||||
    {:ok, %{acls: acls}} =
 | 
			
		||||
      WandererApp.MapRepo.get(map_id,
 | 
			
		||||
        acls: [
 | 
			
		||||
          :owner_id,
 | 
			
		||||
          members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
 | 
			
		||||
        ]
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    invalidate_character_ids
 | 
			
		||||
    |> Task.async_stream(
 | 
			
		||||
@@ -186,11 +186,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
 | 
			
		||||
      {:error, reason} ->
 | 
			
		||||
        Logger.error("Error in cleanup_characters: #{inspect(reason)}")
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    WandererApp.Cache.insert(
 | 
			
		||||
      "map_#{map_id}:invalidate_character_ids",
 | 
			
		||||
      []
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp remove_and_untrack_characters(map_id, character_ids) do
 | 
			
		||||
@@ -373,6 +368,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
 | 
			
		||||
    {:ok, character} =
 | 
			
		||||
      WandererApp.Character.get_map_character(map_id, character_id, not_present: true)
 | 
			
		||||
 | 
			
		||||
    WandererApp.Cache.delete("character:#{character.id}:tracking_paused")
 | 
			
		||||
 | 
			
		||||
    add_character(%{map_id: map_id}, character, true)
 | 
			
		||||
 | 
			
		||||
    WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
 | 
			
		||||
 
 | 
			
		||||
@@ -527,33 +527,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
 | 
			
		||||
 | 
			
		||||
  def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
 | 
			
		||||
      when not is_nil(from_solar_system_id) and not is_nil(to_solar_system_id) do
 | 
			
		||||
    {:ok, known_jumps} =
 | 
			
		||||
      WandererApp.Api.MapSolarSystemJumps.find(%{
 | 
			
		||||
        before_system_id: from_solar_system_id,
 | 
			
		||||
        current_system_id: to_solar_system_id
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    {:ok, from_system_static_info} = get_system_static_info(from_solar_system_id)
 | 
			
		||||
    {:ok, to_system_static_info} = get_system_static_info(to_solar_system_id)
 | 
			
		||||
 | 
			
		||||
    case scope do
 | 
			
		||||
      :wormholes ->
 | 
			
		||||
        not is_prohibited_system_class?(from_system_static_info.system_class) and
 | 
			
		||||
          not is_prohibited_system_class?(to_system_static_info.system_class) and
 | 
			
		||||
          not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
 | 
			
		||||
          not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
 | 
			
		||||
          known_jumps |> Enum.empty?()
 | 
			
		||||
 | 
			
		||||
      :stargates ->
 | 
			
		||||
        # For stargates, we need to check:
 | 
			
		||||
        # 1. Both systems are in known space (HS, LS, NS)
 | 
			
		||||
        # 2. There is a known jump between them
 | 
			
		||||
        # 3. Neither system is prohibited
 | 
			
		||||
        from_system_static_info.system_class in @known_space and
 | 
			
		||||
          to_system_static_info.system_class in @known_space and
 | 
			
		||||
    with {:ok, known_jumps} <- find_solar_system_jump(from_solar_system_id, to_solar_system_id),
 | 
			
		||||
         {:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
 | 
			
		||||
         {:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do
 | 
			
		||||
      case scope do
 | 
			
		||||
        :wormholes ->
 | 
			
		||||
          not is_prohibited_system_class?(from_system_static_info.system_class) and
 | 
			
		||||
          not is_prohibited_system_class?(to_system_static_info.system_class) and
 | 
			
		||||
          not (known_jumps |> Enum.empty?())
 | 
			
		||||
            not is_prohibited_system_class?(to_system_static_info.system_class) and
 | 
			
		||||
            not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
 | 
			
		||||
            not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
 | 
			
		||||
            known_jumps |> Enum.empty?()
 | 
			
		||||
 | 
			
		||||
        :stargates ->
 | 
			
		||||
          # For stargates, we need to check:
 | 
			
		||||
          # 1. Both systems are in known space (HS, LS, NS)
 | 
			
		||||
          # 2. There is a known jump between them
 | 
			
		||||
          # 3. Neither system is prohibited
 | 
			
		||||
          from_system_static_info.system_class in @known_space and
 | 
			
		||||
            to_system_static_info.system_class in @known_space and
 | 
			
		||||
            not is_prohibited_system_class?(from_system_static_info.system_class) and
 | 
			
		||||
            not is_prohibited_system_class?(to_system_static_info.system_class) and
 | 
			
		||||
            not (known_jumps |> Enum.empty?())
 | 
			
		||||
      end
 | 
			
		||||
    else
 | 
			
		||||
      _ -> false
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -570,6 +567,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp find_solar_system_jump(from_solar_system_id, to_solar_system_id) do
 | 
			
		||||
    case WandererApp.CachedInfo.get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
 | 
			
		||||
      {:ok, jump} when not is_nil(jump) -> {:ok, [jump]}
 | 
			
		||||
      _ -> {:ok, []}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_system_static_info(solar_system_id) do
 | 
			
		||||
    case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
 | 
			
		||||
      {:ok, system_static_info} when not is_nil(system_static_info) ->
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ defmodule WandererApp.Map.Server.Impl do
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  @systems_cleanup_timeout :timer.minutes(30)
 | 
			
		||||
  @characters_cleanup_timeout :timer.minutes(1)
 | 
			
		||||
  @characters_cleanup_timeout :timer.minutes(5)
 | 
			
		||||
  @connections_cleanup_timeout :timer.minutes(2)
 | 
			
		||||
 | 
			
		||||
  @pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
 | 
			
		||||
@@ -100,7 +100,7 @@ defmodule WandererApp.Map.Server.Impl do
 | 
			
		||||
          Process.send_after(self(), :update_presence, @update_presence_timeout)
 | 
			
		||||
          Process.send_after(self(), :cleanup_connections, 5_000)
 | 
			
		||||
          Process.send_after(self(), :cleanup_systems, 10_000)
 | 
			
		||||
          Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
 | 
			
		||||
          Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
 | 
			
		||||
          Process.send_after(self(), :backup_state, @backup_state_timeout)
 | 
			
		||||
 | 
			
		||||
          WandererApp.Cache.insert("map_#{map_id}:started", true)
 | 
			
		||||
@@ -127,6 +127,7 @@ defmodule WandererApp.Map.Server.Impl do
 | 
			
		||||
    Logger.debug(fn -> "Stopping map server for #{map_id}" end)
 | 
			
		||||
 | 
			
		||||
    WandererApp.Cache.delete("map_#{map_id}:started")
 | 
			
		||||
    WandererApp.Cache.delete("map_characters-#{map_id}")
 | 
			
		||||
 | 
			
		||||
    :telemetry.execute([:wanderer_app, :map, :stopped], %{count: 1})
 | 
			
		||||
 | 
			
		||||
@@ -278,7 +279,7 @@ defmodule WandererApp.Map.Server.Impl do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_event({:acl_deleted, %{acl_id: acl_id}}, %{map_id: map_id} = state) do
 | 
			
		||||
    AclsImpl.handle_acl_updated(map_id, acl_id)
 | 
			
		||||
    AclsImpl.handle_acl_deleted(map_id, acl_id)
 | 
			
		||||
 | 
			
		||||
    state
 | 
			
		||||
  end
 | 
			
		||||
 
 | 
			
		||||
@@ -114,6 +114,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
 | 
			
		||||
            deleted_sig,
 | 
			
		||||
            Map.take(sig, [
 | 
			
		||||
              :name,
 | 
			
		||||
              :temporary_name,
 | 
			
		||||
              :description,
 | 
			
		||||
              :kind,
 | 
			
		||||
              :group,
 | 
			
		||||
@@ -239,6 +240,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
 | 
			
		||||
        system_id: system_id,
 | 
			
		||||
        eve_id: sig["eve_id"],
 | 
			
		||||
        name: sig["name"],
 | 
			
		||||
        temporary_name: sig["temporary_name"],
 | 
			
		||||
        description: Map.get(sig, "description"),
 | 
			
		||||
        kind: sig["kind"],
 | 
			
		||||
        group: sig["group"],
 | 
			
		||||
 
 | 
			
		||||
@@ -94,13 +94,22 @@ defmodule WandererApp.Maps do
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def load_characters(map, character_settings, user_id) do
 | 
			
		||||
  def load_characters(map, user_id) do
 | 
			
		||||
    {:ok, user_characters} =
 | 
			
		||||
      WandererApp.Api.Character.active_by_user(%{user_id: user_id})
 | 
			
		||||
 | 
			
		||||
    characters =
 | 
			
		||||
    map_available_characters =
 | 
			
		||||
      map
 | 
			
		||||
      |> get_map_available_characters(user_characters)
 | 
			
		||||
 | 
			
		||||
    {:ok, character_settings} =
 | 
			
		||||
      WandererApp.MapCharacterSettingsRepo.get_by_map_filtered(
 | 
			
		||||
        map.id,
 | 
			
		||||
        map_available_characters |> Enum.map(& &1.id)
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
    characters =
 | 
			
		||||
      map_available_characters
 | 
			
		||||
      |> Enum.map(fn c ->
 | 
			
		||||
        map_character(c, character_settings |> Enum.find(&(&1.character_id == c.id)))
 | 
			
		||||
      end)
 | 
			
		||||
@@ -176,48 +185,57 @@ defmodule WandererApp.Maps do
 | 
			
		||||
        tracked: tracked
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
  @decorate cacheable(
 | 
			
		||||
              cache: WandererApp.Cache,
 | 
			
		||||
              key: "map_characters-#{map_id}",
 | 
			
		||||
              opts: [ttl: :timer.seconds(2)]
 | 
			
		||||
            )
 | 
			
		||||
  defp _get_map_characters(%{id: map_id} = map) do
 | 
			
		||||
    map_acls =
 | 
			
		||||
      map.acls
 | 
			
		||||
      |> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
 | 
			
		||||
  defp get_map_characters(%{id: map_id} = map) do
 | 
			
		||||
    WandererApp.Cache.lookup!("map_characters-#{map_id}")
 | 
			
		||||
    |> case do
 | 
			
		||||
      nil ->
 | 
			
		||||
        map_acls =
 | 
			
		||||
          map.acls
 | 
			
		||||
          |> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
 | 
			
		||||
 | 
			
		||||
    map_acl_owner_ids =
 | 
			
		||||
      map_acls
 | 
			
		||||
      |> Enum.map(fn acl -> acl.owner_id end)
 | 
			
		||||
        map_acl_owner_ids =
 | 
			
		||||
          map_acls
 | 
			
		||||
          |> Enum.map(fn acl -> acl.owner_id end)
 | 
			
		||||
 | 
			
		||||
    map_members =
 | 
			
		||||
      map_acls
 | 
			
		||||
      |> Enum.map(fn acl -> acl.members end)
 | 
			
		||||
      |> List.flatten()
 | 
			
		||||
      |> Enum.filter(fn member -> member.role != :blocked end)
 | 
			
		||||
        map_members =
 | 
			
		||||
          map_acls
 | 
			
		||||
          |> Enum.map(fn acl -> acl.members end)
 | 
			
		||||
          |> List.flatten()
 | 
			
		||||
          |> Enum.filter(fn member -> member.role != :blocked end)
 | 
			
		||||
 | 
			
		||||
    map_member_eve_ids =
 | 
			
		||||
      map_members
 | 
			
		||||
      |> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
 | 
			
		||||
      |> Enum.map(fn member -> member.eve_character_id end)
 | 
			
		||||
        map_member_eve_ids =
 | 
			
		||||
          map_members
 | 
			
		||||
          |> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
 | 
			
		||||
          |> Enum.map(fn member -> member.eve_character_id end)
 | 
			
		||||
 | 
			
		||||
    map_member_corporation_ids =
 | 
			
		||||
      map_members
 | 
			
		||||
      |> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
 | 
			
		||||
      |> Enum.map(fn member -> member.eve_corporation_id end)
 | 
			
		||||
        map_member_corporation_ids =
 | 
			
		||||
          map_members
 | 
			
		||||
          |> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
 | 
			
		||||
          |> Enum.map(fn member -> member.eve_corporation_id end)
 | 
			
		||||
 | 
			
		||||
    map_member_alliance_ids =
 | 
			
		||||
      map_members
 | 
			
		||||
      |> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
 | 
			
		||||
      |> Enum.map(fn member -> member.eve_alliance_id end)
 | 
			
		||||
        map_member_alliance_ids =
 | 
			
		||||
          map_members
 | 
			
		||||
          |> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
 | 
			
		||||
          |> Enum.map(fn member -> member.eve_alliance_id end)
 | 
			
		||||
 | 
			
		||||
    {:ok,
 | 
			
		||||
     %{
 | 
			
		||||
       map_acl_owner_ids: map_acl_owner_ids,
 | 
			
		||||
       map_member_eve_ids: map_member_eve_ids,
 | 
			
		||||
       map_member_corporation_ids: map_member_corporation_ids,
 | 
			
		||||
       map_member_alliance_ids: map_member_alliance_ids
 | 
			
		||||
     }}
 | 
			
		||||
        map_characters =
 | 
			
		||||
          %{
 | 
			
		||||
            map_acl_owner_ids: map_acl_owner_ids,
 | 
			
		||||
            map_member_eve_ids: map_member_eve_ids,
 | 
			
		||||
            map_member_corporation_ids: map_member_corporation_ids,
 | 
			
		||||
            map_member_alliance_ids: map_member_alliance_ids
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
        WandererApp.Cache.insert(
 | 
			
		||||
          "map_characters-#{map_id}",
 | 
			
		||||
          map_characters
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        {:ok, map_characters}
 | 
			
		||||
 | 
			
		||||
      map_characters ->
 | 
			
		||||
        {:ok, map_characters}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_map_available_characters(map, user_characters) do
 | 
			
		||||
@@ -227,7 +245,7 @@ defmodule WandererApp.Maps do
 | 
			
		||||
       map_member_eve_ids: map_member_eve_ids,
 | 
			
		||||
       map_member_corporation_ids: map_member_corporation_ids,
 | 
			
		||||
       map_member_alliance_ids: map_member_alliance_ids
 | 
			
		||||
     }} = _get_map_characters(map)
 | 
			
		||||
     }} = get_map_characters(map)
 | 
			
		||||
 | 
			
		||||
    user_characters
 | 
			
		||||
    |> Enum.filter(fn c ->
 | 
			
		||||
 
 | 
			
		||||
@@ -29,15 +29,24 @@ defmodule WandererApp.Metrics.PromExPlugin do
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def event_metrics(_opts) do
 | 
			
		||||
    [
 | 
			
		||||
    base_metrics = [
 | 
			
		||||
      user_event_metrics(),
 | 
			
		||||
      character_event_metrics(),
 | 
			
		||||
      map_event_metrics(),
 | 
			
		||||
      map_subscription_metrics(),
 | 
			
		||||
      map_subscription_metrics()
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    advanced_metrics = [
 | 
			
		||||
      character_event_metrics(),
 | 
			
		||||
      characters_distribution_event_metrics(),
 | 
			
		||||
      esi_event_metrics(),
 | 
			
		||||
      json_api_metrics()
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    if WandererApp.Env.base_metrics_only() do
 | 
			
		||||
      base_metrics
 | 
			
		||||
    else
 | 
			
		||||
      base_metrics ++ advanced_metrics
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp user_event_metrics do
 | 
			
		||||
@@ -227,8 +236,8 @@ defmodule WandererApp.Metrics.PromExPlugin do
 | 
			
		||||
  defp get_esi_error_tag_values(metadata) do
 | 
			
		||||
    %{
 | 
			
		||||
      endpoint: Map.get(metadata, :endpoint, "unknown"),
 | 
			
		||||
      error_type: to_string(Map.get(metadata, :error_type, "unknown")),
 | 
			
		||||
      tracking_pool: Map.get(metadata, :tracking_pool, "unknown")
 | 
			
		||||
      error_type: inspect(Map.get(metadata, :error_type, "unknown")),
 | 
			
		||||
      tracking_pool: Map.get(metadata, :tracking_pool, "default")
 | 
			
		||||
    }
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -53,20 +53,8 @@ defmodule WandererApp.MapCharacterSettingsRepo do
 | 
			
		||||
  def get_tracked_by_map_all(map_id),
 | 
			
		||||
    do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
 | 
			
		||||
 | 
			
		||||
  def get_by_map(map_id, character_id) do
 | 
			
		||||
    case get_by_map_filtered(map_id, [character_id]) do
 | 
			
		||||
      {:ok, [setting | _]} ->
 | 
			
		||||
        {:ok, setting}
 | 
			
		||||
 | 
			
		||||
      {:ok, []} ->
 | 
			
		||||
        {:error, :not_found}
 | 
			
		||||
 | 
			
		||||
      {:error, reason} ->
 | 
			
		||||
        {:error, reason}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def track(settings) do
 | 
			
		||||
    {:ok, _} = get(settings.map_id, settings.character_id)
 | 
			
		||||
    # Only update the tracked field, preserving other fields
 | 
			
		||||
    WandererApp.Api.MapCharacterSettings.track(%{
 | 
			
		||||
      map_id: settings.map_id,
 | 
			
		||||
@@ -75,6 +63,7 @@ defmodule WandererApp.MapCharacterSettingsRepo do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def untrack(settings) do
 | 
			
		||||
    {:ok, _} = get(settings.map_id, settings.character_id)
 | 
			
		||||
    # Only update the tracked field, preserving other fields
 | 
			
		||||
    WandererApp.Api.MapCharacterSettings.untrack(%{
 | 
			
		||||
      map_id: settings.map_id,
 | 
			
		||||
@@ -83,22 +72,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def track!(settings) do
 | 
			
		||||
    case WandererApp.Api.MapCharacterSettings.track(%{
 | 
			
		||||
           map_id: settings.map_id,
 | 
			
		||||
           character_id: settings.character_id
 | 
			
		||||
         }) do
 | 
			
		||||
    case track(settings) do
 | 
			
		||||
      {:ok, result} -> result
 | 
			
		||||
      {:error, error} -> raise "Failed to track: #{inspect(error)}"
 | 
			
		||||
      error -> raise "Failed to track: #{inspect(error)}"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def untrack!(settings) do
 | 
			
		||||
    case WandererApp.Api.MapCharacterSettings.untrack(%{
 | 
			
		||||
           map_id: settings.map_id,
 | 
			
		||||
           character_id: settings.character_id
 | 
			
		||||
         }) do
 | 
			
		||||
    case untrack(settings) do
 | 
			
		||||
      {:ok, result} -> result
 | 
			
		||||
      {:error, error} -> raise "Failed to untrack: #{inspect(error)}"
 | 
			
		||||
      error -> raise "Failed to untrack: #{inspect(error)}"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -117,22 +100,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def follow!(settings) do
 | 
			
		||||
    case WandererApp.Api.MapCharacterSettings.follow(%{
 | 
			
		||||
           map_id: settings.map_id,
 | 
			
		||||
           character_id: settings.character_id
 | 
			
		||||
         }) do
 | 
			
		||||
    case follow(settings) do
 | 
			
		||||
      {:ok, result} -> result
 | 
			
		||||
      {:error, error} -> raise "Failed to follow: #{inspect(error)}"
 | 
			
		||||
      error -> raise "Failed to follow: #{inspect(error)}"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def unfollow!(settings) do
 | 
			
		||||
    case WandererApp.Api.MapCharacterSettings.unfollow(%{
 | 
			
		||||
           map_id: settings.map_id,
 | 
			
		||||
           character_id: settings.character_id
 | 
			
		||||
         }) do
 | 
			
		||||
    case unfollow(settings) do
 | 
			
		||||
      {:ok, result} -> result
 | 
			
		||||
      {:error, error} -> raise "Failed to unfollow: #{inspect(error)}"
 | 
			
		||||
      error -> raise "Failed to unfollow: #{inspect(error)}"
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -39,7 +39,7 @@ defmodule WandererApp.SecurityAudit do
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    # Store in database
 | 
			
		||||
    store_audit_entry(audit_entry)
 | 
			
		||||
    # store_audit_entry(audit_entry)
 | 
			
		||||
 | 
			
		||||
    # Send to telemetry for monitoring
 | 
			
		||||
    emit_telemetry_event(audit_entry)
 | 
			
		||||
@@ -489,11 +489,11 @@ defmodule WandererApp.SecurityAudit do
 | 
			
		||||
 | 
			
		||||
  defp store_audit_entry(audit_entry) do
 | 
			
		||||
    # Handle async processing if enabled
 | 
			
		||||
    if async_enabled?() do
 | 
			
		||||
      WandererApp.SecurityAudit.AsyncProcessor.log_event(audit_entry)
 | 
			
		||||
    else
 | 
			
		||||
      do_store_audit_entry(audit_entry)
 | 
			
		||||
    end
 | 
			
		||||
    # if async_enabled?() do
 | 
			
		||||
    #   WandererApp.SecurityAudit.AsyncProcessor.log_event(audit_entry)
 | 
			
		||||
    # else
 | 
			
		||||
    #   do_store_audit_entry(audit_entry)
 | 
			
		||||
    # end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc false
 | 
			
		||||
 
 | 
			
		||||
@@ -195,7 +195,7 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
 | 
			
		||||
    tracking_pool = WandererApp.Character.TrackingConfigUtils.get_active_pool!()
 | 
			
		||||
 | 
			
		||||
    base_options = [
 | 
			
		||||
      redirect_uri: callback_url(conn),
 | 
			
		||||
      redirect_uri: "#{WandererApp.Env.base_url()}/auth/eve/callback",
 | 
			
		||||
      with_wallet: with_wallet,
 | 
			
		||||
      is_admin?: is_admin?,
 | 
			
		||||
      tracking_pool: tracking_pool
 | 
			
		||||
 
 | 
			
		||||
@@ -52,11 +52,7 @@ defmodule WandererAppWeb.Api.EventsController do
 | 
			
		||||
 | 
			
		||||
  defp establish_sse_connection(conn, map_id, api_key, params) do
 | 
			
		||||
    # Parse event filter if provided
 | 
			
		||||
    event_filter =
 | 
			
		||||
      case Map.get(params, "events") do
 | 
			
		||||
        nil -> :all
 | 
			
		||||
        events -> EventFilter.parse(events)
 | 
			
		||||
      end
 | 
			
		||||
    event_filter = EventFilter.parse(Map.get(params, "events"))
 | 
			
		||||
 | 
			
		||||
    # Parse format parameter
 | 
			
		||||
    event_format = Map.get(params, "format", "legacy")
 | 
			
		||||
@@ -82,7 +78,7 @@ defmodule WandererAppWeb.Api.EventsController do
 | 
			
		||||
          send_event(
 | 
			
		||||
            conn,
 | 
			
		||||
            %{
 | 
			
		||||
              id: Ulid.generate(),
 | 
			
		||||
              id: Ecto.ULID.generate(),
 | 
			
		||||
              event: "connected",
 | 
			
		||||
              data: %{
 | 
			
		||||
                map_id: map_id,
 | 
			
		||||
 
 | 
			
		||||
@@ -113,7 +113,7 @@ defmodule WandererAppWeb.MapAuditAPIController do
 | 
			
		||||
  def index(conn, params) do
 | 
			
		||||
    with {:ok, map_id} <- APIUtils.fetch_map_id(params),
 | 
			
		||||
         {:ok, period} <- APIUtils.require_param(params, "period"),
 | 
			
		||||
         query <- WandererApp.Map.Audit.get_activity_query(map_id, period, "all"),
 | 
			
		||||
         query <- WandererApp.Map.Audit.get_map_activity_query(map_id, period, "all"),
 | 
			
		||||
         {:ok, data} <-
 | 
			
		||||
           Ash.read(query) do
 | 
			
		||||
      data = Enum.map(data, &map_audit_event_to_json/1)
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
 | 
			
		||||
    type: :object,
 | 
			
		||||
    properties: %{
 | 
			
		||||
      id: %OpenApiSpex.Schema{type: :string, format: :uuid},
 | 
			
		||||
      system_id: %OpenApiSpex.Schema{type: :string, format: :uuid},
 | 
			
		||||
      solar_system_id: %OpenApiSpex.Schema{type: :integer},
 | 
			
		||||
      eve_id: %OpenApiSpex.Schema{type: :string},
 | 
			
		||||
      character_eve_id: %OpenApiSpex.Schema{type: :string},
 | 
			
		||||
      name: %OpenApiSpex.Schema{type: :string, nullable: true},
 | 
			
		||||
@@ -31,13 +31,13 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
 | 
			
		||||
    },
 | 
			
		||||
    required: [
 | 
			
		||||
      :id,
 | 
			
		||||
      :system_id,
 | 
			
		||||
      :solar_system_id,
 | 
			
		||||
      :eve_id,
 | 
			
		||||
      :character_eve_id
 | 
			
		||||
    ],
 | 
			
		||||
    example: %{
 | 
			
		||||
      id: "sig-uuid-1",
 | 
			
		||||
      system_id: "sys-uuid-1",
 | 
			
		||||
      solar_system_id: 30_000_142,
 | 
			
		||||
      eve_id: "ABC-123",
 | 
			
		||||
      character_eve_id: "123456789",
 | 
			
		||||
      name: "Wormhole K162",
 | 
			
		||||
@@ -122,7 +122,15 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
 | 
			
		||||
      {:ok, signature} ->
 | 
			
		||||
        case WandererApp.Api.MapSystem.by_id(signature.system_id) do
 | 
			
		||||
          {:ok, system} when system.map_id == map_id ->
 | 
			
		||||
            json(conn, %{data: signature})
 | 
			
		||||
            # Add solar_system_id and remove system_id
 | 
			
		||||
            # Convert to a plain map to avoid encoder issues
 | 
			
		||||
            signature_data =
 | 
			
		||||
              signature
 | 
			
		||||
              |> Map.from_struct()
 | 
			
		||||
              |> Map.put(:solar_system_id, system.solar_system_id)
 | 
			
		||||
              |> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
 | 
			
		||||
 | 
			
		||||
            json(conn, %{data: signature_data})
 | 
			
		||||
 | 
			
		||||
          _ ->
 | 
			
		||||
            conn |> put_status(:not_found) |> json(%{error: "Signature not found"})
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
 | 
			
		||||
 | 
			
		||||
  import Plug.Conn
 | 
			
		||||
 | 
			
		||||
  alias Plug.Crypto
 | 
			
		||||
  alias WandererApp.Api.User
 | 
			
		||||
  alias WandererApp.SecurityAudit
 | 
			
		||||
  alias WandererApp.Audit.RequestContext
 | 
			
		||||
@@ -140,43 +141,60 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
 | 
			
		||||
  defp authenticate_bearer_token(conn) do
 | 
			
		||||
    case get_req_header(conn, "authorization") do
 | 
			
		||||
      ["Bearer " <> token] ->
 | 
			
		||||
        validate_api_token(token)
 | 
			
		||||
        validate_api_token(conn, token)
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        {:error, "Missing or invalid authorization header"}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp validate_api_token(token) do
 | 
			
		||||
    # Look up the map by its public API key
 | 
			
		||||
    case find_map_by_api_key(token) do
 | 
			
		||||
      {:ok, map} when not is_nil(map) ->
 | 
			
		||||
        # Get the actual owner of the map
 | 
			
		||||
        case User.by_id(map.owner_id, load: :characters) do
 | 
			
		||||
          {:ok, user} ->
 | 
			
		||||
            # Return the map owner as the authenticated user
 | 
			
		||||
            {:ok, user, map}
 | 
			
		||||
  defp validate_api_token(conn, token) do
 | 
			
		||||
    # Check for map identifier in path params
 | 
			
		||||
    # According to PR feedback, routes supply params["map_identifier"]
 | 
			
		||||
    case conn.params["map_identifier"] do
 | 
			
		||||
      nil ->
 | 
			
		||||
        # No map identifier in path - this might be a general API endpoint
 | 
			
		||||
        # For now, we'll return an error since we need to validate against a specific map
 | 
			
		||||
        {:error, "Authentication failed", :no_map_context}
 | 
			
		||||
 | 
			
		||||
      identifier ->
 | 
			
		||||
        # Resolve the identifier (could be UUID or slug)
 | 
			
		||||
        case resolve_map_identifier(identifier) do
 | 
			
		||||
          {:ok, map} ->
 | 
			
		||||
            # Validate the token matches this specific map's API key
 | 
			
		||||
            if is_binary(map.public_api_key) &&
 | 
			
		||||
                 Crypto.secure_compare(map.public_api_key, token) do
 | 
			
		||||
              # Get the map owner
 | 
			
		||||
              case User.by_id(map.owner_id, load: :characters) do
 | 
			
		||||
                {:ok, user} ->
 | 
			
		||||
                  {:ok, user, map}
 | 
			
		||||
 | 
			
		||||
                {:error, _} ->
 | 
			
		||||
                  {:error, "Authentication failed", :map_owner_not_found}
 | 
			
		||||
              end
 | 
			
		||||
            else
 | 
			
		||||
              {:error, "Authentication failed", :invalid_token_for_map}
 | 
			
		||||
            end
 | 
			
		||||
 | 
			
		||||
          {:error, _} ->
 | 
			
		||||
            # Return generic error with specific reason for internal logging
 | 
			
		||||
            {:error, "Authentication failed", :map_owner_not_found}
 | 
			
		||||
            {:error, "Authentication failed", :map_not_found}
 | 
			
		||||
        end
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        # Return generic error with specific reason for internal logging
 | 
			
		||||
        {:error, "Authentication failed", :invalid_api_key}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp find_map_by_api_key(api_key) do
 | 
			
		||||
    # Import necessary modules
 | 
			
		||||
    import Ash.Query
 | 
			
		||||
  # Helper to resolve map by ID or slug
 | 
			
		||||
  defp resolve_map_identifier(identifier) do
 | 
			
		||||
    alias WandererApp.Api.Map
 | 
			
		||||
 | 
			
		||||
    # Query for map with matching public API key
 | 
			
		||||
    Map
 | 
			
		||||
    |> filter(public_api_key == ^api_key)
 | 
			
		||||
    |> Ash.read_one()
 | 
			
		||||
    # Try as UUID first
 | 
			
		||||
    case Map.by_id(identifier) do
 | 
			
		||||
      {:ok, map} ->
 | 
			
		||||
        {:ok, map}
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        # Try as slug
 | 
			
		||||
        Map.get_map_by_slug(identifier)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp get_user_role(user) do
 | 
			
		||||
 
 | 
			
		||||
@@ -15,30 +15,6 @@ defmodule WandererAppWeb.Endpoint do
 | 
			
		||||
    max_age: 24 * 60 * 60 * 180
 | 
			
		||||
  ]
 | 
			
		||||
 | 
			
		||||
  # @impl SiteEncrypt
 | 
			
		||||
  # def certification do
 | 
			
		||||
  #   SiteEncrypt.configure(
 | 
			
		||||
  #     client: :native,
 | 
			
		||||
  #     mode: :auto,
 | 
			
		||||
  #     days_to_renew: 30,
 | 
			
		||||
  #     domains: ["dev.wanderer.deadly-w.space"],
 | 
			
		||||
  #     emails: ["dmitriypopovsamara@gmail.com"],
 | 
			
		||||
  #     db_folder: System.get_env("SITE_ENCRYPT_DB", Path.join("tmp", "site_encrypt_db")),
 | 
			
		||||
  #     backup: Path.join(Path.join("tmp", "site_encrypt_db"), "site_encrypt_backup.tgz"),
 | 
			
		||||
  #     directory_url:
 | 
			
		||||
  #       case System.get_env("CERT_MODE", "local") do
 | 
			
		||||
  #         "local" ->
 | 
			
		||||
  #           {:internal, port: 4001}
 | 
			
		||||
 | 
			
		||||
  #         "staging" ->
 | 
			
		||||
  #           "https://acme-staging-v02.api.letsencrypt.org/directory"
 | 
			
		||||
 | 
			
		||||
  #         "production" ->
 | 
			
		||||
  #           "https://acme-v02.api.letsencrypt.org/directory"
 | 
			
		||||
  #       end
 | 
			
		||||
  #   )
 | 
			
		||||
  # end
 | 
			
		||||
 | 
			
		||||
  socket "/live", Phoenix.LiveView.Socket,
 | 
			
		||||
    websocket: [compress: true, connect_info: [session: @session_options]]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -75,13 +75,12 @@ defmodule WandererAppWeb.CharactersLive do
 | 
			
		||||
  def handle_event("delete", %{"character_id" => character_id}, socket) do
 | 
			
		||||
    WandererApp.Character.TrackerManager.stop_tracking(character_id)
 | 
			
		||||
 | 
			
		||||
    {:ok, map_user_settings} =
 | 
			
		||||
    {:ok, map_character_settings} =
 | 
			
		||||
      WandererApp.Api.MapCharacterSettings.tracked_by_character(%{character_id: character_id})
 | 
			
		||||
 | 
			
		||||
    map_user_settings
 | 
			
		||||
    map_character_settings
 | 
			
		||||
    |> Enum.each(fn settings ->
 | 
			
		||||
      settings
 | 
			
		||||
      |> WandererApp.Api.MapCharacterSettings.untrack()
 | 
			
		||||
      {:ok, _} = WandererApp.MapCharacterSettingsRepo.untrack(settings)
 | 
			
		||||
    end)
 | 
			
		||||
 | 
			
		||||
    {:ok, updated_character} =
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do
 | 
			
		||||
  require Logger
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def mount(_params, %{"user_id" => user_id} = _session, socket) when not is_nil(user_id) do
 | 
			
		||||
  def mount(_params, _session, socket) do
 | 
			
		||||
    {:ok, maps} = WandererApp.Maps.get_available_maps(socket.assigns.current_user)
 | 
			
		||||
 | 
			
		||||
    {:ok,
 | 
			
		||||
@@ -14,7 +14,6 @@ defmodule WandererAppWeb.CharactersTrackingLive do
 | 
			
		||||
       characters: [],
 | 
			
		||||
       selected_map: nil,
 | 
			
		||||
       selected_map_slug: nil,
 | 
			
		||||
       user_id: user_id,
 | 
			
		||||
       maps: maps |> Enum.sort_by(& &1.name, :asc)
 | 
			
		||||
     )}
 | 
			
		||||
  end
 | 
			
		||||
@@ -37,24 +36,22 @@ defmodule WandererAppWeb.CharactersTrackingLive do
 | 
			
		||||
    |> assign(:page_title, "Characters Tracking")
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp apply_action(socket, :characters, %{"slug" => map_slug} = _params) do
 | 
			
		||||
    selected_map = socket.assigns.maps |> Enum.find(&(&1.slug == map_slug))
 | 
			
		||||
 | 
			
		||||
    {:ok, character_settings} =
 | 
			
		||||
      WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
 | 
			
		||||
 | 
			
		||||
    user_id = socket.assigns.user_id
 | 
			
		||||
  defp apply_action(
 | 
			
		||||
         %{assigns: %{current_user: current_user, maps: maps}} = socket,
 | 
			
		||||
         :characters,
 | 
			
		||||
         %{"slug" => map_slug} = _params
 | 
			
		||||
       ) do
 | 
			
		||||
    selected_map = maps |> Enum.find(&(&1.slug == map_slug))
 | 
			
		||||
 | 
			
		||||
    socket
 | 
			
		||||
    |> assign(:active_page, :characters_tracking)
 | 
			
		||||
    |> assign(:page_title, "Characters Tracking")
 | 
			
		||||
    |> assign(
 | 
			
		||||
      selected_map: selected_map,
 | 
			
		||||
      selected_map_slug: map_slug,
 | 
			
		||||
      character_settings: character_settings
 | 
			
		||||
      selected_map_slug: map_slug
 | 
			
		||||
    )
 | 
			
		||||
    |> assign_async(:characters, fn ->
 | 
			
		||||
      WandererApp.Maps.load_characters(selected_map, character_settings, user_id)
 | 
			
		||||
      WandererApp.Maps.load_characters(selected_map, current_user.id)
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -71,55 +68,36 @@ defmodule WandererAppWeb.CharactersTrackingLive do
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @impl true
 | 
			
		||||
  def handle_event("toggle_track", %{"character_id" => character_id}, socket) do
 | 
			
		||||
  def handle_event(
 | 
			
		||||
        "toggle_track",
 | 
			
		||||
        %{"character_id" => character_id},
 | 
			
		||||
        %{assigns: %{current_user: current_user}} = socket
 | 
			
		||||
      ) do
 | 
			
		||||
    selected_map = socket.assigns.selected_map
 | 
			
		||||
    character_settings = socket.assigns.character_settings
 | 
			
		||||
 | 
			
		||||
    case character_settings |> Enum.find(&(&1.character_id == character_id)) do
 | 
			
		||||
      nil ->
 | 
			
		||||
        WandererApp.MapCharacterSettingsRepo.create(%{
 | 
			
		||||
          character_id: character_id,
 | 
			
		||||
          map_id: selected_map.id,
 | 
			
		||||
          tracked: true
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        {:noreply, socket}
 | 
			
		||||
 | 
			
		||||
      character_setting ->
 | 
			
		||||
        case character_setting.tracked do
 | 
			
		||||
          true ->
 | 
			
		||||
            character_setting
 | 
			
		||||
            |> WandererApp.MapCharacterSettingsRepo.untrack!()
 | 
			
		||||
 | 
			
		||||
            WandererApp.Map.Server.untrack_characters(selected_map.id, [
 | 
			
		||||
              character_setting.character_id
 | 
			
		||||
            ])
 | 
			
		||||
 | 
			
		||||
          _ ->
 | 
			
		||||
            character_setting
 | 
			
		||||
            |> WandererApp.MapCharacterSettingsRepo.track!()
 | 
			
		||||
        end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    %{result: characters} = socket.assigns.characters
 | 
			
		||||
 | 
			
		||||
    {:ok, character_settings} =
 | 
			
		||||
      WandererApp.Character.Activity.get_map_character_settings(selected_map.id)
 | 
			
		||||
    case characters |> Enum.find(&(&1.id == character_id)) do
 | 
			
		||||
      %{tracked: false} ->
 | 
			
		||||
        WandererApp.MapCharacterSettingsRepo.track(%{
 | 
			
		||||
          character_id: character_id,
 | 
			
		||||
          map_id: selected_map.id
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    characters =
 | 
			
		||||
      characters
 | 
			
		||||
      |> Enum.map(fn c ->
 | 
			
		||||
        WandererApp.Maps.map_character(
 | 
			
		||||
          c,
 | 
			
		||||
          character_settings |> Enum.find(&(&1.character_id == c.id))
 | 
			
		||||
        )
 | 
			
		||||
      end)
 | 
			
		||||
      %{tracked: true} ->
 | 
			
		||||
        WandererApp.MapCharacterSettingsRepo.untrack(%{
 | 
			
		||||
          character_id: character_id,
 | 
			
		||||
          map_id: selected_map.id
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        WandererApp.Map.Server.untrack_characters(selected_map.id, [
 | 
			
		||||
          character_id
 | 
			
		||||
        ])
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    {:noreply,
 | 
			
		||||
     socket
 | 
			
		||||
     |> assign(character_settings: character_settings)
 | 
			
		||||
     |> assign_async(:characters, fn ->
 | 
			
		||||
       {:ok, %{characters: characters}}
 | 
			
		||||
       WandererApp.Maps.load_characters(selected_map, current_user.id)
 | 
			
		||||
     end)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -333,21 +333,18 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
 | 
			
		||||
  def needs_tracking_setup?(
 | 
			
		||||
        only_tracked_characters,
 | 
			
		||||
        characters,
 | 
			
		||||
        character_settings,
 | 
			
		||||
        user_permissions
 | 
			
		||||
      ) do
 | 
			
		||||
    tracked_count =
 | 
			
		||||
      characters
 | 
			
		||||
      |> Enum.count(fn char ->
 | 
			
		||||
        setting = Enum.find(character_settings, &(&1.character_id == char.id))
 | 
			
		||||
        setting && setting.tracked
 | 
			
		||||
        char.tracked
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    untracked_count =
 | 
			
		||||
      characters
 | 
			
		||||
      |> Enum.count(fn char ->
 | 
			
		||||
        setting = Enum.find(character_settings, &(&1.character_id == char.id))
 | 
			
		||||
        setting == nil || !setting.tracked
 | 
			
		||||
        !char.tracked
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    user_permissions.track_character &&
 | 
			
		||||
 
 | 
			
		||||
@@ -80,25 +80,73 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
 | 
			
		||||
            current_user: %{id: current_user_id},
 | 
			
		||||
            main_character_id: main_character_id,
 | 
			
		||||
            has_tracked_characters?: true,
 | 
			
		||||
            map_user_settings: map_user_settings,
 | 
			
		||||
            user_permissions: %{delete_connection: true}
 | 
			
		||||
          }
 | 
			
		||||
        } =
 | 
			
		||||
          socket
 | 
			
		||||
      )
 | 
			
		||||
      when not is_nil(main_character_id) do
 | 
			
		||||
    solar_system_source_id = solar_system_source_id |> String.to_integer()
 | 
			
		||||
    solar_system_target_id = solar_system_target_id |> String.to_integer()
 | 
			
		||||
 | 
			
		||||
    map_id
 | 
			
		||||
    |> WandererApp.Map.Server.delete_connection(%{
 | 
			
		||||
      solar_system_source_id: solar_system_source_id |> String.to_integer(),
 | 
			
		||||
      solar_system_target_id: solar_system_target_id |> String.to_integer()
 | 
			
		||||
      solar_system_source_id: solar_system_source_id,
 | 
			
		||||
      solar_system_target_id: solar_system_target_id
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    delete_connection_with_sigs =
 | 
			
		||||
      map_user_settings
 | 
			
		||||
      |> WandererApp.MapUserSettingsRepo.to_form_data!()
 | 
			
		||||
      |> WandererApp.MapUserSettingsRepo.get_boolean_setting("delete_connection_with_sigs")
 | 
			
		||||
 | 
			
		||||
    if delete_connection_with_sigs do
 | 
			
		||||
      target_system =
 | 
			
		||||
        WandererApp.Map.find_system_by_location(
 | 
			
		||||
          map_id,
 | 
			
		||||
          %{solar_system_id: solar_system_target_id}
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
      if not is_nil(target_system.linked_sig_eve_id) do
 | 
			
		||||
        {:ok, signatures} =
 | 
			
		||||
          WandererApp.Api.MapSystemSignature.by_linked_system_id(solar_system_target_id)
 | 
			
		||||
 | 
			
		||||
        signatures
 | 
			
		||||
        |> Enum.each(fn s ->
 | 
			
		||||
          if not is_nil(s.temporary_name) && s.temporary_name == target_system.temporary_name do
 | 
			
		||||
            map_id
 | 
			
		||||
            |> WandererApp.Map.Server.update_system_temporary_name(%{
 | 
			
		||||
              solar_system_id: solar_system_target_id,
 | 
			
		||||
              temporary_name: nil
 | 
			
		||||
            })
 | 
			
		||||
          end
 | 
			
		||||
 | 
			
		||||
          map_id
 | 
			
		||||
          |> WandererApp.Map.Server.update_system_linked_sig_eve_id(%{
 | 
			
		||||
            solar_system_id: solar_system_target_id,
 | 
			
		||||
            linked_sig_eve_id: nil
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          s
 | 
			
		||||
          |> WandererApp.Api.MapSystemSignature.destroy!()
 | 
			
		||||
        end)
 | 
			
		||||
 | 
			
		||||
        WandererApp.Map.Server.Impl.broadcast!(
 | 
			
		||||
          map_id,
 | 
			
		||||
          :signatures_updated,
 | 
			
		||||
          solar_system_source_id
 | 
			
		||||
        )
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    {:ok, _} =
 | 
			
		||||
      WandererApp.User.ActivityTracker.track_map_event(:map_connection_removed, %{
 | 
			
		||||
        character_id: main_character_id,
 | 
			
		||||
        user_id: current_user_id,
 | 
			
		||||
        map_id: map_id,
 | 
			
		||||
        solar_system_source_id: "#{solar_system_source_id}" |> String.to_integer(),
 | 
			
		||||
        solar_system_target_id: "#{solar_system_target_id}" |> String.to_integer()
 | 
			
		||||
        solar_system_source_id: solar_system_source_id,
 | 
			
		||||
        solar_system_target_id: solar_system_target_id
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
    {:noreply, socket}
 | 
			
		||||
 
 | 
			
		||||
@@ -422,14 +422,11 @@ defmodule WandererAppWeb.MapCoreEventHandler do
 | 
			
		||||
             current_user_characters |> Enum.map(& &1.id)
 | 
			
		||||
           ),
 | 
			
		||||
         {:ok, map_user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
 | 
			
		||||
         {:ok, character_settings} <-
 | 
			
		||||
           WandererApp.Character.Activity.get_map_character_settings(map_id),
 | 
			
		||||
         {:ok, %{characters: available_map_characters}} =
 | 
			
		||||
           WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
 | 
			
		||||
           WandererApp.Maps.load_characters(map, current_user_id) do
 | 
			
		||||
      tracked_data =
 | 
			
		||||
        get_tracked_data(
 | 
			
		||||
          available_map_characters,
 | 
			
		||||
          character_settings,
 | 
			
		||||
          user_permissions,
 | 
			
		||||
          only_tracked_characters
 | 
			
		||||
        )
 | 
			
		||||
@@ -473,15 +470,13 @@ defmodule WandererAppWeb.MapCoreEventHandler do
 | 
			
		||||
 | 
			
		||||
  defp get_tracked_data(
 | 
			
		||||
         available_map_characters,
 | 
			
		||||
         character_settings,
 | 
			
		||||
         user_permissions,
 | 
			
		||||
         only_tracked_characters
 | 
			
		||||
       ) do
 | 
			
		||||
    tracked_characters =
 | 
			
		||||
      available_map_characters
 | 
			
		||||
      |> Enum.filter(fn char ->
 | 
			
		||||
        setting = Enum.find(character_settings, &(&1.character_id == char.id))
 | 
			
		||||
        setting != nil && setting.tracked == true
 | 
			
		||||
        char.tracked
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    all_tracked? =
 | 
			
		||||
@@ -492,7 +487,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
 | 
			
		||||
      MapCharactersEventHandler.needs_tracking_setup?(
 | 
			
		||||
        only_tracked_characters,
 | 
			
		||||
        available_map_characters,
 | 
			
		||||
        character_settings,
 | 
			
		||||
        user_permissions
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -179,42 +179,50 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
 | 
			
		||||
        } = socket
 | 
			
		||||
      )
 | 
			
		||||
      when not is_nil(main_character_id) do
 | 
			
		||||
    solar_system_source = get_integer(solar_system_source)
 | 
			
		||||
    solar_system_target = get_integer(solar_system_target)
 | 
			
		||||
    with solar_system_source <- get_integer(solar_system_source),
 | 
			
		||||
         solar_system_target <- get_integer(solar_system_target),
 | 
			
		||||
         {:ok, source_system} <-
 | 
			
		||||
           WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
 | 
			
		||||
             map_id: map_id,
 | 
			
		||||
             solar_system_id: solar_system_source
 | 
			
		||||
           }),
 | 
			
		||||
         signature <-
 | 
			
		||||
           WandererApp.Api.MapSystemSignature.by_system_id!(source_system.id)
 | 
			
		||||
           |> Enum.find(fn s -> s.eve_id == signature_eve_id end),
 | 
			
		||||
         target_system <-
 | 
			
		||||
           WandererApp.Map.find_system_by_location(
 | 
			
		||||
             map_id,
 | 
			
		||||
             %{solar_system_id: solar_system_target}
 | 
			
		||||
           ) do
 | 
			
		||||
      if not is_nil(signature) do
 | 
			
		||||
        signature
 | 
			
		||||
        |> WandererApp.Api.MapSystemSignature.update_group!(%{group: "Wormhole"})
 | 
			
		||||
        |> WandererApp.Api.MapSystemSignature.update_linked_system(%{
 | 
			
		||||
          linked_system_id: solar_system_target
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    case WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
 | 
			
		||||
           map_id: map_id,
 | 
			
		||||
           solar_system_id: solar_system_source
 | 
			
		||||
         }) do
 | 
			
		||||
      {:ok, system} ->
 | 
			
		||||
        WandererApp.Api.MapSystemSignature.by_system_id!(system.id)
 | 
			
		||||
        |> Enum.filter(fn s -> s.eve_id == signature_eve_id end)
 | 
			
		||||
        |> Enum.each(fn s ->
 | 
			
		||||
          s
 | 
			
		||||
          |> WandererApp.Api.MapSystemSignature.update_group!(%{group: "Wormhole"})
 | 
			
		||||
          |> WandererApp.Api.MapSystemSignature.update_linked_system(%{
 | 
			
		||||
            linked_system_id: solar_system_target
 | 
			
		||||
          })
 | 
			
		||||
        end)
 | 
			
		||||
 | 
			
		||||
        map_system =
 | 
			
		||||
          WandererApp.Map.find_system_by_location(
 | 
			
		||||
            map_id,
 | 
			
		||||
            %{solar_system_id: solar_system_target}
 | 
			
		||||
          )
 | 
			
		||||
 | 
			
		||||
        if not is_nil(map_system) && is_nil(map_system.linked_sig_eve_id) do
 | 
			
		||||
        if not is_nil(target_system) &&
 | 
			
		||||
             is_nil(target_system.linked_sig_eve_id) do
 | 
			
		||||
          map_id
 | 
			
		||||
          |> WandererApp.Map.Server.update_system_linked_sig_eve_id(%{
 | 
			
		||||
            solar_system_id: solar_system_target,
 | 
			
		||||
            linked_sig_eve_id: signature_eve_id
 | 
			
		||||
          })
 | 
			
		||||
 | 
			
		||||
          if not is_nil(signature.temporary_name) do
 | 
			
		||||
            map_id
 | 
			
		||||
            |> WandererApp.Map.Server.update_system_temporary_name(%{
 | 
			
		||||
              solar_system_id: solar_system_target,
 | 
			
		||||
              temporary_name: signature.temporary_name
 | 
			
		||||
            })
 | 
			
		||||
          end
 | 
			
		||||
        end
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
        WandererApp.Map.Server.Impl.broadcast!(map_id, :signatures_updated, solar_system_source)
 | 
			
		||||
 | 
			
		||||
        {:noreply, socket}
 | 
			
		||||
      WandererApp.Map.Server.Impl.broadcast!(map_id, :signatures_updated, solar_system_source)
 | 
			
		||||
 | 
			
		||||
      {:noreply, socket}
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        {:noreply, socket}
 | 
			
		||||
    end
 | 
			
		||||
@@ -320,6 +328,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
 | 
			
		||||
          :eve_id,
 | 
			
		||||
          :character_eve_id,
 | 
			
		||||
          :name,
 | 
			
		||||
          :temporary_name,
 | 
			
		||||
          :description,
 | 
			
		||||
          :kind,
 | 
			
		||||
          :group,
 | 
			
		||||
 
 | 
			
		||||
@@ -153,7 +153,7 @@ defmodule WandererAppWeb.MapAuditLive do
 | 
			
		||||
    } =
 | 
			
		||||
      socket.assigns
 | 
			
		||||
 | 
			
		||||
    query = WandererApp.Map.Audit.get_activity_query(map_id, period, activity)
 | 
			
		||||
    query = WandererApp.Map.Audit.get_map_activity_query(map_id, period, activity)
 | 
			
		||||
 | 
			
		||||
    AshPagify.validate_and_run(query, params, opts)
 | 
			
		||||
    |> case do
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								mix.exs
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								mix.exs
									
									
									
									
									
								
							@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
 | 
			
		||||
 | 
			
		||||
  @source_url "https://github.com/wanderer-industries/wanderer"
 | 
			
		||||
 | 
			
		||||
  @version "1.75.13"
 | 
			
		||||
  @version "1.77.2"
 | 
			
		||||
 | 
			
		||||
  def project do
 | 
			
		||||
    [
 | 
			
		||||
@@ -105,7 +105,7 @@ defmodule WandererApp.MixProject do
 | 
			
		||||
      {:ash_postgres, "~> 2.4"},
 | 
			
		||||
      {:exsync, "~> 0.4", only: :dev},
 | 
			
		||||
      {:nimble_csv, "~> 1.2.0"},
 | 
			
		||||
      {:ulid, "~> 0.2.0"},
 | 
			
		||||
      {:ecto_ulid_next, "~> 1.0.2"},
 | 
			
		||||
      {:cachex, "~> 3.6"},
 | 
			
		||||
      {:live_select, "~> 1.5"},
 | 
			
		||||
      {:nebulex, "~> 2.6"},
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								mix.lock
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								mix.lock
									
									
									
									
									
								
							@@ -35,6 +35,7 @@
 | 
			
		||||
  "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"},
 | 
			
		||||
  "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"},
 | 
			
		||||
  "ecto_sql": {:hex, :ecto_sql, "3.12.0", "73cea17edfa54bde76ee8561b30d29ea08f630959685006d9c6e7d1e59113b7d", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dc9e4d206f274f3947e96142a8fdc5f69a2a6a9abb4649ef5c882323b6d512f0"},
 | 
			
		||||
  "ecto_ulid_next": {:hex, :ecto_ulid_next, "1.0.2", "8372f3c589c8fa50ea7b127dabe008528837b11781f65bfc72d96259d49b44c5", [:mix], [{:ecto, "~> 3.2", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "61c9c2c531f87ce7e2e9e57fc60d533fe97b3a62a43c21b632b0824f0773bcbe"},
 | 
			
		||||
  "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"},
 | 
			
		||||
  "error_tracker": {:hex, :error_tracker, "0.2.2", "7635f5ed6016df10d8e63348375acb2ca411e2f6f9703ee90cc2d4262af5faec", [:mix], [{:ecto, "~> 3.11", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, ">= 0.0.0", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, "~> 4.6", [hex: :phoenix_ecto, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.19 or ~> 1.0", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "b975978f64d27373d3486d7de477a699e735f8c0b1c74a7370ecb80e7ae97903"},
 | 
			
		||||
  "eternal": {:hex, :eternal, "1.2.2", "d1641c86368de99375b98d183042dd6c2b234262b8d08dfd72b9eeaafc2a1abd", [:mix], [], "hexpm", "2c9fe32b9c3726703ba5e1d43a1d255a4f3f2d8f8f9bc19f094c7cb1a7a9e782"},
 | 
			
		||||
@@ -43,6 +44,7 @@
 | 
			
		||||
  "ex_check": {:hex, :ex_check, "0.14.0", "d6fbe0bcc51cf38fea276f5bc2af0c9ae0a2bb059f602f8de88709421dae4f0e", [:mix], [], "hexpm", "8a602e98c66e6a4be3a639321f1f545292042f290f91fa942a285888c6868af0"},
 | 
			
		||||
  "ex_doc": {:hex, :ex_doc, "0.37.3", "f7816881a443cd77872b7d6118e8a55f547f49903aef8747dbcb345a75b462f9", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "e6aebca7156e7c29b5da4daa17f6361205b2ae5f26e5c7d8ca0d3f7e18972233"},
 | 
			
		||||
  "ex_rated": {:hex, :ex_rated, "2.1.0", "d40e6fe35097b10222df2db7bb5dd801d57211bac65f29063de5f201c2a6aebc", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm", "936c155337253ed6474f06d941999dd3a9cf0fe767ec99a59f2d2989dc2cc13f"},
 | 
			
		||||
  "ex_ulid": {:hex, :ex_ulid, "0.1.0", "e6e717c57344f6e500d0190ccb4edc862b985a3680f15834af992ec065d4dcff", [:mix], [], "hexpm", "a2befd477aebc4639563de7e233e175cacf8a8f42c8f6778c88d60c13bf20860"},
 | 
			
		||||
  "excoveralls": {:hex, :excoveralls, "0.18.5", "e229d0a65982613332ec30f07940038fe451a2e5b29bce2a5022165f0c9b157e", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "523fe8a15603f86d64852aab2abe8ddbd78e68579c8525ae765facc5eae01562"},
 | 
			
		||||
  "expo": {:hex, :expo, "0.5.2", "beba786aab8e3c5431813d7a44b828e7b922bfa431d6bfbada0904535342efe2", [:mix], [], "hexpm", "8c9bfa06ca017c9cb4020fabe980bc7fdb1aaec059fd004c2ab3bff03b1c599c"},
 | 
			
		||||
  "exsync": {:hex, :exsync, "0.4.1", "0a14fe4bfcb80a509d8a0856be3dd070fffe619b9ba90fec13c58b316c176594", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "cefb22aa805ec97ffc5b75a4e1dc54bcaf781e8b32564bf74abbe5803d1b5178"},
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,9 @@ As part of the Wanderer platform, a public API has been introduced to help users
 | 
			
		||||
## Authentication
 | 
			
		||||
 | 
			
		||||
Each request to the Wanderer APIs that being with /api/map must include a valid API key in the `Authorization` header. The format is:
 | 
			
		||||
 | 
			
		||||
    Authorization: Bearer <YOUR_MAP_API_KEY>
 | 
			
		||||
```
 | 
			
		||||
Authorization: Bearer <YOUR_MAP_API_KEY>
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
If the API key is missing or incorrect, you'll receive a `401 Unauthorized` response.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
defmodule WandererApp.Repo.Migrations.AddSignatureTempName do
 | 
			
		||||
  @moduledoc """
 | 
			
		||||
  Updates resources based on their most recent snapshots.
 | 
			
		||||
 | 
			
		||||
  This file was autogenerated with `mix ash_postgres.generate_migrations`
 | 
			
		||||
  """
 | 
			
		||||
 | 
			
		||||
  use Ecto.Migration
 | 
			
		||||
 | 
			
		||||
  def up do
 | 
			
		||||
    alter table(:map_system_signatures_v1) do
 | 
			
		||||
      add :temporary_name, :text
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down do
 | 
			
		||||
    alter table(:map_system_signatures_v1) do
 | 
			
		||||
      remove :temporary_name
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
@@ -0,0 +1,217 @@
 | 
			
		||||
{
 | 
			
		||||
  "attributes": [
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"gen_random_uuid()\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": true,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "id",
 | 
			
		||||
      "type": "uuid"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "eve_id",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "character_eve_id",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "name",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "description",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "temporary_name",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "type",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "linked_system_id",
 | 
			
		||||
      "type": "bigint"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "kind",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "group",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "custom_info",
 | 
			
		||||
      "type": "text"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "false",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "deleted",
 | 
			
		||||
      "type": "boolean"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "update_forced_at",
 | 
			
		||||
      "type": "utc_datetime"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "inserted_at",
 | 
			
		||||
      "type": "utc_datetime_usec"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": false,
 | 
			
		||||
      "default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": null,
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "updated_at",
 | 
			
		||||
      "type": "utc_datetime_usec"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "allow_nil?": true,
 | 
			
		||||
      "default": "nil",
 | 
			
		||||
      "generated?": false,
 | 
			
		||||
      "primary_key?": false,
 | 
			
		||||
      "references": {
 | 
			
		||||
        "deferrable": false,
 | 
			
		||||
        "destination_attribute": "id",
 | 
			
		||||
        "destination_attribute_default": null,
 | 
			
		||||
        "destination_attribute_generated": null,
 | 
			
		||||
        "index?": false,
 | 
			
		||||
        "match_type": null,
 | 
			
		||||
        "match_with": null,
 | 
			
		||||
        "multitenancy": {
 | 
			
		||||
          "attribute": null,
 | 
			
		||||
          "global": null,
 | 
			
		||||
          "strategy": null
 | 
			
		||||
        },
 | 
			
		||||
        "name": "map_system_signatures_v1_system_id_fkey",
 | 
			
		||||
        "on_delete": null,
 | 
			
		||||
        "on_update": null,
 | 
			
		||||
        "primary_key?": true,
 | 
			
		||||
        "schema": "public",
 | 
			
		||||
        "table": "map_system_v1"
 | 
			
		||||
      },
 | 
			
		||||
      "size": null,
 | 
			
		||||
      "source": "system_id",
 | 
			
		||||
      "type": "uuid"
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "base_filter": null,
 | 
			
		||||
  "check_constraints": [],
 | 
			
		||||
  "custom_indexes": [],
 | 
			
		||||
  "custom_statements": [],
 | 
			
		||||
  "has_create_action": true,
 | 
			
		||||
  "hash": "D1885311D35F70BB9117EB170BD2E07D0CFEEB9E6AE4D971C7DE8DBF9CCDED10",
 | 
			
		||||
  "identities": [
 | 
			
		||||
    {
 | 
			
		||||
      "all_tenants?": false,
 | 
			
		||||
      "base_filter": null,
 | 
			
		||||
      "index_name": "map_system_signatures_v1_uniq_system_eve_id_index",
 | 
			
		||||
      "keys": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "atom",
 | 
			
		||||
          "value": "system_id"
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          "type": "atom",
 | 
			
		||||
          "value": "eve_id"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "name": "uniq_system_eve_id",
 | 
			
		||||
      "nils_distinct?": true,
 | 
			
		||||
      "where": null
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "multitenancy": {
 | 
			
		||||
    "attribute": null,
 | 
			
		||||
    "global": null,
 | 
			
		||||
    "strategy": null
 | 
			
		||||
  },
 | 
			
		||||
  "repo": "Elixir.WandererApp.Repo",
 | 
			
		||||
  "schema": null,
 | 
			
		||||
  "table": "map_system_signatures_v1"
 | 
			
		||||
}
 | 
			
		||||
@@ -100,7 +100,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
 | 
			
		||||
    test "creates a new signature with valid parameters", %{conn: conn, map: map} do
 | 
			
		||||
      signature_params = %{
 | 
			
		||||
        "system_id" => Ecto.UUID.generate(),
 | 
			
		||||
        "solar_system_id" => 30_000_142,
 | 
			
		||||
        "eve_id" => "ABC-123",
 | 
			
		||||
        "character_eve_id" => "123456789",
 | 
			
		||||
        "name" => "Test Signature",
 | 
			
		||||
@@ -132,7 +132,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
 | 
			
		||||
    test "handles signature creation with minimal required fields", %{conn: conn, map: map} do
 | 
			
		||||
      minimal_params = %{
 | 
			
		||||
        "system_id" => Ecto.UUID.generate(),
 | 
			
		||||
        "solar_system_id" => 30_000_143,
 | 
			
		||||
        "eve_id" => "XYZ-456",
 | 
			
		||||
        "character_eve_id" => "987654321"
 | 
			
		||||
      }
 | 
			
		||||
@@ -152,7 +152,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
 | 
			
		||||
    test "handles signature creation with all optional fields", %{conn: conn, map: map} do
 | 
			
		||||
      complete_params = %{
 | 
			
		||||
        "system_id" => Ecto.UUID.generate(),
 | 
			
		||||
        "solar_system_id" => 30_000_144,
 | 
			
		||||
        "eve_id" => "DEF-789",
 | 
			
		||||
        "character_eve_id" => "456789123",
 | 
			
		||||
        "name" => "Complete Signature",
 | 
			
		||||
@@ -181,7 +181,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
      map = Factory.insert(:map)
 | 
			
		||||
 | 
			
		||||
      signature_params = %{
 | 
			
		||||
        "system_id" => Ecto.UUID.generate(),
 | 
			
		||||
        "solar_system_id" => 30_000_145,
 | 
			
		||||
        "eve_id" => "ABC-123",
 | 
			
		||||
        "character_eve_id" => "123456789"
 | 
			
		||||
      }
 | 
			
		||||
@@ -392,11 +392,11 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
 | 
			
		||||
    test "validates signature creation with invalid data types", %{conn: conn, map: map} do
 | 
			
		||||
      invalid_params = [
 | 
			
		||||
        %{"system_id" => "not-a-uuid", "eve_id" => "ABC", "character_eve_id" => "123"},
 | 
			
		||||
        %{"system_id" => Ecto.UUID.generate(), "eve_id" => 123, "character_eve_id" => "123"},
 | 
			
		||||
        %{"system_id" => Ecto.UUID.generate(), "eve_id" => "ABC", "character_eve_id" => 123},
 | 
			
		||||
        %{"solar_system_id" => "not-an-integer", "eve_id" => "ABC", "character_eve_id" => "123"},
 | 
			
		||||
        %{"solar_system_id" => 30_000_142, "eve_id" => 123, "character_eve_id" => "123"},
 | 
			
		||||
        %{"solar_system_id" => 30_000_142, "eve_id" => "ABC", "character_eve_id" => 123},
 | 
			
		||||
        %{
 | 
			
		||||
          "system_id" => Ecto.UUID.generate(),
 | 
			
		||||
          "solar_system_id" => 30_000_142,
 | 
			
		||||
          "eve_id" => "ABC",
 | 
			
		||||
          "character_eve_id" => "123",
 | 
			
		||||
          "linked_system_id" => "not-an-integer"
 | 
			
		||||
@@ -426,7 +426,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
      long_string = String.duplicate("a", 1000)
 | 
			
		||||
 | 
			
		||||
      long_params = %{
 | 
			
		||||
        "system_id" => Ecto.UUID.generate(),
 | 
			
		||||
        "solar_system_id" => 30_000_146,
 | 
			
		||||
        "eve_id" => "LONG-123",
 | 
			
		||||
        "character_eve_id" => "123456789",
 | 
			
		||||
        "name" => long_string,
 | 
			
		||||
@@ -448,7 +448,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
 | 
			
		||||
    test "handles special characters in signature data", %{conn: conn, map: map} do
 | 
			
		||||
      special_params = %{
 | 
			
		||||
        "system_id" => Ecto.UUID.generate(),
 | 
			
		||||
        "solar_system_id" => 30_000_147,
 | 
			
		||||
        "eve_id" => "ABC-123",
 | 
			
		||||
        "character_eve_id" => "123456789",
 | 
			
		||||
        "name" => "Special chars: àáâãäåæçèéêë",
 | 
			
		||||
@@ -470,7 +470,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
 | 
			
		||||
    test "handles empty string values", %{conn: conn, map: map} do
 | 
			
		||||
      empty_params = %{
 | 
			
		||||
        "system_id" => Ecto.UUID.generate(),
 | 
			
		||||
        "solar_system_id" => 30_000_148,
 | 
			
		||||
        "eve_id" => "",
 | 
			
		||||
        "character_eve_id" => "",
 | 
			
		||||
        "name" => "",
 | 
			
		||||
@@ -537,7 +537,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
          if length(data) > 0 do
 | 
			
		||||
            signature = List.first(data)
 | 
			
		||||
            assert Map.has_key?(signature, "id")
 | 
			
		||||
            assert Map.has_key?(signature, "system_id")
 | 
			
		||||
            assert Map.has_key?(signature, "solar_system_id")
 | 
			
		||||
            assert Map.has_key?(signature, "eve_id")
 | 
			
		||||
            assert Map.has_key?(signature, "character_eve_id")
 | 
			
		||||
          end
 | 
			
		||||
@@ -564,7 +564,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
 | 
			
		||||
    test "created signature response structure", %{conn: conn, map: map} do
 | 
			
		||||
      signature_params = %{
 | 
			
		||||
        "system_id" => Ecto.UUID.generate(),
 | 
			
		||||
        "solar_system_id" => 30_000_149,
 | 
			
		||||
        "eve_id" => "TEST-001",
 | 
			
		||||
        "character_eve_id" => "123456789",
 | 
			
		||||
        "name" => "Test Signature"
 | 
			
		||||
@@ -582,7 +582,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIControllerTest do
 | 
			
		||||
      case response do
 | 
			
		||||
        %{"data" => data} ->
 | 
			
		||||
          # Should have signature structure
 | 
			
		||||
          assert Map.has_key?(data, "id") or Map.has_key?(data, "system_id")
 | 
			
		||||
          assert Map.has_key?(data, "id") or Map.has_key?(data, "solar_system_id")
 | 
			
		||||
 | 
			
		||||
        %{"error" => _error} ->
 | 
			
		||||
          # Error response is also valid
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
 | 
			
		||||
  describe "parameter validation" do
 | 
			
		||||
    test "validates missing connection assigns for create_signature" do
 | 
			
		||||
      conn = %{assigns: %{}}
 | 
			
		||||
      params = %{"solar_system_id" => "30000142"}
 | 
			
		||||
      params = %{"solar_system_id" => 30_000_142}
 | 
			
		||||
 | 
			
		||||
      result = Signatures.create_signature(conn, params)
 | 
			
		||||
      assert {:error, :missing_params} = result
 | 
			
		||||
@@ -209,7 +209,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      # Test with minimal required parameters
 | 
			
		||||
      params = %{"solar_system_id" => "30000142"}
 | 
			
		||||
      params = %{"solar_system_id" => 30_000_142}
 | 
			
		||||
 | 
			
		||||
      MapTestHelpers.expect_map_server_error(fn ->
 | 
			
		||||
        result = Signatures.create_signature(conn, params)
 | 
			
		||||
@@ -416,7 +416,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
 | 
			
		||||
        nil
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
      params = %{"solar_system_id" => "30000142"}
 | 
			
		||||
      params = %{"solar_system_id" => 30_000_142}
 | 
			
		||||
 | 
			
		||||
      Enum.each(malformed_conns, fn conn ->
 | 
			
		||||
        # This should either crash (expected) or return error
 | 
			
		||||
@@ -462,17 +462,16 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
 | 
			
		||||
      tasks =
 | 
			
		||||
        Enum.map(1..3, fn i ->
 | 
			
		||||
          Task.async(fn ->
 | 
			
		||||
            MapTestHelpers.expect_map_server_error(fn ->
 | 
			
		||||
              params = %{"solar_system_id" => "3000014#{i}"}
 | 
			
		||||
              Signatures.create_signature(conn, params)
 | 
			
		||||
            end)
 | 
			
		||||
            params = %{"solar_system_id" => 30_000_140 + i}
 | 
			
		||||
            result = Signatures.create_signature(conn, params)
 | 
			
		||||
            # We expect either system_not_found (system doesn't exist in test) 
 | 
			
		||||
            # or the MapTestHelpers would have caught the map server error
 | 
			
		||||
            assert {:error, :system_not_found} = result
 | 
			
		||||
          end)
 | 
			
		||||
        end)
 | 
			
		||||
 | 
			
		||||
      # All tasks should complete without crashing
 | 
			
		||||
      Enum.each(tasks, fn task ->
 | 
			
		||||
        assert Task.await(task) == :ok
 | 
			
		||||
      end)
 | 
			
		||||
      Enum.each(tasks, &Task.await/1)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
@@ -669,7 +668,7 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
 | 
			
		||||
        }
 | 
			
		||||
      ]
 | 
			
		||||
 | 
			
		||||
      params = %{"solar_system_id" => "30000142"}
 | 
			
		||||
      params = %{"solar_system_id" => 30_000_142}
 | 
			
		||||
 | 
			
		||||
      Enum.each(assign_variations, fn assigns ->
 | 
			
		||||
        conn = %{assigns: assigns}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user