Compare commits

..

62 Commits

Author SHA1 Message Date
CI
802e81b1cd chore: release version v1.65.7 2025-05-26 14:05:57 +00:00
Dmitry Popov
41f0834c51 chore: release version v1.65.6 2025-05-26 16:05:12 +02:00
Dmitry Popov
880de0b047 chore: release version v1.65.6 2025-05-26 15:57:34 +02:00
Dmitry Popov
bbe7fda4e0 fix(Core): Fixed map character tracking issues 2025-05-26 15:56:46 +02:00
CI
92a9274dce chore: release version v1.65.6 2025-05-26 10:19:27 +00:00
Dmitry Popov
8765d83083 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-26 12:09:13 +02:00
Dmitry Popov
a298152bc8 fix(Core): Fixed map character tracking issues 2025-05-26 12:09:09 +02:00
CI
2b7abe5774 chore: release version v1.65.5
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-05-26 00:11:32 +00:00
Dmitry Popov
3e9241892e fix(Core): Fixed map character tracking issues 2025-05-26 01:41:21 +02:00
Dmitry Popov
6ea79a7960 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-25 09:41:39 +02:00
Dmitry Popov
2af562e313 fix(Signature): Update restored signature character 2025-05-25 09:41:33 +02:00
CI
40672f6a47 chore: release version v1.65.4 2025-05-24 17:30:03 +00:00
Dmitry Popov
6d66ae3f50 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-24 19:18:57 +02:00
Dmitry Popov
94c89e0325 fix(Signature): Force signature update even if there are no any changes 2025-05-24 19:18:50 +02:00
CI
3670ef40a3 chore: release version v1.65.3 2025-05-23 18:29:03 +00:00
Dmitry Popov
16d464fba5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-23 20:06:20 +02:00
Dmitry Popov
0b7e0b9cd0 fix(Signature): Fixed signature clenup 2025-05-23 20:06:17 +02:00
CI
dd5fd114d2 chore: release version v1.65.2 2025-05-23 15:33:00 +00:00
Dmitry Popov
6e53879344 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-23 17:24:33 +02:00
Dmitry Popov
af2bfd4d59 fix(Signature): Fixed signature updates 2025-05-23 17:24:30 +02:00
CI
a4a34c8ba7 chore: release version v1.65.1
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-05-22 11:47:01 +00:00
Dmitry Popov
8c609f4fdf Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-22 11:52:23 +02:00
Dmitry Popov
197f5b583f fix(Core): Added unsync map events timeout handling (force page refresh if outdated map events found) 2025-05-22 11:52:20 +02:00
CI
4eb4a03e59 chore: release version v1.65.0 2025-05-22 09:45:26 +00:00
Dmitry Popov
3d4e66d438 Merge pull request #399 from guarzo/guarzo/moreopenapi
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
fix: remove required id field from character schema
2025-05-21 20:07:32 +04:00
Guarzo
ffbc9f169a fix: remove required id field from character schema 2025-05-21 11:59:03 -04:00
Dmitry Popov
99650187e9 Merge pull request #397 from guarzo/guarzo/c1fix
fix: correct issue with connection types between connected k-space systems
2025-05-21 17:47:21 +04:00
Guarzo
92699317cd fix: update openapi spec response types 2025-05-21 09:19:12 -04:00
Guarzo
0e48315803 fix: fix issue with connection generation between k-space 2025-05-21 09:19:12 -04:00
Dmitry Popov
868ec246bd Merge pull request #395 from wanderer-industries/develop
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
Develop
2025-05-21 15:39:01 +04:00
Dmitry Popov
0030a688c6 Merge branch 'develop' of github.com:wanderer-industries/wanderer into develop 2025-05-21 11:52:18 +02:00
Dmitry Popov
3ba8f51a2f fix(Signature): Fixed signatures updates 2025-05-21 11:52:15 +02:00
Dmitry Popov
04576b335c Merge pull request #392 from guarzo/guarzo/c1qol
feat: default c1 connections to medium
2025-05-21 13:12:04 +04:00
Dmitry Popov
ea29aa176f Merge branch 'main' into develop 2025-05-21 08:46:48 +02:00
CI
9a9b7289ba chore: release version v1.64.8
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-05-20 14:33:21 +00:00
Dmitry Popov
d601790864 fix(Core): Added unsync map events timeout handling (force page refresh if outdated map events found) 2025-05-20 16:08:43 +02:00
Dmitry Popov
bf58d3ae93 Merge branch 'main' into develop 2025-05-20 11:24:46 +02:00
Guarzo
d6c32e2d39 feat: default connections from c1 holes to medium size 2025-05-20 04:39:17 -04:00
Dmitry Popov
bdc4948afb Merge pull request #366 from guarzo/guarzo/undo
feat: improve signature undo process
2025-05-19 22:44:13 +04:00
Guarzo
331db10029 fix: update openapi spec for other apis 2025-05-19 11:35:15 -04:00
Dmitry Popov
2daf9e34d2 chore: release version v1.64.7
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-05-19 15:30:06 +02:00
CI
558cd9b8b3 chore: release version v1.64.7
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-05-15 21:40:47 +00:00
Dmitry Popov
a0f02d0d2f Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-15 23:15:10 +02:00
Dmitry Popov
9feb8492aa fix(Core): Fixed connection EOL time refreshed every 2 minutes 2025-05-15 23:15:07 +02:00
CI
e5aa726899 chore: release version v1.64.6
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-05-15 10:23:04 +00:00
Dmitry Popov
93d1c28ccd Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-15 12:02:02 +02:00
Dmitry Popov
b5ba9200bc fix(Core): Added map hubs limits checking & a proper warning message shown 2025-05-15 12:01:59 +02:00
CI
699d866670 chore: release version v1.64.5
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-05-14 22:13:32 +00:00
Dmitry Popov
c3071344cb chore: Added link to YT channel 2025-05-14 23:59:06 +02:00
Dmitry Popov
9e998dd2b6 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-14 23:24:57 +02:00
Dmitry Popov
c9accf6079 fix(Core): Added character name update on re-auth 2025-05-14 23:24:49 +02:00
CI
1b41a51004 chore: release version v1.64.4 2025-05-14 20:49:45 +00:00
guarzo
3338dce900 Merge branch 'develop' into guarzo/undo 2025-05-14 15:04:42 -04:00
Guarzo
1364779f81 feat: support german and french signatures 2025-05-14 10:38:18 -04:00
Dmitry Popov
b49d3423fc fix(Core): Added 1 min timeout for ship and location updates on ESI API errors
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / Manual Approval (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/arm64) (push) Has been cancelled
Build / merge (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-05-14 16:35:21 +02:00
CI
cccab2a985 chore: release version v1.64.3 2025-05-14 10:14:49 +00:00
Dmitry Popov
1abaa90a7d Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-05-14 11:54:41 +02:00
Dmitry Popov
6e1993ca8a fix(Core): Fixed character tracking initialization logic & removed search caching 2025-05-14 11:54:37 +02:00
guarzo
cb318aa6c6 Merge branch 'develop' into guarzo/undo 2025-05-13 12:35:11 -04:00
guarzo
1a27b21efe Merge branch 'develop' into guarzo/undo 2025-05-12 11:55:19 -04:00
guarzo
e57f565812 Merge branch 'develop' into guarzo/undo 2025-05-09 18:59:46 -04:00
Guarzo
da2605ee03 feat: improve signature undo process 2025-05-09 18:59:33 -04:00
66 changed files with 2879 additions and 966 deletions

View File

@@ -18,49 +18,8 @@ permissions:
contents: write
jobs:
deploy-test:
name: 🚀 Deploy to test env (fly.io)
runs-on: ubuntu-latest
if: ${{ github.base_ref == 'main' || (github.ref == 'refs/heads/main' && github.event_name == 'push') }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: 👀 Read app name
uses: SebRollen/toml-action@v1.0.0
id: app_name
with:
file: "fly.toml"
field: "app"
- name: 🚀 Deploy Test
run: flyctl deploy --remote-only --wait-timeout=300 --ha=false
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
manual-approval:
name: Manual Approval
runs-on: ubuntu-latest
needs: deploy-test
if: success()
permissions:
issues: write
steps:
- name: Await Manual Approval
uses: trstringer/manual-approval@v1
with:
secret: ${{ github.TOKEN }}
approvers: DmitryPopov
minimum-approvals: 1
issue-title: "Manual Approval Required for Release"
issue-body: "Please approve or deny the deployment."
build:
name: 🛠 Build
needs: manual-approval
runs-on: ubuntu-22.04
if: ${{ (github.ref == 'refs/heads/main') && github.event_name == 'push' }}
permissions:

View File

@@ -2,6 +2,150 @@
<!-- changelog -->
## [v1.65.7](https://github.com/wanderer-industries/wanderer/compare/v1.65.6...v1.65.7) (2025-05-26)
### Bug Fixes:
* Core: Fixed map character tracking issues
## [v1.65.6](https://github.com/wanderer-industries/wanderer/compare/v1.65.5...v1.65.6) (2025-05-26)
### Bug Fixes:
* Core: Fixed map character tracking issues
## [v1.65.5](https://github.com/wanderer-industries/wanderer/compare/v1.65.4...v1.65.5) (2025-05-26)
### Bug Fixes:
* Core: Fixed map character tracking issues
* Signature: Update restored signature character
## [v1.65.4](https://github.com/wanderer-industries/wanderer/compare/v1.65.3...v1.65.4) (2025-05-24)
### Bug Fixes:
* Signature: Force signature update even if there are no any changes
## [v1.65.3](https://github.com/wanderer-industries/wanderer/compare/v1.65.2...v1.65.3) (2025-05-23)
### Bug Fixes:
* Signature: Fixed signature clenup
## [v1.65.2](https://github.com/wanderer-industries/wanderer/compare/v1.65.1...v1.65.2) (2025-05-23)
### Bug Fixes:
* Signature: Fixed signature updates
## [v1.65.1](https://github.com/wanderer-industries/wanderer/compare/v1.65.0...v1.65.1) (2025-05-22)
### Bug Fixes:
* Core: Added unsync map events timeout handling (force page refresh if outdated map events found)
## [v1.65.0](https://github.com/wanderer-industries/wanderer/compare/v1.64.8...v1.65.0) (2025-05-22)
### Features:
* default connections from c1 holes to medium size
* support german and french signatures
* improve signature undo process
### Bug Fixes:
* remove required id field from character schema
* update openapi spec response types
* fix issue with connection generation between k-space
* Signature: Fixed signatures updates
* update openapi spec for other apis
## [v1.64.8](https://github.com/wanderer-industries/wanderer/compare/v1.64.7...v1.64.8) (2025-05-20)
### Bug Fixes:
* Core: Added unsync map events timeout handling (force page refresh if outdated map events found)
## [v1.64.7](https://github.com/wanderer-industries/wanderer/compare/v1.64.6...v1.64.7) (2025-05-15)
### Bug Fixes:
* Core: Fixed connection EOL time refreshed every 2 minutes
## [v1.64.6](https://github.com/wanderer-industries/wanderer/compare/v1.64.5...v1.64.6) (2025-05-15)
### Bug Fixes:
* Core: Added map hubs limits checking & a proper warning message shown
## [v1.64.5](https://github.com/wanderer-industries/wanderer/compare/v1.64.4...v1.64.5) (2025-05-14)
### Bug Fixes:
* Core: Added character name update on re-auth
## [v1.64.4](https://github.com/wanderer-industries/wanderer/compare/v1.64.3...v1.64.4) (2025-05-14)
### Bug Fixes:
* Core: Added 1 min timeout for ship and location updates on ESI API errors
## [v1.64.3](https://github.com/wanderer-industries/wanderer/compare/v1.64.2...v1.64.3) (2025-05-14)
### Bug Fixes:
* Core: Fixed character tracking initialization logic & removed search caching
## [v1.64.2](https://github.com/wanderer-industries/wanderer/compare/v1.64.1...v1.64.2) (2025-05-13)

View File

@@ -10,7 +10,7 @@ export interface SignatureViewProps {
export const SignatureView = ({ signature, showCharacterPortrait = false }: SignatureViewProps) => {
const isWormhole = signature?.group === SignatureGroup.Wormhole;
const hasCharacterInfo = showCharacterPortrait && signature.character_eve_id;
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : (signature?.group ?? SignatureGroup.CosmicSignature);
const groupDisplay = isWormhole ? SignatureGroup.Wormhole : signature?.group ?? SignatureGroup.CosmicSignature;
const characterName = signature.character_name || 'Unknown character';
return (

View File

@@ -19,7 +19,7 @@ export type HeaderProps = {
lazyDeleteValue: boolean;
onLazyDeleteChange: (checked: boolean) => void;
pendingCount: number;
pendingTimeRemaining?: number; // Time remaining in ms
undoCountdown?: number;
onUndoClick: () => void;
onSettingsClick: () => void;
};
@@ -29,7 +29,7 @@ export const SystemSignaturesHeader = ({
lazyDeleteValue,
onLazyDeleteChange,
pendingCount,
pendingTimeRemaining,
undoCountdown,
onUndoClick,
onSettingsClick,
}: HeaderProps) => {
@@ -43,13 +43,6 @@ export const SystemSignaturesHeader = ({
const containerRef = useRef<HTMLDivElement>(null);
const isCompact = useMaxWidth(containerRef, COMPACT_MAX_WIDTH);
// Format time remaining as seconds
const formatTimeRemaining = () => {
if (!pendingTimeRemaining) return '';
const seconds = Math.ceil(pendingTimeRemaining / 1000);
return ` (${seconds}s remaining)`;
};
return (
<div ref={containerRef} className="w-full">
<div className="flex justify-between items-center text-xs w-full h-full">
@@ -78,7 +71,9 @@ export const SystemSignaturesHeader = ({
<WdImgButton
className={PrimeIcons.UNDO}
style={{ color: 'red' }}
tooltip={{ content: `Undo pending changes (${pendingCount})${formatTimeRemaining()}` }}
tooltip={{
content: `Undo pending deletions (${pendingCount})${undoCountdown && undoCountdown > 0 ? `${undoCountdown}s left` : ''}`,
}}
onClick={onUndoClick}
/>
)}

View File

@@ -1,99 +1,156 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useState, useEffect, useRef, useMemo } from 'react';
import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
import { SystemSignaturesContent } from './SystemSignaturesContent';
import { SystemSignatureSettingsDialog } from './SystemSignatureSettingsDialog';
import { ExtendedSystemSignature, SystemSignature } from '@/hooks/Mapper/types';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useHotkey } from '@/hooks/Mapper/hooks';
import { SystemSignaturesHeader } from './SystemSignatureHeader';
import useLocalStorageState from 'use-local-storage-state';
import { useHotkey } from '@/hooks/Mapper/hooks/useHotkey';
import {
SETTINGS_KEYS,
SETTINGS_VALUES,
SIGNATURE_DELETION_TIMEOUTS,
SIGNATURE_SETTING_STORE_KEY,
SIGNATURE_WINDOW_ID,
SIGNATURES_DELETION_TIMING,
SignatureSettingsType,
SIGNATURES_DELETION_TIMING,
SIGNATURE_DELETION_TIMEOUTS,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { calculateTimeRemaining } from './helpers';
import { OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers';
export const SystemSignatures = () => {
const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState<number>(0);
const [pendingSigs, setPendingSigs] = useState<SystemSignature[]>([]);
const [pendingTimeRemaining, setPendingTimeRemaining] = useState<number | undefined>();
const undoPendingFnRef = useRef<() => void>(() => {});
/**
* Custom hook for managing pending signature deletions and undo countdown.
*/
function useSignatureUndo(
systemId: string | undefined,
settings: SignatureSettingsType,
outCommand: OutCommandHandler,
) {
const [countdown, setCountdown] = useState<number>(0);
const [pendingIds, setPendingIds] = useState<Set<string>>(new Set());
const intervalRef = useRef<number | null>(null);
const {
data: { selectedSystems },
} = useMapRootState();
const [currentSettings, setCurrentSettings] = useLocalStorageState(SIGNATURE_SETTING_STORE_KEY, {
defaultValue: SETTINGS_VALUES,
});
const handleSigCountChange = useCallback((count: number) => {
setSigCount(count);
const addDeleted = useCallback((ids: string[]) => {
setPendingIds(prev => {
const next = new Set(prev);
ids.forEach(id => next.add(id));
return next;
});
}, []);
const [systemId] = selectedSystems;
const isNotSelectedSystem = selectedSystems.length !== 1;
const handleSettingsChange = useCallback((newSettings: SignatureSettingsType) => {
setCurrentSettings(newSettings);
setVisible(false);
}, []);
const handleLazyDeleteChange = useCallback((value: boolean) => {
setCurrentSettings(prev => ({ ...prev, [SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value }));
}, []);
useHotkey(true, ['z'], event => {
if (pendingSigs.length > 0) {
event.preventDefault();
event.stopPropagation();
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}
});
const handleUndoClick = useCallback(() => {
undoPendingFnRef.current();
setPendingSigs([]);
setPendingTimeRemaining(undefined);
}, []);
const handleSettingsButtonClick = useCallback(() => {
setVisible(true);
}, []);
const handlePendingChange = useCallback(
(pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>, newUndo: () => void) => {
setPendingSigs(() => {
return Object.values(pending.current).filter(sig => sig.pendingDeletion);
});
undoPendingFnRef.current = newUndo;
},
[],
);
// Calculate the minimum time remaining for any pending signature
// kick off or clear countdown whenever pendingIds changes
useEffect(() => {
if (pendingSigs.length === 0) {
setPendingTimeRemaining(undefined);
// clear any existing timer
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
if (pendingIds.size === 0) {
setCountdown(0);
return;
}
const calculate = () => {
setPendingTimeRemaining(() => calculateTimeRemaining(pendingSigs));
};
// determine timeout from settings
const timingKey = Number(settings[SETTINGS_KEYS.DELETION_TIMING] ?? SIGNATURES_DELETION_TIMING.DEFAULT);
const timeoutMs =
Number(SIGNATURE_DELETION_TIMEOUTS[timingKey as keyof typeof SIGNATURE_DELETION_TIMEOUTS]) || 10000;
setCountdown(Math.ceil(timeoutMs / 1000));
calculate();
const interval = setInterval(calculate, 1000);
return () => clearInterval(interval);
}, [pendingSigs]);
// start new interval
intervalRef.current = window.setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(intervalRef.current!);
intervalRef.current = null;
setPendingIds(new Set());
return 0;
}
return prev - 1;
});
}, 1000);
return () => {
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
};
}, [pendingIds, settings[SETTINGS_KEYS.DELETION_TIMING]]);
// undo handler
const handleUndo = useCallback(async () => {
if (!systemId || pendingIds.size === 0) return;
await outCommand({
type: OutCommand.undoDeleteSignatures,
data: { system_id: systemId, eve_ids: Array.from(pendingIds) },
});
setPendingIds(new Set());
setCountdown(0);
if (intervalRef.current != null) {
clearInterval(intervalRef.current);
intervalRef.current = null;
}
}, [systemId, pendingIds, outCommand]);
return {
pendingIds,
countdown,
addDeleted,
handleUndo,
};
}
export const SystemSignatures = () => {
const [visible, setVisible] = useState(false);
const [sigCount, setSigCount] = useState(0);
const {
data: { selectedSystems },
outCommand,
} = useMapRootState();
const [currentSettings, setCurrentSettings] = useLocalStorageState<SignatureSettingsType>(
SIGNATURE_SETTING_STORE_KEY,
{
defaultValue: SETTINGS_VALUES,
},
);
const [systemId] = selectedSystems;
const isSystemSelected = useMemo(() => selectedSystems.length === 1, [selectedSystems.length]);
const { pendingIds, countdown, addDeleted, handleUndo } = useSignatureUndo(systemId, currentSettings, outCommand);
useHotkey(true, ['z', 'Z'], (event: KeyboardEvent) => {
if (pendingIds.size > 0 && countdown > 0) {
event.preventDefault();
event.stopPropagation();
handleUndo();
}
});
const handleCountChange = useCallback((count: number) => {
setSigCount(count);
}, []);
const handleSettingsSave = useCallback(
(newSettings: SignatureSettingsType) => {
setCurrentSettings(newSettings);
setVisible(false);
},
[setCurrentSettings],
);
const handleLazyDeleteToggle = useCallback(
(value: boolean) => {
setCurrentSettings(prev => ({
...prev,
[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES]: value,
}));
},
[setCurrentSettings],
);
const openSettings = useCallback(() => setVisible(true), []);
return (
<Widget
@@ -101,16 +158,16 @@ export const SystemSignatures = () => {
<SystemSignaturesHeader
sigCount={sigCount}
lazyDeleteValue={currentSettings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean}
pendingCount={pendingSigs.length}
pendingTimeRemaining={pendingTimeRemaining}
onLazyDeleteChange={handleLazyDeleteChange}
onUndoClick={handleUndoClick}
onSettingsClick={handleSettingsButtonClick}
pendingCount={pendingIds.size}
undoCountdown={countdown}
onLazyDeleteChange={handleLazyDeleteToggle}
onUndoClick={handleUndo}
onSettingsClick={openSettings}
/>
}
windowId={SIGNATURE_WINDOW_ID}
>
{isNotSelectedSystem ? (
{!isSystemSelected ? (
<div className="w-full h-full flex justify-center items-center select-none text-center text-stone-400/80 text-sm">
System is not selected
</div>
@@ -118,22 +175,17 @@ export const SystemSignatures = () => {
<SystemSignaturesContent
systemId={systemId}
settings={currentSettings}
deletionTiming={
SIGNATURE_DELETION_TIMEOUTS[
(currentSettings[SETTINGS_KEYS.DELETION_TIMING] as keyof typeof SIGNATURE_DELETION_TIMEOUTS) ||
SIGNATURES_DELETION_TIMING.DEFAULT
] as number
}
onLazyDeleteChange={handleLazyDeleteChange}
onCountChange={handleSigCountChange}
onPendingChange={handlePendingChange}
onLazyDeleteChange={handleLazyDeleteToggle}
onCountChange={handleCountChange}
onSignatureDeleted={addDeleted}
/>
)}
{visible && (
<SystemSignatureSettingsDialog
settings={currentSettings}
onCancel={() => setVisible(false)}
onSave={handleSettingsChange}
onSave={handleSettingsSave}
/>
)}
</Widget>

View File

@@ -57,12 +57,8 @@ interface SystemSignaturesContentProps {
onSelect?: (signature: SystemSignature) => void;
onLazyDeleteChange?: (value: boolean) => void;
onCountChange?: (count: number) => void;
onPendingChange?: (
pending: React.MutableRefObject<Record<string, ExtendedSystemSignature>>,
undo: () => void,
) => void;
deletionTiming?: number;
filterSignature?: (signature: SystemSignature) => boolean;
onSignatureDeleted?: (deletedIds: string[]) => void;
}
export const SystemSignaturesContent = ({
@@ -73,9 +69,8 @@ export const SystemSignaturesContent = ({
onSelect,
onLazyDeleteChange,
onCountChange,
onPendingChange,
deletionTiming,
filterSignature,
onSignatureDeleted,
}: SystemSignaturesContentProps) => {
const [selectedSignatureForDialog, setSelectedSignatureForDialog] = useState<SystemSignature | null>(null);
const [showSignatureSettings, setShowSignatureSettings] = useState(false);
@@ -95,15 +90,21 @@ export const SystemSignaturesContent = ({
{ defaultValue: SORT_DEFAULT_VALUES },
);
const { signatures, selectedSignatures, setSelectedSignatures, handleDeleteSelected, handleSelectAll, handlePaste } =
useSystemSignaturesData({
systemId,
settings,
onCountChange,
onPendingChange,
onLazyDeleteChange,
deletionTiming,
});
const {
signatures,
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
} = useSystemSignaturesData({
systemId,
settings,
onCountChange,
onLazyDeleteChange,
onSignatureDeleted,
});
useEffect(() => {
if (selectable) return;
@@ -125,6 +126,10 @@ export const SystemSignaturesContent = ({
event.preventDefault();
event.stopPropagation();
if (onSignatureDeleted && selectedSignatures.length > 0) {
const deletedIds = selectedSignatures.map(s => s.eve_id);
onSignatureDeleted(deletedIds);
}
handleDeleteSelected();
});
@@ -155,7 +160,7 @@ export const SystemSignaturesContent = ({
(e: { value: SystemSignature[] }) => {
selectable ? onSelect?.(e.value[0]) : setSelectedSignatures(e.value as ExtendedSystemSignature[]);
},
[selectable],
[onSelect, selectable, setSelectedSignatures],
);
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
@@ -188,7 +193,11 @@ export const SystemSignaturesContent = ({
x => GROUPS_LIST.includes(x as SignatureGroup) && settings[x as SETTINGS_KEYS],
);
return enabledGroups.includes(getGroupIdByRawGroup(sig.group));
const mappedGroup = getGroupIdByRawGroup(sig.group);
if (!mappedGroup) {
return true; // If we can't determine the group, still show it
}
return enabledGroups.includes(mappedGroup);
}
return true;
@@ -236,113 +245,121 @@ export const SystemSignaturesContent = ({
No signatures
</div>
) : (
<DataTable
value={filteredSignatures}
size="small"
selectionMode="multiple"
selection={selectedSignatures}
metaKeySelection
onSelectionChange={handleSelectSignatures}
dataKey="eve_id"
className="w-full select-none"
resizableColumns={false}
rowHover
selectAll
onRowDoubleClick={handleRowClick}
sortField={sortSettings.sortField}
sortOrder={sortSettings.sortOrder}
onSort={handleSortSettings}
onRowMouseEnter={onRowMouseEnter}
onRowMouseLeave={onRowMouseLeave}
// @ts-ignore
rowClassName={getRowClassName}
>
<Column
field="icon"
header=""
body={renderColIcon}
bodyClassName="p-0 px-1"
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
/>
<Column
field="eve_id"
header="Id"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
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
/>
<Column
field="info"
header="Info"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
/>
{showDescriptionColumn && (
<Column
field="description"
header="Description"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={isCompact}
body={renderDescription}
sortable
/>
<>
{hasUnsupportedLanguage && (
<div className="w-full flex justify-center items-center text-amber-500 text-xs p-1 bg-amber-950/20 border-b border-amber-800/30">
<i className={PrimeIcons.EXCLAMATION_TRIANGLE + ' mr-1'} />
Non-English signatures detected. Some signatures may not display correctly. Double-click to edit signature details.
</div>
)}
<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"
header="Updated"
dataType="date"
body={renderUpdatedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
)}
{showCharacterColumn && (
<Column
field="character_name"
header="Character"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
sortable
></Column>
)}
{!selectable && (
<DataTable
value={filteredSignatures}
size="small"
selectionMode="multiple"
selection={selectedSignatures}
metaKeySelection
onSelectionChange={handleSelectSignatures}
dataKey="eve_id"
className="w-full select-none"
resizableColumns={false}
rowHover
selectAll
onRowDoubleClick={handleRowClick}
sortField={sortSettings.sortField}
sortOrder={sortSettings.sortOrder}
onSort={handleSortSettings}
onRowMouseEnter={onRowMouseEnter}
onRowMouseLeave={onRowMouseLeave}
// @ts-ignore
rowClassName={getRowClassName}
>
<Column
field="icon"
header=""
body={() => (
<div className="flex justify-end items-center gap-2 mr-[4px]">
<WdTooltipWrapper content="Double-click a row to edit signature">
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
</WdTooltipWrapper>
</div>
)}
body={renderColIcon}
bodyClassName="p-0 px-1"
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
bodyClassName="p-0 pl-1 pr-2"
/>
)}
</DataTable>
<Column
field="eve_id"
header="Id"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
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
/>
<Column
field="info"
header="Info"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
style={{ maxWidth: nameColumnWidth }}
hidden={isCompact || isMedium}
body={renderInfoColumn}
/>
{showDescriptionColumn && (
<Column
field="description"
header="Description"
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
hidden={isCompact}
body={renderDescription}
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
/>
{showUpdatedColumn && (
<Column
field="updated_at"
header="Updated"
dataType="date"
body={renderUpdatedTimeLeft}
style={{ minWidth: 70, maxWidth: 80 }}
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
sortable
/>
)}
{showCharacterColumn && (
<Column
field="character_name"
header="Character"
bodyClassName="w-[70px] text-ellipsis overflow-hidden whitespace-nowrap"
sortable
></Column>
)}
{!selectable && (
<Column
header=""
body={() => (
<div className="flex justify-end items-center gap-2 mr-[4px]">
<WdTooltipWrapper content="Double-click a row to edit signature">
<span className={PrimeIcons.PENCIL + ' text-[10px]'} />
</WdTooltipWrapper>
</div>
)}
style={{ maxWidth: 26, minWidth: 26, width: 26 }}
bodyClassName="p-0 pl-1 pr-2"
/>
)}
</DataTable>
</>
)}
<WdTooltip

View File

@@ -1,10 +1,14 @@
import {
GroupType,
SignatureGroup,
SignatureGroupDE,
SignatureGroupENG,
SignatureGroupFR,
SignatureGroupRU,
SignatureKind,
SignatureKindDE,
SignatureKindENG,
SignatureKindFR,
SignatureKindRU,
} from '@/hooks/Mapper/types';
@@ -40,46 +44,58 @@ export const GROUPS: Record<SignatureGroup, GroupType> = {
[SignatureGroup.CosmicSignature]: { id: SignatureGroup.CosmicSignature, icon: '/icons/x_close14.png', w: 9, h: 9 },
};
export const MAPPING_GROUP_TO_ENG = {
// ENGLISH
[SignatureGroupENG.GasSite]: SignatureGroup.GasSite,
[SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupENG.DataSite]: SignatureGroup.DataSite,
[SignatureGroupENG.OreSite]: SignatureGroup.OreSite,
[SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature,
// RUSSIAN
[SignatureGroupRU.GasSite]: SignatureGroup.GasSite,
[SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupRU.DataSite]: SignatureGroup.DataSite,
[SignatureGroupRU.OreSite]: SignatureGroup.OreSite,
[SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature,
export const LANGUAGE_GROUP_MAPPINGS = {
EN: {
[SignatureGroupENG.GasSite]: SignatureGroup.GasSite,
[SignatureGroupENG.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupENG.DataSite]: SignatureGroup.DataSite,
[SignatureGroupENG.OreSite]: SignatureGroup.OreSite,
[SignatureGroupENG.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupENG.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupENG.CosmicSignature]: SignatureGroup.CosmicSignature,
},
RU: {
[SignatureGroupRU.GasSite]: SignatureGroup.GasSite,
[SignatureGroupRU.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupRU.DataSite]: SignatureGroup.DataSite,
[SignatureGroupRU.OreSite]: SignatureGroup.OreSite,
[SignatureGroupRU.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupRU.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupRU.CosmicSignature]: SignatureGroup.CosmicSignature,
},
FR: {
[SignatureGroupFR.GasSite]: SignatureGroup.GasSite,
[SignatureGroupFR.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupFR.DataSite]: SignatureGroup.DataSite,
[SignatureGroupFR.OreSite]: SignatureGroup.OreSite,
[SignatureGroupFR.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupFR.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupFR.CosmicSignature]: SignatureGroup.CosmicSignature,
},
DE: {
[SignatureGroupDE.GasSite]: SignatureGroup.GasSite,
[SignatureGroupDE.RelicSite]: SignatureGroup.RelicSite,
[SignatureGroupDE.DataSite]: SignatureGroup.DataSite,
[SignatureGroupDE.OreSite]: SignatureGroup.OreSite,
[SignatureGroupDE.CombatSite]: SignatureGroup.CombatSite,
[SignatureGroupDE.Wormhole]: SignatureGroup.Wormhole,
[SignatureGroupDE.CosmicSignature]: SignatureGroup.CosmicSignature,
},
};
export const MAPPING_TYPE_TO_ENG = {
// ENGLISH
[SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindENG.Structure]: SignatureKind.Structure,
[SignatureKindENG.Ship]: SignatureKind.Ship,
[SignatureKindENG.Deployable]: SignatureKind.Deployable,
[SignatureKindENG.Drone]: SignatureKind.Drone,
// Flatten the structure for backward compatibility
export const MAPPING_GROUP_TO_ENG: Record<string, SignatureGroup> = (() => {
const flattened: Record<string, SignatureGroup> = {};
for (const [, mappings] of Object.entries(LANGUAGE_GROUP_MAPPINGS)) {
Object.assign(flattened, mappings);
}
return flattened;
})();
// RUSSIAN
[SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindRU.Structure]: SignatureKind.Structure,
[SignatureKindRU.Ship]: SignatureKind.Ship,
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
[SignatureKindRU.Drone]: SignatureKind.Drone,
export const getGroupIdByRawGroup = (val: string): SignatureGroup | undefined => {
return MAPPING_GROUP_TO_ENG[val] || undefined;
};
export const getGroupIdByRawGroup = (val: string) => MAPPING_GROUP_TO_ENG[val as SignatureGroup];
export const SIGNATURE_WINDOW_ID = 'system_signatures_window';
export const SIGNATURE_SETTING_STORE_KEY = 'wanderer_system_signature_settings_v6_5';
@@ -123,7 +139,7 @@ export type Setting = {
name: string;
type: SettingsTypes;
isSeparator?: boolean;
options?: { label: string; value: any }[];
options?: { label: string; value: number | string | boolean }[];
};
export enum SIGNATURES_DELETION_TIMING {
@@ -208,3 +224,52 @@ export const SIGNATURE_DELETION_TIMEOUTS: SignatureDeletionTimingType = {
[SIGNATURES_DELETION_TIMING.IMMEDIATE]: 0,
[SIGNATURES_DELETION_TIMING.EXTENDED]: 30_000,
};
// Replace the flat structure with a nested structure by language
export const LANGUAGE_TYPE_MAPPINGS = {
EN: {
[SignatureKindENG.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindENG.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindENG.Structure]: SignatureKind.Structure,
[SignatureKindENG.Ship]: SignatureKind.Ship,
[SignatureKindENG.Deployable]: SignatureKind.Deployable,
[SignatureKindENG.Drone]: SignatureKind.Drone,
[SignatureKindENG.Starbase]: SignatureKind.Starbase,
},
RU: {
[SignatureKindRU.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindRU.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindRU.Structure]: SignatureKind.Structure,
[SignatureKindRU.Ship]: SignatureKind.Ship,
[SignatureKindRU.Deployable]: SignatureKind.Deployable,
[SignatureKindRU.Drone]: SignatureKind.Drone,
[SignatureKindRU.Starbase]: SignatureKind.Starbase,
},
FR: {
[SignatureKindFR.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindFR.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindFR.Structure]: SignatureKind.Structure,
[SignatureKindFR.Ship]: SignatureKind.Ship,
[SignatureKindFR.Deployable]: SignatureKind.Deployable,
[SignatureKindFR.Drone]: SignatureKind.Drone,
[SignatureKindFR.Starbase]: SignatureKind.Starbase,
},
DE: {
[SignatureKindDE.CosmicSignature]: SignatureKind.CosmicSignature,
[SignatureKindDE.CosmicAnomaly]: SignatureKind.CosmicAnomaly,
[SignatureKindDE.Structure]: SignatureKind.Structure,
[SignatureKindDE.Ship]: SignatureKind.Ship,
[SignatureKindDE.Deployable]: SignatureKind.Deployable,
[SignatureKindDE.Drone]: SignatureKind.Drone,
[SignatureKindDE.Starbase]: SignatureKind.Starbase,
},
};
// Flatten the structure for backward compatibility
export const MAPPING_TYPE_TO_ENG: Record<string, SignatureKind> = (() => {
const flattened: Record<string, SignatureKind> = {};
for (const [, mappings] of Object.entries(LANGUAGE_TYPE_MAPPINGS)) {
Object.assign(flattened, mappings);
}
return flattened;
})();

View File

@@ -1,5 +1,5 @@
import { SystemSignature } from '@/hooks/Mapper/types';
import { GROUPS_LIST } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants';
import { SystemSignature } from '@/hooks/Mapper/types';
import { getState } from './getState';
/**
@@ -22,6 +22,7 @@ export const getActualSigs = (
oldSignatures.forEach(oldSig => {
const newSig = newSignatures.find(s => s.eve_id === oldSig.eve_id);
if (newSig) {
const needUpgrade = getState(GROUPS_LIST, newSig) > getState(GROUPS_LIST, oldSig);
const mergedSig = { ...oldSig };

View File

@@ -1,13 +1,15 @@
import { SystemSignature } from '@/hooks/Mapper/types';
import { UNKNOWN_SIGNATURE_NAME } from '@/hooks/Mapper/helpers';
import { SignatureGroup, SystemSignature } from '@/hooks/Mapper/types';
export const getState = (_: string[], newSig: SystemSignature) => {
let state = -1;
if (!newSig.group) {
if (!newSig.group || newSig.group === SignatureGroup.CosmicSignature) {
state = 0;
} else if (!newSig.name || newSig.name === '') {
} else if (!newSig.name || newSig.name === '' || newSig.name === UNKNOWN_SIGNATURE_NAME) {
state = 1;
} else if (newSig.name !== '') {
state = 2;
}
return state;
};

View File

@@ -1,23 +1,18 @@
import { useCallback, useRef, useEffect } from 'react';
import { useCallback, useRef } from 'react';
import { OutCommand } from '@/hooks/Mapper/types/mapHandlers';
import { prepareUpdatePayload, scheduleLazyTimers } from '../helpers';
import { prepareUpdatePayload } from '../helpers';
import { UsePendingDeletionParams } from './types';
import { FINAL_DURATION_MS } from '../constants';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { ExtendedSystemSignature } from '@/hooks/Mapper/types';
export function usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
onPendingChange,
}: UsePendingDeletionParams) {
}: Omit<UsePendingDeletionParams, 'deletionTiming'>) {
const { outCommand } = useMapRootState();
const pendingDeletionMapRef = useRef<Record<string, ExtendedSystemSignature>>({});
// Use the provided deletion timing or fall back to the default
const finalDuration = deletionTiming !== undefined ? deletionTiming : FINAL_DURATION_MS;
const processRemovedSignatures = useCallback(
async (
removed: ExtendedSystemSignature[],
@@ -25,63 +20,15 @@ export function usePendingDeletions({
updated: ExtendedSystemSignature[],
) => {
if (!removed.length) return;
// If deletion timing is 0, immediately delete without pending state
if (finalDuration === 0) {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
return;
}
const now = Date.now();
const processedRemoved = removed.map(r => ({
...r,
pendingDeletion: true,
pendingUntil: now + finalDuration,
}));
pendingDeletionMapRef.current = {
...pendingDeletionMapRef.current,
...processedRemoved.reduce((acc: any, sig) => {
acc[sig.eve_id] = sig;
return acc;
}, {}),
};
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
setSignatures(prev =>
prev.map(sig => {
if (processedRemoved.find(r => r.eve_id === sig.eve_id)) {
return { ...sig, pendingDeletion: true, pendingUntil: now + finalDuration };
}
return sig;
}),
);
scheduleLazyTimers(
processedRemoved,
pendingDeletionMapRef,
async sig => {
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, [], [], [sig]),
});
delete pendingDeletionMapRef.current[sig.eve_id];
setSignatures(prev => prev.filter(x => x.eve_id !== sig.eve_id));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);
},
finalDuration,
);
await outCommand({
type: OutCommand.updateSignatures,
data: prepareUpdatePayload(systemId, added, updated, removed),
});
},
[systemId, outCommand, finalDuration],
[systemId, outCommand],
);
const clearPendingDeletions = useCallback(() => {
Object.values(pendingDeletionMapRef.current).forEach(({ finalTimeoutId }) => {
clearTimeout(finalTimeoutId);
});
pendingDeletionMapRef.current = {};
setSignatures(prev => prev.map(x => (x.pendingDeletion ? { ...x, pendingDeletion: false } : x)));
onPendingChange?.(pendingDeletionMapRef, clearPendingDeletions);

View File

@@ -18,16 +18,18 @@ export const useSystemSignaturesData = ({
onCountChange,
onPendingChange,
onLazyDeleteChange,
deletionTiming,
}: UseSystemSignaturesDataProps) => {
onSignatureDeleted,
}: Omit<UseSystemSignaturesDataProps, 'deletionTiming'> & {
onSignatureDeleted?: (deletedIds: string[]) => void;
}) => {
const { outCommand } = useMapRootState();
const [signatures, setSignatures, signaturesRef] = useRefState<ExtendedSystemSignature[]>([]);
const [selectedSignatures, setSelectedSignatures] = useState<ExtendedSystemSignature[]>([]);
const [hasUnsupportedLanguage, setHasUnsupportedLanguage] = useState<boolean>(false);
const { pendingDeletionMapRef, processRemovedSignatures, clearPendingDeletions } = usePendingDeletions({
systemId,
setSignatures,
deletionTiming,
onPendingChange,
});
@@ -42,6 +44,7 @@ export const useSystemSignaturesData = ({
async (clipboardString: string) => {
const lazyDeleteValue = settings[SETTINGS_KEYS.LAZY_DELETE_SIGNATURES] as boolean;
// Parse the incoming signatures
const incomingSignatures = parseSignatures(
clipboardString,
Object.keys(settings).filter(skey => skey in SignatureKind),
@@ -51,14 +54,30 @@ export const useSystemSignaturesData = ({
return;
}
// Check if any signatures might be using unsupported languages
// This is a basic heuristic: if we have signatures where the original group wasn't mapped
const clipboardRows = clipboardString.split('\n').filter(row => row.trim() !== '');
const detectedSignatureCount = clipboardRows.filter(row => row.match(/^[A-Z]{3}-\d{3}/)).length;
// If we detected valid IDs but got fewer parsed signatures, we might have language issues
if (detectedSignatureCount > 0 && incomingSignatures.length < detectedSignatureCount) {
setHasUnsupportedLanguage(true);
} else {
setHasUnsupportedLanguage(false);
}
const currentNonPending = lazyDeleteValue
? signaturesRef.current.filter(sig => !sig.pendingDeletion)
: signaturesRef.current.filter(sig => !sig.pendingDeletion || !sig.pendingAddition);
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, true);
const { added, updated, removed } = getActualSigs(currentNonPending, incomingSignatures, !lazyDeleteValue, false);
if (removed.length > 0) {
await processRemovedSignatures(removed, added, updated);
if (onSignatureDeleted) {
const deletedIds = removed.map(sig => sig.eve_id);
onSignatureDeleted(deletedIds);
}
}
if (updated.length !== 0 || added.length !== 0) {
@@ -78,17 +97,16 @@ export const useSystemSignaturesData = ({
onLazyDeleteChange?.(false);
}
},
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange],
[settings, signaturesRef, processRemovedSignatures, outCommand, systemId, onLazyDeleteChange, onSignatureDeleted],
);
const handleDeleteSelected = useCallback(async () => {
if (!selectedSignatures.length) return;
const selectedIds = selectedSignatures.map(s => s.eve_id);
const finalList = signatures.filter(s => !selectedIds.includes(s.eve_id));
await handleUpdateSignatures(finalList, false, true);
setSelectedSignatures([]);
}, [selectedSignatures, signatures]);
}, [handleUpdateSignatures, selectedSignatures, signatures]);
const handleSelectAll = useCallback(() => {
setSelectedSignatures(signatures);
@@ -119,11 +137,12 @@ export const useSystemSignaturesData = ({
}, [signatures]);
return {
signatures,
signatures: signatures.filter(sig => !sig.deleted),
selectedSignatures,
setSelectedSignatures,
handleDeleteSelected,
handleSelectAll,
handlePaste,
hasUnsupportedLanguage,
};
};

View File

@@ -1,5 +1,10 @@
import {
MAPPING_GROUP_TO_ENG,
MAPPING_TYPE_TO_ENG,
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
import { SignatureGroup, SignatureKind, SystemSignature } from '@/hooks/Mapper/types';
import { MAPPING_TYPE_TO_ENG } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/constants.ts';
export const UNKNOWN_SIGNATURE_NAME = 'Unknown';
export const parseSignatures = (value: string, availableKeys: string[]): SystemSignature[] => {
const outArr: SystemSignature[] = [];
@@ -14,13 +19,39 @@ export const parseSignatures = (value: string, availableKeys: string[]): SystemS
continue;
}
const kind = MAPPING_TYPE_TO_ENG[sigArrInfo[1] as SignatureKind];
// Extract the signature ID and check if it's valid (XXX-XXX format)
const sigId = sigArrInfo[0];
if (!sigId || !sigId.match(/^[A-Z]{3}-\d{3}$/)) {
continue;
}
// Try to map the kind, or fall back to CosmicSignature if unknown
const typeString = sigArrInfo[1];
let kind = SignatureKind.CosmicSignature;
// Try to map the kind using the flattened mapping
const mappedKind = MAPPING_TYPE_TO_ENG[typeString];
if (mappedKind && availableKeys.includes(mappedKind)) {
kind = mappedKind;
}
// Try to map the group, or fall back to CosmicSignature if unknown
const rawGroup = sigArrInfo[2];
let group = SignatureGroup.CosmicSignature;
// Try to map the group using the flattened mapping
const mappedGroup = MAPPING_GROUP_TO_ENG[rawGroup];
if (mappedGroup) {
group = mappedGroup;
}
const signature: SystemSignature = {
eve_id: sigArrInfo[0],
kind: availableKeys.includes(kind) ? kind : SignatureKind.CosmicSignature,
group: sigArrInfo[2] as SignatureGroup,
name: sigArrInfo[3],
eve_id: sigId,
kind,
group,
name: sigArrInfo[3] || UNKNOWN_SIGNATURE_NAME,
type: '',
};

View File

@@ -242,15 +242,12 @@ export enum OutCommand {
addSystemComment = 'addSystemComment',
deleteSystemComment = 'deleteSystemComment',
getSystemComments = 'getSystemComments',
// toggleTrack = 'toggle_track',
toggleFollow = 'toggle_follow',
getCharacterInfo = 'getCharacterInfo',
getCharactersTrackingInfo = 'getCharactersTrackingInfo',
updateCharacterTracking = 'updateCharacterTracking',
updateFollowingCharacter = 'updateFollowingCharacter',
updateMainCharacter = 'updateMainCharacter',
// Only UI commands
openSettings = 'open_settings',
showActivity = 'show_activity',
showTracking = 'show_tracking',
@@ -258,7 +255,7 @@ export enum OutCommand {
updateUserSettings = 'update_user_settings',
unlinkSignature = 'unlink_signature',
searchSystems = 'search_systems',
undoDeleteSignatures = 'undo_delete_signatures',
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type OutCommandHandler = <T = any>(event: { type: OutCommand; data: any }) => Promise<T>;
export type OutCommandHandler = <T = unknown>(event: { type: OutCommand; data: unknown }) => Promise<T>;

View File

@@ -30,6 +30,7 @@ export type GroupType = {
export type SignatureCustomInfo = {
k162Type?: string;
isEOL?: boolean;
isCrit?: boolean;
};
export type SystemSignature = {
@@ -46,6 +47,7 @@ export type SystemSignature = {
linked_system?: SolarSystemStaticInfoRaw;
inserted_at?: string;
updated_at?: string;
deleted?: boolean;
};
export interface ExtendedSystemSignature extends SystemSignature {
@@ -53,6 +55,7 @@ export interface ExtendedSystemSignature extends SystemSignature {
pendingAddition?: boolean;
pendingUntil?: number;
finalTimeoutId?: number;
deleted?: boolean;
}
export enum SignatureKindENG {
@@ -75,6 +78,26 @@ export enum SignatureKindRU {
Starbase = 'Starbase',
}
export enum SignatureKindFR {
CosmicSignature = 'Signature cosmique (type)',
CosmicAnomaly = 'Anomalie cosmique',
Structure = 'Structure',
Ship = 'Vaisseau',
Deployable = 'Déployable',
Drone = 'Drone',
Starbase = 'Base stellaire',
}
export enum SignatureKindDE {
CosmicSignature = 'Kosmische Signatur (typ)',
CosmicAnomaly = 'Kosmische Anomalie',
Structure = 'Struktur',
Ship = 'Schiff',
Deployable = 'Mobile Struktur',
Drone = 'Drohne',
Starbase = 'Sternenbasis',
}
export enum SignatureGroupENG {
CosmicSignature = 'Cosmic Signature',
Wormhole = 'Wormhole',
@@ -94,3 +117,23 @@ export enum SignatureGroupRU {
OreSite = 'Астероидный район',
CombatSite = 'Боевой район',
}
export enum SignatureGroupFR {
CosmicSignature = 'Signature cosmique (groupe)',
Wormhole = 'Trou de ver',
GasSite = 'Site de gaz',
RelicSite = 'Site de reliques',
DataSite = 'Site de données',
OreSite = 'Site de minerai',
CombatSite = 'Site de combat',
}
export enum SignatureGroupDE {
CosmicSignature = 'Kosmische Signatur (gruppe)',
Wormhole = 'Wurmloch',
GasSite = 'Gasgebiet',
RelicSite = 'Reliktgebiet',
DataSite = 'Datengebiet',
OreSite = 'Mineraliengebiet',
CombatSite = 'Kampfgebiet',
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -85,7 +85,7 @@ defmodule WandererApp.Api.Character do
update :update do
require_atomic? false
accept([:access_token, :refresh_token, :expires_at, :scopes])
accept([:name, :access_token, :refresh_token, :expires_at, :scopes])
change(set_attribute(:deleted, false))
end

View File

@@ -3,17 +3,19 @@ defmodule WandererApp.Api.MapCharacterSettings do
use Ash.Resource,
domain: WandererApp.Api,
data_layer: AshPostgres.DataLayer
data_layer: AshPostgres.DataLayer,
extensions: [AshCloak]
@derive {Jason.Encoder, only: [
:id,
:map_id,
:character_id,
:tracked,
:followed,
:inserted_at,
:updated_at
]}
@derive {Jason.Encoder,
only: [
:id,
:map_id,
:character_id,
:tracked,
:followed,
:inserted_at,
:updated_at
]}
postgres do
repo(WandererApp.Repo)
@@ -23,8 +25,10 @@ defmodule WandererApp.Api.MapCharacterSettings do
code_interface do
define(:create, action: :create)
define(:destroy, action: :destroy)
define(:update, action: :update)
define(:read_by_map, action: :read_by_map)
define(:read_by_map_and_character, action: :read_by_map_and_character)
define(:by_map_filtered, action: :by_map_filtered)
define(:tracked_by_map_filtered, action: :tracked_by_map_filtered)
define(:tracked_by_character, action: :tracked_by_character)
@@ -44,7 +48,31 @@ defmodule WandererApp.Api.MapCharacterSettings do
:tracked
]
defaults [:create, :read, :update, :destroy]
defaults [:read, :destroy]
create :create do
primary? true
upsert? true
upsert_identity :uniq_map_character
upsert_fields [
:map_id,
:character_id
]
accept [
:map_id,
:character_id,
:tracked,
:followed
]
argument :map_id, :uuid, allow_nil?: false
argument :character_id, :uuid, allow_nil?: false
change manage_relationship(:map_id, :map, on_lookup: :relate, on_no_match: nil)
change manage_relationship(:character_id, :character, on_lookup: :relate, on_no_match: nil)
end
read :by_map_filtered do
argument(:map_id, :string, allow_nil?: false)
@@ -67,6 +95,15 @@ defmodule WandererApp.Api.MapCharacterSettings do
filter(expr(map_id == ^arg(:map_id)))
end
read :read_by_map_and_character do
get? true
argument(:map_id, :string, allow_nil?: false)
argument(:character_id, :uuid, allow_nil?: false)
filter(expr(map_id == ^arg(:map_id) and character_id == ^arg(:character_id)))
end
read :tracked_by_map_all do
argument(:map_id, :string, allow_nil?: false)
filter(expr(map_id == ^arg(:map_id) and tracked == true))
@@ -77,6 +114,20 @@ defmodule WandererApp.Api.MapCharacterSettings do
filter(expr(character_id == ^arg(:character_id) and tracked == true))
end
update :update do
primary? true
require_atomic? false
accept([
:ship,
:ship_name,
:ship_item_id,
:solar_system_id,
:structure_id,
:station_id
])
end
update :track do
accept [:map_id, :character_id]
argument :map_id, :string, allow_nil?: false
@@ -134,6 +185,28 @@ defmodule WandererApp.Api.MapCharacterSettings do
end
end
cloak do
vault(WandererApp.Vault)
attributes([
:ship,
:ship_name,
:ship_item_id,
:solar_system_id,
:structure_id,
:station_id
])
decrypt_by_default([
:ship,
:ship_name,
:ship_item_id,
:solar_system_id,
:structure_id,
:station_id
])
end
attributes do
uuid_primary_key :id
@@ -147,6 +220,13 @@ defmodule WandererApp.Api.MapCharacterSettings do
allow_nil? true
end
attribute :solar_system_id, :integer
attribute :structure_id, :integer
attribute :station_id, :integer
attribute :ship, :integer
attribute :ship_name, :string
attribute :ship_item_id, :integer
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
end

View File

@@ -38,7 +38,8 @@ defmodule WandererApp.Api.MapConnection do
:map_id,
:solar_system_source,
:solar_system_target,
:type
:type,
:ship_size_type
]
defaults [:create, :read, :update, :destroy]

View File

@@ -24,7 +24,19 @@ defmodule WandererApp.Api.MapSystemSignature do
)
define(:by_system_id, action: :by_system_id, args: [:system_id])
define(:by_system_id_all, action: :by_system_id_all, args: [:system_id])
define(:by_system_id_and_eve_ids,
action: :by_system_id_and_eve_ids,
args: [:system_id, :eve_ids]
)
define(:by_linked_system_id, action: :by_linked_system_id, args: [:linked_system_id])
define(:by_deleted_and_updated_before!,
action: :by_deleted_and_updated_before,
args: [:deleted, :updated_before]
)
end
actions do
@@ -36,7 +48,8 @@ defmodule WandererApp.Api.MapSystemSignature do
:description,
:kind,
:group,
:type
:type,
:deleted
]
defaults [:read, :destroy]
@@ -64,7 +77,8 @@ defmodule WandererApp.Api.MapSystemSignature do
:kind,
:group,
:type,
:custom_info
:custom_info,
:deleted
]
argument :system_id, :uuid, allow_nil?: false
@@ -83,7 +97,8 @@ defmodule WandererApp.Api.MapSystemSignature do
:group,
:type,
:custom_info,
:updated
:deleted,
:update_forced_at
]
primary? true
@@ -105,14 +120,32 @@ defmodule WandererApp.Api.MapSystemSignature do
read :by_system_id do
argument(:system_id, :string, allow_nil?: false)
filter(expr(system_id == ^arg(:system_id) and deleted == false))
end
read :by_system_id_all do
argument(:system_id, :string, allow_nil?: false)
filter(expr(system_id == ^arg(:system_id)))
end
read :by_system_id_and_eve_ids do
argument(:system_id, :string, allow_nil?: false)
argument(:eve_ids, {:array, :string}, allow_nil?: false)
filter(expr(system_id == ^arg(:system_id) and eve_id in ^arg(:eve_ids)))
end
read :by_linked_system_id do
argument(:linked_system_id, :integer, allow_nil?: false)
filter(expr(linked_system_id == ^arg(:linked_system_id)))
end
read :by_deleted_and_updated_before do
argument(:deleted, :boolean, allow_nil?: false)
argument(:updated_before, :utc_datetime, allow_nil?: false)
filter(expr(deleted == ^arg(:deleted) and updated_at < ^arg(:updated_before)))
end
end
attributes do
@@ -149,7 +182,14 @@ defmodule WandererApp.Api.MapSystemSignature do
allow_nil? true
end
attribute :updated, :integer
attribute :deleted, :boolean do
allow_nil? false
default false
end
attribute :update_forced_at, :utc_datetime do
allow_nil? true
end
create_timestamp(:inserted_at)
update_timestamp(:updated_at)
@@ -166,21 +206,20 @@ defmodule WandererApp.Api.MapSystemSignature do
end
@derive {Jason.Encoder,
only: [
:id,
:system_id,
:eve_id,
:character_eve_id,
:name,
:description,
:type,
:linked_system_id,
:kind,
:group,
:custom_info,
:updated,
:inserted_at,
:updated_at
]
}
only: [
:id,
:system_id,
:eve_id,
:character_eve_id,
:name,
:description,
:type,
:linked_system_id,
:kind,
:group,
:custom_info,
:deleted,
:inserted_at,
:updated_at
]}
end

View File

@@ -39,40 +39,49 @@ defmodule WandererApp.CachedInfo do
def get_system_static_info(solar_system_id) do
case Cachex.get(:system_static_info_cache, solar_system_id) do
{:ok, nil} ->
{:ok, systems} = WandererApp.Api.MapSolarSystem.read()
case WandererApp.Api.MapSolarSystem.read() do
{:ok, systems} ->
systems
|> Enum.each(fn system ->
Cachex.put(
:system_static_info_cache,
system.solar_system_id,
Map.take(system, [
:solar_system_id,
:region_id,
:constellation_id,
:solar_system_name,
:solar_system_name_lc,
:constellation_name,
:region_name,
:system_class,
:security,
:type_description,
:class_title,
:is_shattered,
:effect_name,
:effect_power,
:statics,
:wandering,
:triglavian_invasion_status,
:sun_type_id
])
)
end)
systems
|> Enum.each(fn system ->
Cachex.put(
:system_static_info_cache,
system.solar_system_id,
Map.take(system, [
:solar_system_id,
:region_id,
:constellation_id,
:solar_system_name,
:solar_system_name_lc,
:constellation_name,
:region_name,
:system_class,
:security,
:type_description,
:class_title,
:is_shattered,
:effect_name,
:effect_power,
:statics,
:wandering,
:triglavian_invasion_status,
:sun_type_id
])
)
end)
Cachex.get(:system_static_info_cache, solar_system_id)
Cachex.get(:system_static_info_cache, solar_system_id)
{:error, reason} ->
Logger.error("Failed to read solar systems from API: #{inspect(reason)}")
{:error, :api_error}
end
{:ok, system_static_info} ->
{:ok, system_static_info}
{:error, reason} ->
Logger.error("Failed to get system static info from cache: #{inspect(reason)}")
{:error, :cache_error}
end
end

View File

@@ -7,6 +7,15 @@ defmodule WandererApp.Character do
@read_character_wallet_scope "esi-wallet.read_character_wallet.v1"
@read_corp_wallet_scope "esi-wallet.read_corporation_wallets.v1"
@default_character_tracking_data %{
solar_system_id: nil,
structure_id: nil,
station_id: nil,
ship: nil,
ship_name: nil,
ship_item_id: nil
}
@decorate cacheable(
cache: WandererApp.Cache,
key: "characters-#{character_eve_id}"
@@ -45,6 +54,32 @@ defmodule WandererApp.Character do
end
end
def get_map_character(map_id, character_id) do
case get_character(character_id) do
{:ok, character} ->
{:ok,
character
|> maybe_merge_map_character_settings(
map_id,
WandererApp.Character.TrackerManager.Impl.character_is_present(map_id, character_id)
)}
_ ->
{:ok, nil}
end
end
def get_map_character!(map_id, character_id) do
case get_map_character(map_id, character_id) do
{:ok, character} ->
character
_ ->
Logger.error("Failed to get map character #{map_id} #{character_id}")
nil
end
end
def get_character_eve_ids!(character_ids),
do:
character_ids
@@ -146,7 +181,7 @@ defmodule WandererApp.Character do
params: opts[:params]
) do
{:ok, result} ->
{:ok, result |> _prepare_search_results()}
{:ok, result |> prepare_search_results()}
{:error, error} ->
Logger.warning("#{__MODULE__} failed search: #{inspect(error)}")
@@ -208,7 +243,28 @@ defmodule WandererApp.Character do
end
end
defp _prepare_search_results(result) do
defp maybe_merge_map_character_settings(character, map_id, true), do: character
defp maybe_merge_map_character_settings(
%{id: character_id} = character,
map_id,
_character_is_present
) do
WandererApp.MapCharacterSettingsRepo.get(map_id, character_id)
|> case do
{:ok, settings} when not is_nil(settings) ->
character
|> Map.put(:online, false)
|> Map.merge(settings)
_ ->
character
|> Map.put(:online, false)
|> Map.merge(@default_character_tracking_data)
end
end
defp prepare_search_results(result) do
{:ok, characters} =
_load_eve_info(Map.get(result, "character"), :get_character_info, &_map_character_info/1)

View File

@@ -139,6 +139,13 @@ defmodule WandererApp.Character.Tracker do
{:error, error} ->
Logger.error("#{__MODULE__} failed to update_ship: #{inspect(error)}")
WandererApp.Cache.put(
"character:#{character_id}:ship_forbidden",
true,
ttl: @forbidden_ttl
)
{:error, error}
end
end
@@ -191,6 +198,13 @@ defmodule WandererApp.Character.Tracker do
{:error, error} ->
Logger.error("#{__MODULE__} failed to update_location: #{inspect(error)}")
WandererApp.Cache.put(
"character:#{character_id}:location_forbidden",
true,
ttl: @forbidden_ttl
)
{:error, error}
end
@@ -305,16 +319,12 @@ defmodule WandererApp.Character.Tracker do
duration = DateTime.diff(DateTime.utc_now(), error_time, :second)
if duration >= @online_error_timeout do
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
WandererApp.Character.update_character(character_id, %{online: false})
# WandererApp.Cache.delete("character:#{character_id}:location_started")
# WandererApp.Cache.delete("character:#{character_id}:start_solar_system_id")
WandererApp.Character.update_character_state(character_id, %{
character_state
| is_online: false
is_online: false
})
:ok

View File

@@ -13,7 +13,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
}
@garbage_collection_interval :timer.minutes(15)
@untrack_characters_interval :timer.minutes(5)
@untrack_characters_interval :timer.minutes(1)
@inactive_character_timeout :timer.minutes(5)
@logger Application.compile_env(:wanderer_app, :logger)
@@ -34,6 +34,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
def start(state) do
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
WandererApp.Cache.insert("tracked_characters", [])
tracked_characters
|> Enum.each(fn character_id ->
@@ -51,6 +52,12 @@ defmodule WandererApp.Character.TrackerManager.Impl do
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, [
@@ -213,6 +220,18 @@ defmodule WandererApp.Character.TrackerManager.Impl do
track: false
})
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, _updated} =
WandererApp.MapCharacterSettingsRepo.update(map_id, character_id, %{
ship: character.ship,
ship_name: character.ship_name,
ship_item_id: character.ship_item_id,
solar_system_id: character.solar_system_id,
structure_id: character.structure_id,
station_id: character.station_id
})
WandererApp.Character.update_character_state(character_id, character_state)
end
end,
@@ -239,7 +258,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
def handle_info(_event, state),
do: state
defp character_is_present(map_id, character_id) do
def character_is_present(map_id, character_id) do
{:ok, presence_character_ids} =
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])

View File

@@ -177,6 +177,10 @@ defmodule WandererApp.Character.TrackerPool do
characters
|> Enum.each(fn character_id ->
WandererApp.Character.update_character(character_id, %{online: false})
WandererApp.Character.update_character_state(character_id, %{
is_online: false
})
end)
{:noreply, state}

View File

@@ -121,7 +121,6 @@ defmodule WandererApp.Character.TrackingUtils do
WandererApp.MapCharacterSettingsRepo.untrack(existing_settings)
:ok = untrack([character], map_id, caller_pid)
:ok = remove_characters([character], map_id)
{:ok, updated_settings}
else
{:ok, existing_settings}
@@ -132,7 +131,6 @@ defmodule WandererApp.Character.TrackingUtils do
if track do
{:ok, updated_settings} = WandererApp.MapCharacterSettingsRepo.track(existing_settings)
:ok = track([character], map_id, true, caller_pid)
:ok = add_characters([character], map_id, true)
{:ok, updated_settings}
else
{:ok, existing_settings}
@@ -149,7 +147,6 @@ defmodule WandererApp.Character.TrackingUtils do
})
:ok = track([character], map_id, true, caller_pid)
:ok = add_characters([character], map_id, true)
{:ok, settings}
else
{:error, "Character settings not found"}
@@ -231,15 +228,15 @@ defmodule WandererApp.Character.TrackingUtils do
with false <- is_nil(caller_pid) do
character_ids = characters |> Enum.map(& &1.id)
characters
|> Enum.each(fn character ->
WandererAppWeb.Presence.update(caller_pid, map_id, character.id, %{
character_ids
|> Enum.each(fn character_id ->
WandererAppWeb.Presence.update(caller_pid, map_id, character_id, %{
tracked: false,
from: DateTime.utc_now()
})
end)
WandererApp.Map.Server.untrack_characters(map_id, character_ids)
# WandererApp.Map.Server.untrack_characters(map_id, character_ids)
:ok
else
@@ -249,19 +246,19 @@ defmodule WandererApp.Character.TrackingUtils do
end
end
def add_characters([], _map_id, _track_character), do: :ok
# def add_characters([], _map_id, _track_character), do: :ok
def add_characters([character | characters], map_id, track_character) do
:ok = WandererApp.Map.Server.add_character(map_id, character, track_character)
add_characters(characters, map_id, track_character)
end
# def add_characters([character | characters], map_id, track_character) do
# :ok = WandererApp.Map.Server.add_character(map_id, character, track_character)
# add_characters(characters, map_id, track_character)
# end
def remove_characters([], _map_id), do: :ok
# def remove_characters([], _map_id), do: :ok
def remove_characters([character | characters], map_id) do
:ok = WandererApp.Map.Server.remove_character(map_id, character.id)
remove_characters(characters, map_id)
end
# def remove_characters([character | characters], map_id) do
# :ok = WandererApp.Map.Server.remove_character(map_id, character.id)
# remove_characters(characters, map_id)
# end
def get_main_character(
nil,

View File

@@ -102,7 +102,7 @@ defmodule WandererApp.Esi.ApiClient do
{:ok, []}
end
chains = _remove_intersection([map_chains | thera_chains] |> List.flatten())
chains = remove_intersection([map_chains | thera_chains] |> List.flatten())
chains =
case routes_settings.include_cruise do
@@ -302,7 +302,7 @@ defmodule WandererApp.Esi.ApiClient do
opts: [ttl: @ttl]
)
def get_killmail(killmail_id, killmail_hash, opts \\ []) do
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts)
get("/killmails/#{killmail_id}/#{killmail_hash}/", opts, @cache_opts)
end
@decorate cacheable(
@@ -325,7 +325,8 @@ defmodule WandererApp.Esi.ApiClient do
def get_character_info(eve_id, opts \\ []) do
case get(
"/characters/#{eve_id}/",
opts
opts,
@cache_opts
) do
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
{:error, error} -> {:error, error}
@@ -339,25 +340,35 @@ defmodule WandererApp.Esi.ApiClient do
def get_custom_route_base_url, do: WandererApp.Env.custom_route_base_url()
def get_character_wallet(character_eve_id, opts \\ []),
do: _get_character_auth_data(character_eve_id, "wallet", opts)
do: get_character_auth_data(character_eve_id, "wallet", opts ++ @cache_opts)
def get_corporation_wallets(corporation_id, opts \\ []),
do: _get_corporation_auth_data(corporation_id, "wallets", opts)
do: get_corporation_auth_data(corporation_id, "wallets", opts)
def get_corporation_wallet_journal(corporation_id, division, opts \\ []),
do: _get_corporation_auth_data(corporation_id, "wallets/#{division}/journal", opts)
do:
get_corporation_auth_data(
corporation_id,
"wallets/#{division}/journal",
opts
)
def get_corporation_wallet_transactions(corporation_id, division, opts \\ []),
do: _get_corporation_auth_data(corporation_id, "wallets/#{division}/transactions", opts)
do:
get_corporation_auth_data(
corporation_id,
"wallets/#{division}/transactions",
opts
)
def get_character_location(character_eve_id, opts \\ []),
do: _get_character_auth_data(character_eve_id, "location", opts)
do: get_character_auth_data(character_eve_id, "location", opts ++ @cache_opts)
def get_character_online(character_eve_id, opts \\ []),
do: _get_character_auth_data(character_eve_id, "online", opts)
do: get_character_auth_data(character_eve_id, "online", opts ++ @cache_opts)
def get_character_ship(character_eve_id, opts \\ []),
do: _get_character_auth_data(character_eve_id, "ship", opts)
do: get_character_auth_data(character_eve_id, "ship", opts ++ @cache_opts)
def search(character_eve_id, opts \\ []) do
search_val = to_string(opts[:params][:search] || "")
@@ -372,7 +383,7 @@ defmodule WandererApp.Esi.ApiClient do
]
merged_opts = Keyword.put(opts, :params, query_params)
_search(character_eve_id, search_val, categories_val, merged_opts)
get_search(character_eve_id, search_val, categories_val, merged_opts)
end
@decorate cacheable(
@@ -380,11 +391,11 @@ defmodule WandererApp.Esi.ApiClient do
key: "search-#{character_eve_id}-#{categories_val}-#{search_val |> Slug.slugify()}",
opts: [ttl: @ttl]
)
defp _search(character_eve_id, search_val, categories_val, merged_opts) do
_get_character_auth_data(character_eve_id, "search", merged_opts)
defp get_search(character_eve_id, search_val, categories_val, merged_opts) do
get_character_auth_data(character_eve_id, "search", merged_opts)
end
defp _remove_intersection(pairs_arr) do
defp remove_intersection(pairs_arr) do
tuples = pairs_arr |> Enum.map(fn x -> {x.first, x.second} end)
tuples
@@ -405,9 +416,9 @@ defmodule WandererApp.Esi.ApiClient do
end
defp _get_routes(origin, destination, params, opts),
do: _get_routes_eve(origin, destination, params, opts)
do: get_routes_eve(origin, destination, params, opts)
defp _get_routes_eve(origin, destination, params, opts) do
defp get_routes_eve(origin, destination, params, opts) do
esi_params =
Map.merge(params, %{
connections: params.connections |> Enum.join(","),
@@ -416,7 +427,8 @@ defmodule WandererApp.Esi.ApiClient do
get(
"/route/#{origin}/#{destination}/?#{esi_params |> Plug.Conn.Query.encode()}",
opts
opts,
@cache_opts
)
end
@@ -426,17 +438,19 @@ defmodule WandererApp.Esi.ApiClient do
do:
get(
"/alliances/#{alliance_eve_id}/#{info_path}",
opts
opts,
@cache_opts
)
defp _get_corporation_info(corporation_eve_id, info_path, opts),
do:
get(
"/corporations/#{corporation_eve_id}/#{info_path}",
opts
opts,
@cache_opts
)
defp _get_character_auth_data(character_eve_id, info_path, opts) do
defp get_character_auth_data(character_eve_id, info_path, opts) do
path = "/characters/#{character_eve_id}/#{info_path}"
auth_opts =
@@ -445,7 +459,7 @@ defmodule WandererApp.Esi.ApiClient do
character_id = opts |> Keyword.get(:character_id, nil)
if not _is_access_token_expired?(character_id) do
if not is_access_token_expired?(character_id) do
get(
path,
auth_opts,
@@ -456,7 +470,7 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp _is_access_token_expired?(character_id) do
defp is_access_token_expired?(character_id) do
{:ok, %{expires_at: expires_at} = _character} =
WandererApp.Character.get_character(character_id)
@@ -465,13 +479,13 @@ defmodule WandererApp.Esi.ApiClient do
expires_at - now <= 0
end
defp _get_corporation_auth_data(corporation_eve_id, info_path, opts),
defp get_corporation_auth_data(corporation_eve_id, info_path, opts),
do:
get(
"/corporations/#{corporation_eve_id}/#{info_path}",
[params: opts[:params] || []] ++
(opts |> get_auth_opts()),
opts
opts ++ @cache_opts
)
defp with_user_agent_opts(opts) do
@@ -513,7 +527,7 @@ defmodule WandererApp.Esi.ApiClient do
|> Keyword.merge(@timeout_opts)
) do
{:ok, %{status: 200, body: body, headers: headers}} ->
maybe_cache_response(path, body, headers)
maybe_cache_response(path, body, headers, opts)
{:ok, body}
@@ -530,11 +544,9 @@ defmodule WandererApp.Esi.ApiClient do
get_retry(path, api_opts, opts, :error_limited)
{:ok, %{status: status}} ->
IO.inspect(status)
{:error, "Unexpected status: #{status}"}
{:error, _reason} ->
IO.inspect(_reason)
{:error, "Request failed"}
end
rescue
@@ -545,18 +557,20 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp maybe_cache_response(path, body, %{"expires" => [expires]})
defp maybe_cache_response(path, body, %{"expires" => [expires]}, opts)
when is_binary(path) and not is_nil(expires) do
try do
cached_ttl =
DateTime.diff(Timex.parse!(expires, "{RFC1123}"), DateTime.utc_now(), :millisecond)
if opts |> Keyword.get(:cache, false) do
cached_ttl =
DateTime.diff(Timex.parse!(expires, "{RFC1123}"), DateTime.utc_now(), :millisecond)
Cachex.put(
:api_cache,
path,
body,
ttl: cached_ttl
)
Cachex.put(
:api_cache,
path,
body,
ttl: cached_ttl
)
end
rescue
e ->
@logger.error(Exception.message(e))
@@ -565,7 +579,7 @@ defmodule WandererApp.Esi.ApiClient do
end
end
defp maybe_cache_response(_path, _body, _headers), do: :ok
defp maybe_cache_response(_path, _body, _headers, _opts), do: :ok
defp post(url, opts) do
try do

View File

@@ -96,7 +96,7 @@ defmodule WandererApp.Map do
map_id
|> get_map!()
|> Map.get(:characters, [])
|> Enum.map(&WandererApp.Character.get_character!(&1))
|> Enum.map(fn character_id -> WandererApp.Character.get_map_character!(map_id, character_id) end)
def list_systems(map_id),
do: {:ok, map_id |> get_map!() |> Map.get(:systems, Map.new()) |> Map.values()}

View File

@@ -9,12 +9,15 @@ defmodule WandererApp.Map.Manager do
alias WandererApp.Map.Server
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
def start_map(map_id) when is_binary(map_id),
do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
@@ -44,6 +47,9 @@ defmodule WandererApp.Map.Manager do
{:ok, garbage_collector_timer} =
:timer.send_interval(@garbage_collection_interval, :garbage_collect)
{:ok, signatures_cleanup_timer} =
:timer.send_interval(@signatures_cleanup_interval, :cleanup_signatures)
try do
Task.async(fn ->
start_last_active_maps()
@@ -56,7 +62,8 @@ defmodule WandererApp.Map.Manager do
{:ok,
%{
garbage_collector_timer: garbage_collector_timer,
check_maps_queue_timer: check_maps_queue_timer
check_maps_queue_timer: check_maps_queue_timer,
signatures_cleanup_timer: signatures_cleanup_timer
}}
end
@@ -118,6 +125,36 @@ defmodule WandererApp.Map.Manager do
end
end
@impl true
def handle_info(:cleanup_signatures, state) do
try do
cleanup_deleted_signatures()
{:noreply, state}
rescue
e ->
Logger.error("Failed to cleanup signatures: #{inspect(e)}")
{:noreply, state}
end
end
def cleanup_deleted_signatures() do
delete_after_date = DateTime.utc_now() |> DateTime.add(-1 * @delete_after_minutes, :minute)
case MapSystemSignature.by_deleted_and_updated_before!(true, delete_after_date) do
{:ok, deleted_signatures} ->
Enum.each(deleted_signatures, fn sig ->
Ash.destroy!(sig)
end)
:ok
{:error, error} ->
Logger.error("Failed to fetch deleted signatures: #{inspect(error)}")
{:error, error}
end
end
defp start_last_active_maps() do
{:ok, last_map_states} =
WandererApp.Api.MapState.get_last_active(

View File

@@ -11,7 +11,6 @@ defmodule WandererApp.Map.ZkbDataFetcher do
@interval :timer.seconds(15)
@store_map_kills_timeout :timer.hours(1)
@logger Application.compile_env(:wanderer_app, :logger)
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
# This means 120 “ticks” of 15s each → ~30 minutes
@preload_cycle_ticks 120
@@ -118,7 +117,10 @@ defmodule WandererApp.Map.ZkbDataFetcher do
|> Enum.map(&elem(&1, 0))
if changed_systems == [] do
Logger.debug(fn -> "[ZkbDataFetcher] No changes in detailed kills for map_id=#{map_id}" end)
Logger.debug(fn ->
"[ZkbDataFetcher] No changes in detailed kills for map_id=#{map_id}"
end)
:ok
else
# Build new details for each changed system
@@ -153,10 +155,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
changed_data = Map.take(updated_details_map, changed_systems)
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{
event: :detailed_kills_updated,
payload: changed_data
})
WandererApp.Map.Server.Impl.broadcast!(map_id, :detailed_kills_updated, changed_data)
:ok
end
@@ -173,6 +172,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
Enum.filter(all_system_ids, fn system_id ->
new_kills_count = Map.get(new_kills_map, system_id, 0)
old_kills_count = Map.get(old_kills_map, system_id, 0)
new_kills_count != old_kills_count and
(new_kills_count > 0 or (old_kills_count > 0 and new_kills_count == 0))
end)
@@ -187,10 +187,7 @@ defmodule WandererApp.Map.ZkbDataFetcher do
payload = Map.take(new_kills_map, changed_system_ids)
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{
event: :kills_updated,
payload: payload
})
WandererApp.Map.Server.Impl.broadcast!(map_id, :kills_updated, payload)
:ok
end

View File

@@ -1,12 +1,122 @@
defmodule WandererApp.Map.Operations.Connections do
@moduledoc """
CRUD and batch upsert for map connections.
Operations for managing map connections, including creation, updates, and deletions.
Handles special cases like C1 wormhole sizing rules and unique constraint handling.
"""
require Logger
alias WandererApp.Map.Server.{ConnectionsImpl, Server}
alias Ash.Error.Invalid
alias WandererApp.MapConnectionRepo
alias WandererApp.Map.Server
require Logger
# Connection type constants
@connection_type_wormhole 0
@connection_type_stargate 1
# Ship size constants
@small_ship_size 0
@medium_ship_size 1
@large_ship_size 2
@xlarge_ship_size 3
# System class constants
@c1_system_class "C1"
@doc """
Creates a connection between two systems, applying special rules for C1 wormholes.
Handles parsing of input parameters, validates system information, and manages
unique constraint violations gracefully.
"""
def create(attrs, map_id, char_id) do
do_create(attrs, map_id, char_id)
end
defp do_create(attrs, map_id, char_id) do
with {:ok, source} <- parse_int(attrs["solar_system_source"], "solar_system_source"),
{:ok, target} <- parse_int(attrs["solar_system_target"], "solar_system_target"),
{:ok, src_info} <- ConnectionsImpl.get_system_static_info(source),
{:ok, tgt_info} <- ConnectionsImpl.get_system_static_info(target) do
build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info)
else
{:error, reason} -> handle_precondition_error(reason, attrs)
{:ok, []} -> {:error, :inconsistent_state}
other -> {:error, :unexpected_precondition_error, other}
end
end
defp build_and_add_connection(attrs, map_id, char_id, src_info, tgt_info) do
info = %{
solar_system_source_id: src_info.solar_system_id,
solar_system_target_id: tgt_info.solar_system_id,
character_id: char_id,
type: parse_type(attrs["type"]),
ship_size_type: resolve_ship_size(attrs, src_info, tgt_info)
}
case Server.add_connection(map_id, info) do
:ok -> {:ok, :created}
{:ok, []} -> log_warn_and(:inconsistent_state, info)
{:error, %Invalid{errors: errs}} = err ->
if Enum.any?(errs, &is_unique_constraint_error?/1), do: {:skip, :exists}, else: err
{:error, _} = err -> Logger.error("[add_connection] #{inspect(err)}"); {:error, :server_error}
other -> Logger.error("[add_connection] unexpected: #{inspect(other)}"); {:error, :unexpected_error}
end
end
defp resolve_ship_size(attrs, src_info, tgt_info) do
type = parse_type(attrs["type"])
if type == @connection_type_wormhole and
(src_info.system_class == @c1_system_class or
tgt_info.system_class == @c1_system_class) do
@medium_ship_size
else
parse_ship_size(attrs["ship_size_type"], @large_ship_size)
end
end
defp parse_ship_size(nil, default), do: default
defp parse_ship_size(val, _default) when is_integer(val), do: val
defp parse_ship_size(val, default) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> i
:error -> default
end
end
defp parse_ship_size(_, default), do: default
defp parse_type(nil), do: @connection_type_wormhole
defp parse_type(val) when is_integer(val), do: val
defp parse_type(val) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> i
:error -> @connection_type_wormhole
end
end
defp parse_type(_), do: @connection_type_wormhole
defp parse_int(nil, field), do: {:error, {:missing_field, field}}
defp parse_int(val, _) when is_integer(val), do: {:ok, val}
defp parse_int(val, _) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> {:ok, i}
:error -> {:error, :invalid_integer}
end
end
defp parse_int(_, field), do: {:error, {:invalid_field, field}}
defp handle_precondition_error(reason, attrs) do
Logger.warning("[add_connection] precondition failed: #{inspect(reason)} for #{inspect(attrs)}")
{:error, :precondition_failed, reason}
end
defp log_warn_and(return, info) do
Logger.warning("[add_connection] inconsistent for #{inspect(info)}")
{:error, return}
end
defp is_unique_constraint_error?(%{code: :unique_constraint}), do: true
defp is_unique_constraint_error?(_), do: false
@spec list_connections(String.t()) :: [map()] | {:error, atom()}
def list_connections(map_id) do
@@ -38,52 +148,6 @@ defmodule WandererApp.Map.Operations.Connections do
end
end
@spec create_connection(Plug.Conn.t(), map()) :: {:ok, map()} | {:skip, :exists} | {:error, atom()}
def create_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, attrs) do
do_create(attrs, map_id, char_id)
end
def create_connection(map_id, attrs, char_id) do
do_create(attrs, map_id, char_id)
end
defp do_create(attrs, map_id, char_id) do
with {:ok, source} <- parse_int(attrs["solar_system_source"], "solar_system_source"),
{:ok, target} <- parse_int(attrs["solar_system_target"], "solar_system_target") do
info = %{
solar_system_source_id: source,
solar_system_target_id: target,
character_id: char_id,
type: parse_type(attrs["type"])
}
add_result = Server.add_connection(map_id, info)
case add_result do
:ok -> {:ok, :created}
{:ok, []} ->
Logger.warning("[do_create] Server.add_connection returned :ok, [] for map_id=#{inspect(map_id)}, source=#{inspect(source)}, target=#{inspect(target)}")
{:error, :inconsistent_state}
{:error, %Invalid{errors: errors}} = err ->
if Enum.any?(errors, &is_unique_constraint_error?/1), do: {:skip, :exists}, else: err
{:error, _} = err ->
Logger.error("[do_create] Server.add_connection error: #{inspect(err)}")
{:error, :server_error}
_ ->
Logger.error("[do_create] Unexpected add_result: #{inspect(add_result)}")
{:error, :unexpected_error}
end
else
{:ok, []} ->
Logger.warning("[do_create] Source or target system not found: attrs=#{inspect(attrs)}")
{:error, :inconsistent_state}
{:error, _} = err ->
Logger.error("[do_create] parse_int error: #{inspect(err)}, attrs=#{inspect(attrs)}")
{:error, :parse_error}
_ ->
Logger.error("[do_create] Unexpected error in preconditions: attrs=#{inspect(attrs)}")
{:error, :unexpected_precondition_error}
end
end
@spec update_connection(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
def update_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, conn_id, attrs) do
with {:ok, conn_struct} <- MapConnectionRepo.get_by_id(map_id, conn_id),
@@ -185,29 +249,6 @@ defmodule WandererApp.Map.Operations.Connections do
# -- Helpers ---------------------------------------------------------------
defp parse_int(val, _field) when is_integer(val), do: {:ok, val}
defp parse_int(val, field) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> {:ok, i}
_ -> {:error, "Invalid #{field}: #{val}"}
end
end
defp parse_int(nil, field), do: {:error, "Missing #{field}"}
defp parse_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
defp parse_type(val) when is_integer(val), do: val
defp parse_type(val) when is_binary(val) do
case Integer.parse(val) do
{i, _} -> i
_ -> 0
end
end
defp parse_type(_), do: 0
defp is_unique_constraint_error?(%{constraint: :unique}), do: true
defp is_unique_constraint_error?(%{constraint: :unique_constraint}), do: true
defp is_unique_constraint_error?(_), do: false
defp apply_connection_updates(map_id, conn, attrs, _char_id) do
Enum.reduce_while(attrs, :ok, fn {key, val}, _acc ->
result =
@@ -256,4 +297,16 @@ defmodule WandererApp.Map.Operations.Connections do
})
end
@doc "Creates a connection between two systems"
@spec create_connection(String.t(), map(), String.t()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()}
def create_connection(map_id, attrs, char_id) do
do_create(attrs, map_id, char_id)
end
@doc "Creates a connection between two systems from a Plug.Conn"
@spec create_connection(Plug.Conn.t(), map()) :: {:ok, :created} | {:skip, :exists} | {:error, atom()}
def create_connection(%{assigns: %{map_id: map_id, owner_character_id: char_id}} = _conn, attrs) do
do_create(attrs, map_id, char_id)
end
end

View File

@@ -3,8 +3,6 @@ defmodule WandererApp.Map.Server.AclsImpl do
require Logger
alias WandererApp.Map.Server.Impl
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
def handle_map_acl_updated(%{map_id: map_id, map: old_map} = state, added_acls, removed_acls) do
@@ -86,7 +84,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
end
end
def handle_acl_deleted(map_id, acl_id) do
def handle_acl_deleted(map_id, _acl_id) do
{:ok, map} =
WandererApp.MapRepo.get(map_id,
acls: [

View File

@@ -5,25 +5,22 @@ defmodule WandererApp.Map.Server.CharactersImpl do
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
def get_characters(%{map_id: map_id} = _state),
do: {:ok, map_id |> WandererApp.Map.list_characters()}
def add_character(%{map_id: map_id} = state, %{id: character_id} = character, track_character) do
Task.start_link(fn ->
with :ok <- map_id |> WandererApp.Map.add_character(character),
{:ok, _} <-
{:ok, _settings} <-
WandererApp.MapCharacterSettingsRepo.create(%{
character_id: character_id,
map_id: map_id,
tracked: track_character
}),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
{:ok, character} <- WandererApp.Character.get_map_character(map_id, character_id) do
Impl.broadcast!(map_id, :character_added, character)
:telemetry.execute([:wanderer_app, :map, :character, :added], %{count: 1})
:ok
else
_error ->
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, character} = WandererApp.Character.get_map_character(map_id, character_id)
Impl.broadcast!(map_id, :character_added, character)
:ok
end
@@ -35,7 +32,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
def remove_character(map_id, character_id) do
Task.start_link(fn ->
with :ok <- WandererApp.Map.remove_character(map_id, character_id),
{:ok, character} <- WandererApp.Character.get_character(character_id) do
{:ok, character} <- WandererApp.Character.get_map_character(map_id, character_id) do
Impl.broadcast!(map_id, :character_removed, character)
:telemetry.execute([:wanderer_app, :map, :character, :removed], %{count: 1})
@@ -64,7 +61,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
map_tracked_character_ids
|> Enum.filter(fn character -> character in tracked_characters end)
{:ok, old_map_tracked_characters} = WandererApp.Cache.lookup("maps:#{map_id}:tracked_characters", [])
{:ok, old_map_tracked_characters} =
WandererApp.Cache.lookup("maps:#{map_id}:tracked_characters", [])
characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
{:ok, invalidate_character_ids} =
@@ -73,7 +72,11 @@ defmodule WandererApp.Map.Server.CharactersImpl do
[]
)
WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", (invalidate_character_ids ++ characters_to_remove) |> Enum.uniq())
WandererApp.Cache.insert(
"map_#{map_id}:invalidate_character_ids",
(invalidate_character_ids ++ characters_to_remove) |> Enum.uniq()
)
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
:ok
@@ -84,12 +87,26 @@ defmodule WandererApp.Map.Server.CharactersImpl do
do:
character_ids
|> Enum.each(fn character_id ->
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: false
})
if is_character_map_active?(map_id, character_id) do
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: false
})
Impl.broadcast!(map_id, :untrack_character, character_id)
end
end)
def is_character_map_active?(map_id, character_id) do
case WandererApp.Character.get_character_state(character_id) do
{:ok, %{active_maps: active_maps}} ->
map_id in active_maps
_ ->
false
end
end
def cleanup_characters(map_id, owner_id) do
{:ok, invalidate_character_ids} =
WandererApp.Cache.lookup(
@@ -265,7 +282,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
end
defp update_character(map_id, character_id) do
{:ok, character} = WandererApp.Character.get_character(character_id)
{:ok, character} = WandererApp.Character.get_map_character(map_id, character_id)
Impl.broadcast!(map_id, :character_updated, character)
end
@@ -315,15 +332,18 @@ defmodule WandererApp.Map.Server.CharactersImpl do
is_nil(structure_id) and is_nil(station_id)
end
defp track_character(map_id, character_id),
do:
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: true,
track_online: true,
track_location: true,
track_ship: true
})
defp track_character(map_id, character_id) do
{:ok, character} = WandererApp.Character.get_character(character_id)
add_character(%{map_id: map_id}, character, true)
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
map_id: map_id,
track: true,
track_online: true,
track_location: true,
track_ship: true
})
end
defp maybe_update_online(map_id, character_id) do
with {:ok, old_online} <-
@@ -394,8 +414,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
{:ok, old_structure_id} =
WandererApp.Cache.lookup("map:#{map_id}:character:#{character_id}:structure_id")
{:ok,
%{solar_system_id: solar_system_id, structure_id: structure_id, station_id: station_id}} =
{:ok, %{solar_system_id: solar_system_id, structure_id: structure_id, station_id: station_id}} =
WandererApp.Character.get_character(character_id)
WandererApp.Cache.insert(
@@ -413,14 +432,15 @@ defmodule WandererApp.Map.Server.CharactersImpl do
structure_id
)
if solar_system_id != old_solar_system_id || structure_id != old_structure_id || station_id != old_station_id do
if solar_system_id != old_solar_system_id || structure_id != old_structure_id ||
station_id != old_station_id do
[
{:character_location,
%{
solar_system_id: solar_system_id,
structure_id: structure_id,
station_id: station_id
}, %{solar_system_id: old_solar_system_id}}
%{
solar_system_id: solar_system_id,
structure_id: structure_id,
station_id: station_id
}, %{solar_system_id: old_solar_system_id}}
]
else
[:skip]

View File

@@ -69,6 +69,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
@connection_time_status_eol 1
@connection_type_wormhole 0
@connection_type_stargate 1
@medium_ship_size 1
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
@@ -173,10 +174,12 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
%{time_status: old_time_status}, %{id: connection_id, time_status: time_status} ->
case time_status == @connection_time_status_eol do
true ->
WandererApp.Cache.put(
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
DateTime.utc_now()
)
if old_time_status != @connection_time_status_eol do
WandererApp.Cache.put(
"map_#{map_id}:conn_#{connection_id}:mark_eol_time",
DateTime.utc_now()
)
end
_ ->
if old_time_status == @connection_time_status_eol do
@@ -351,12 +354,26 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
@connection_type_wormhole
end
# Check if either system is C1 before creating the connection
{:ok, source_system_info} = get_system_static_info(old_location.solar_system_id)
{:ok, target_system_info} = get_system_static_info(location.solar_system_id)
# Set ship size type to medium only for wormhole connections involving C1 systems
ship_size_type = if connection_type == @connection_type_wormhole and
(source_system_info.system_class == @c1 or
target_system_info.system_class == @c1) do
@medium_ship_size
else
2 # Default to large for non-wormhole or non-C1 connections
end
{:ok, connection} =
WandererApp.MapConnectionRepo.create(%{
map_id: map_id,
solar_system_source: old_location.solar_system_id,
solar_system_target: location.solar_system_id,
type: connection_type
type: connection_type,
ship_size_type: ship_size_type
})
if connection_type == @connection_type_wormhole do
@@ -495,7 +512,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
known_jumps |> Enum.empty?()
:stargates ->
not is_prohibited_system_class?(from_system_static_info.system_class) and
# 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

View File

@@ -319,7 +319,11 @@ defmodule WandererApp.Map.Server.Impl do
def broadcast!(map_id, event, payload \\ nil) do
if can_broadcast?(map_id) do
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{event: event, payload: payload})
@pubsub_client.broadcast!(WandererApp.PubSub, map_id, %{
event: event,
payload: payload,
timestamp: DateTime.utc_now()
})
end
:ok

View File

@@ -3,147 +3,212 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
require Logger
alias WandererApp.Api.{MapSystem, MapSystemSignature}
alias WandererApp.Character
alias WandererApp.User.ActivityTracker
alias WandererApp.Map.Server.{Impl, ConnectionsImpl, SystemsImpl}
@doc """
Public entrypoint for updating signatures on a map system.
"""
def update_signatures(
%{map_id: map_id} = state,
%{
solar_system_id: solar_system_id,
character_id: character_id,
solar_system_id: system_solar_id,
character_id: char_id,
user_id: user_id,
delete_connection_with_sigs: delete_connection_with_sigs,
added_signatures: added_signatures,
updated_signatures: updated_signatures,
removed_signatures: removed_signatures
} =
_signatures_update
delete_connection_with_sigs: delete_conn?,
added_signatures: added_params,
updated_signatures: updated_params,
removed_signatures: removed_params
}
)
when not is_nil(character_id) do
WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
map_id: map_id,
solar_system_id: solar_system_id
})
|> case do
{:ok, system} ->
{:ok, %{eve_id: character_eve_id}} = WandererApp.Character.get_character(character_id)
added_signatures =
added_signatures
|> parse_signatures(character_eve_id, system.id)
updated_signatures =
updated_signatures
|> parse_signatures(character_eve_id, system.id)
updated_signatures_eve_ids =
updated_signatures
|> Enum.map(fn s -> s.eve_id end)
removed_signatures_eve_ids =
removed_signatures
|> parse_signatures(character_eve_id, system.id)
|> Enum.map(fn s -> s.eve_id end)
WandererApp.Api.MapSystemSignature.by_system_id!(system.id)
|> Enum.filter(fn s -> s.eve_id in removed_signatures_eve_ids end)
|> Enum.each(fn s ->
if delete_connection_with_sigs && not is_nil(s.linked_system_id) do
state
|> ConnectionsImpl.delete_connection(%{
solar_system_source_id: system.solar_system_id,
solar_system_target_id: s.linked_system_id
})
end
if not is_nil(s.linked_system_id) do
state
|> SystemsImpl.update_system_linked_sig_eve_id(%{
solar_system_id: s.linked_system_id,
linked_sig_eve_id: nil
})
end
s
|> Ash.destroy!()
end)
WandererApp.Api.MapSystemSignature.by_system_id!(system.id)
|> Enum.filter(fn s -> s.eve_id in updated_signatures_eve_ids end)
|> Enum.each(fn s ->
updated = updated_signatures |> Enum.find(fn u -> u.eve_id == s.eve_id end)
if not is_nil(updated) do
s
|> WandererApp.Api.MapSystemSignature.update(
updated
|> Map.put(:updated, System.os_time())
)
end
end)
added_signatures
|> Enum.each(fn s ->
s |> WandererApp.Api.MapSystemSignature.create!()
end)
added_signatures_eve_ids =
added_signatures
|> Enum.map(fn s -> s.eve_id end)
if not (added_signatures_eve_ids |> Enum.empty?()) do
WandererApp.User.ActivityTracker.track_map_event(:signatures_added, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: system.solar_system_id,
signatures: added_signatures_eve_ids
})
end
if not (removed_signatures_eve_ids |> Enum.empty?()) do
WandererApp.User.ActivityTracker.track_map_event(:signatures_removed, %{
character_id: character_id,
user_id: user_id,
map_id: map_id,
solar_system_id: system.solar_system_id,
signatures: removed_signatures_eve_ids
})
end
Impl.broadcast!(map_id, :signatures_updated, system.solar_system_id)
state
_ ->
when not is_nil(char_id) do
with {:ok, system} <-
MapSystem.read_by_map_and_solar_system(%{
map_id: map_id,
solar_system_id: system_solar_id
}),
{:ok, %{eve_id: char_eve_id}} <- Character.get_character(char_id) do
do_update_signatures(
state,
system,
char_eve_id,
user_id,
delete_conn?,
added_params,
updated_params,
removed_params
)
else
error ->
Logger.warning("Skipping signature update: #{inspect(error)}")
state
end
end
def update_signatures(
state,
_signatures_update
),
do: state
def update_signatures(state, _), do: state
defp parse_signatures(signatures, character_eve_id, system_id),
do:
signatures
|> Enum.map(fn %{
"eve_id" => eve_id,
"name" => name,
"kind" => kind,
"group" => group
} = signature ->
%{
system_id: system_id,
eve_id: eve_id,
name: name,
description: Map.get(signature, "description"),
kind: kind,
group: group,
type: Map.get(signature, "type"),
custom_info: Map.get(signature, "custom_info"),
character_eve_id: character_eve_id
}
end)
defp do_update_signatures(
state,
system,
character_eve_id,
user_id,
delete_conn?,
added_params,
updated_params,
removed_params
) do
# parse incoming DTOs
added_sigs = parse_signatures(added_params, character_eve_id, system.id)
updated_sigs = parse_signatures(updated_params, character_eve_id, system.id)
removed_sigs = parse_signatures(removed_params, character_eve_id, system.id)
# fetch both current & all (including deleted) signatures once
existing_current = MapSystemSignature.by_system_id!(system.id)
existing_all = MapSystemSignature.by_system_id_all!(system.id)
removed_ids = Enum.map(removed_sigs, & &1.eve_id)
updated_ids = Enum.map(updated_sigs, & &1.eve_id)
added_ids = Enum.map(added_sigs, & &1.eve_id)
# 1. Removals
existing_current
|> Enum.filter(&(&1.eve_id in removed_ids))
|> Enum.each(&remove_signature(&1, state, system, delete_conn?))
# 2. Updates
existing_current
|> Enum.filter(&(&1.eve_id in updated_ids))
|> Enum.each(fn existing ->
update = Enum.find(updated_sigs, &(&1.eve_id == existing.eve_id))
apply_update_signature(existing, update)
end)
# 3. Additions & restorations
added_eve_ids = Enum.map(added_sigs, & &1.eve_id)
existing_index =
MapSystemSignature.by_system_id_all!(system.id)
|> Enum.filter(&(&1.eve_id in added_eve_ids))
|> Map.new(&{&1.eve_id, &1})
added_sigs
|> Enum.each(fn sig ->
case existing_index[sig.eve_id] do
nil ->
MapSystemSignature.create!(sig)
%MapSystemSignature{deleted: true} = deleted_sig ->
MapSystemSignature.update!(
deleted_sig,
Map.take(sig, [
:name,
:description,
:kind,
:group,
:type,
:character_eve_id,
:custom_info,
:deleted,
:update_forced_at
])
)
_ ->
:noop
end
end)
# 4. Activity tracking
if added_ids != [] do
track_activity(
:signatures_added,
state.map_id,
system.solar_system_id,
user_id,
character_eve_id,
added_ids
)
end
if removed_ids != [] do
track_activity(
:signatures_removed,
state.map_id,
system.solar_system_id,
user_id,
character_eve_id,
removed_ids
)
end
# 5. Broadcast to any live subscribers
Impl.broadcast!(state.map_id, :signatures_updated, system.solar_system_id)
state
end
defp remove_signature(sig, state, system, delete_conn?) do
# optionally remove the linked connection
if delete_conn? && sig.linked_system_id do
ConnectionsImpl.delete_connection(state, %{
solar_system_source_id: system.solar_system_id,
solar_system_target_id: sig.linked_system_id
})
end
# clear any linked_sig_eve_id on the target system
if sig.linked_system_id do
SystemsImpl.update_system_linked_sig_eve_id(state, %{
solar_system_id: sig.linked_system_id,
linked_sig_eve_id: nil
})
end
# mark as deleted
MapSystemSignature.update!(sig, %{deleted: true})
end
defp apply_update_signature(%MapSystemSignature{} = existing, update_params)
when not is_nil(update_params) do
case MapSystemSignature.update(
existing,
update_params |> Map.put(:update_forced_at, DateTime.utc_now())
) do
{:ok, _updated} ->
:ok
{:error, reason} ->
Logger.error("Failed to update signature #{existing.id}: #{inspect(reason)}")
end
end
defp track_activity(event, map_id, solar_system_id, user_id, character_id, signatures) do
ActivityTracker.track_map_event(event, %{
map_id: map_id,
solar_system_id: solar_system_id,
user_id: user_id,
character_id: character_id,
signatures: signatures
})
end
@doc false
defp parse_signatures(signatures, character_eve_id, system_id) do
Enum.map(signatures, fn sig ->
%{
system_id: system_id,
eve_id: sig["eve_id"],
name: sig["name"],
description: Map.get(sig, "description"),
kind: sig["kind"],
group: sig["group"],
type: Map.get(sig, "type"),
custom_info: Map.get(sig, "custom_info"),
character_eve_id: character_eve_id,
deleted: false
}
end)
end
end

View File

@@ -17,7 +17,7 @@ defmodule WandererApp.Maps do
:is_shattered
]
def find_routes(map_id, hubs, origin, routes_settings) do
def find_routes(map_id, hubs, origin, routes_settings, false) do
{:ok, routes} =
WandererApp.Esi.find_routes(
map_id,
@@ -48,6 +48,19 @@ defmodule WandererApp.Maps do
{:ok, %{routes: routes, systems_static_data: systems_static_data}}
end
def find_routes(map_id, hubs, origin, routes_settings, true) do
origin = origin |> String.to_integer()
hubs = hubs |> Enum.map(&(&1 |> String.to_integer()))
routes =
hubs
|> Enum.map(fn hub ->
%{origin: origin, destination: hub, success: false, systems: [], has_connection: false}
end)
{:ok, %{routes: routes, systems_static_data: []}}
end
def get_available_maps() do
case WandererApp.Api.Map.available() do
{:ok, maps} -> {:ok, maps}

View File

@@ -1,8 +1,37 @@
defmodule WandererApp.MapCharacterSettingsRepo do
use WandererApp, :repository
def create(settings),
do: WandererApp.Api.MapCharacterSettings.create(settings)
def get(map_id, character_id) do
case WandererApp.Api.MapCharacterSettings.read_by_map_and_character(%{
map_id: map_id,
character_id: character_id
}) do
{:ok, settings} when not is_nil(settings) ->
{:ok, settings}
_ ->
WandererApp.Api.MapCharacterSettings.create(%{
character_id: character_id,
map_id: map_id,
tracked: false
})
end
end
def create(settings) do
WandererApp.Api.MapCharacterSettings.create(settings)
end
def update(map_id, character_id, updated_settings) do
case get(map_id, character_id) do
{:ok, settings} when not is_nil(settings) ->
settings
|> WandererApp.Api.MapCharacterSettings.update(updated_settings)
_ ->
{:ok, nil}
end
end
def get_tracked_by_map_filtered(map_id, character_ids),
do:

View File

@@ -9,6 +9,7 @@ defmodule WandererApp.Structure do
def update_structures(system, added, updated, removed, main_character_eve_id, user_id \\ nil) do
Logger.info("[Structure] update_structures called by user_id=#{inspect(user_id)}")
added_structs =
parse_structures(added, main_character_eve_id, system)
|> Enum.map(&Map.delete(&1, :id))
@@ -107,7 +108,11 @@ defmodule WandererApp.Structure do
updated_data = Map.delete(updated_data, :id)
# Merge update data with existing record to avoid nil required fields
merged_data = Map.merge(Map.from_struct(existing), updated_data, fn _k, v1, v2 -> if is_nil(v2), do: v1, else: v2 end)
merged_data =
Map.merge(Map.from_struct(existing), updated_data, fn _k, v1, v2 ->
if is_nil(v2), do: v1, else: v2
end)
# Only keep fields accepted by Ash update action
allowed_keys = [
:system_id,
@@ -124,10 +129,18 @@ defmodule WandererApp.Structure do
:status,
:end_time
]
filtered_data = Map.take(merged_data, allowed_keys)
Logger.info("[Structure] update_structures_in_db: calling update for id=#{existing.id} with: #{inspect(filtered_data)}")
Logger.debug(fn ->
"[Structure] update_structures_in_db: calling update for id=#{existing.id} with: #{inspect(filtered_data)}"
end)
new_record = MapSystemStructure.update(existing, filtered_data)
Logger.info("[Structure] update_structures_in_db: update result for id=#{existing.id}: #{inspect(new_record)}")
Logger.debug(fn ->
"[Structure] update_structures_in_db: update result for id=#{existing.id}: #{inspect(new_record)}"
end)
Logger.debug(fn ->
"[Structure] updated record =>\n" <> inspect(new_record, pretty: true)

View File

@@ -66,12 +66,44 @@ defmodule WandererAppWeb.Layouts do
"""
end
def youtube_container(assigns) do
~H"""
<.link
href="https://www.youtube.com/@wanderer_ltd"
class="flex flex-col p-4 items-center absolute bottom-52 left-0 gap-2 tooltip tooltip-right text-gray-400 hover:text-white"
>
<svg
width="24px"
height="24px"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M20.5245 6.00694C20.3025 5.81544 20.0333 5.70603 19.836 5.63863C19.6156 5.56337 19.3637 5.50148 19.0989 5.44892C18.5677 5.34348 17.9037 5.26005 17.1675 5.19491C15.6904 5.06419 13.8392 5 12 5C10.1608 5 8.30956 5.06419 6.83246 5.1949C6.09632 5.26005 5.43231 5.34348 4.9011 5.44891C4.63628 5.50147 4.38443 5.56337 4.16403 5.63863C3.96667 5.70603 3.69746 5.81544 3.47552 6.00694C3.26514 6.18846 3.14612 6.41237 3.07941 6.55976C3.00507 6.724 2.94831 6.90201 2.90314 7.07448C2.81255 7.42043 2.74448 7.83867 2.69272 8.28448C2.58852 9.18195 2.53846 10.299 2.53846 11.409C2.53846 12.5198 2.58859 13.6529 2.69218 14.5835C2.74378 15.047 2.81086 15.4809 2.89786 15.8453C2.97306 16.1603 3.09841 16.5895 3.35221 16.9023C3.58757 17.1925 3.92217 17.324 4.08755 17.3836C4.30223 17.461 4.55045 17.5218 4.80667 17.572C5.32337 17.6733 5.98609 17.7527 6.72664 17.8146C8.2145 17.9389 10.1134 18 12 18C13.8865 18 15.7855 17.9389 17.2733 17.8146C18.0139 17.7527 18.6766 17.6733 19.1933 17.572C19.4495 17.5218 19.6978 17.461 19.9124 17.3836C20.0778 17.324 20.4124 17.1925 20.6478 16.9023C20.9016 16.5895 21.0269 16.1603 21.1021 15.8453C21.1891 15.4809 21.2562 15.047 21.3078 14.5835C21.4114 13.6529 21.4615 12.5198 21.4615 11.409C21.4615 10.299 21.4115 9.18195 21.3073 8.28448C21.2555 7.83868 21.1874 7.42043 21.0969 7.07448C21.0517 6.90201 20.9949 6.72401 20.9206 6.55976C20.8539 6.41236 20.7349 6.18846 20.5245 6.00694Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14.5385 11.5L10.0962 14.3578L10.0962 8.64207L14.5385 11.5Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</.link>
"""
end
def donate_container(assigns) do
~H"""
<.link
href="https://www.patreon.com/WandererLtd"
target="_blank"
class="flex flex-col p-4 items-center absolute bottom-52 left-1 gap-2 tooltip tooltip-right text-gray-400 hover:text-white"
class="flex flex-col p-4 items-center absolute bottom-64 left-1 gap-2 tooltip tooltip-right text-gray-400 hover:text-white"
>
<.icon name="hero-banknotes-solid" class="h-4 w-4" />
</.link>

View File

@@ -19,6 +19,7 @@
<.ping_container rtt_class={@rtt_class} />
<.donate_container />
<.feedback_container />
<.youtube_container />
<.new_version_banner app_version={@app_version} enabled={@map_subscriptions_enabled?} />
</div>

View File

@@ -200,48 +200,28 @@ defmodule WandererAppWeb.MapAccessListAPIController do
@spec index(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :index,
summary: "List ACLs for a Map",
description: "Lists the ACLs for a given map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
description: "Lists the ACLs for a given map. Provide only one of map_id or slug as a query parameter. If both are provided, the request will fail.",
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
description: "Map identifier (UUID) - Provide only one of map_id or slug.",
type: :string,
required: false,
example: "00000000-0000-0000-0000-000000000000"
required: false
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
description: "Map slug - Provide only one of map_id or slug.",
type: :string,
required: false,
example: "map-name"
required: false
]
],
responses: [
ok: {
"List of ACLs",
"application/json",
@acl_index_response_schema
},
ok: {"List of ACLs", "application/json", @acl_index_response_schema},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
properties: %{error: %OpenApiSpex.Schema{type: :string}},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
}
}},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Map not found. Please provide a valid map_id or slug as a query parameter."
}
example: %{"error" => "Must provide only one of map_id or slug as a query parameter"}
}}
]
def index(conn, params) do
@@ -277,46 +257,30 @@ defmodule WandererAppWeb.MapAccessListAPIController do
"""
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
operation :create,
summary: "Create a new ACL",
description: "Creates a new ACL for a map. Requires either 'map_id' or 'slug' as a query parameter to identify the map.",
summary: "Create ACL for a Map",
description: "Creates a new ACL for a given map. Provide only one of map_id or slug as a query parameter. If both are provided, the request will fail.",
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
description: "Map identifier (UUID) - Provide only one of map_id or slug.",
type: :string,
required: false,
example: "00000000-0000-0000-0000-000000000000"
required: false
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
description: "Map slug - Provide only one of map_id or slug.",
type: :string,
required: false,
example: "map-name"
required: false
]
],
request_body: {"Access List parameters", "application/json", @acl_create_request_schema},
request_body: {"ACL parameters", "application/json", @acl_create_request_schema},
responses: [
ok: {"Access List", "application/json", @acl_create_response_schema},
created: {"Created ACL", "application/json", @acl_create_response_schema},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
properties: %{error: %OpenApiSpex.Schema{type: :string}},
required: ["error"],
example: %{
"error" => "Must provide either ?map_id=UUID or ?slug=SLUG as a query parameter"
}
}},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Map not found. Please provide a valid map_id or slug as a query parameter."
}
example: %{"error" => "Must provide only one of map_id or slug as a query parameter"}
}}
]
def create(conn, params) do

View File

@@ -25,6 +25,7 @@ defmodule WandererAppWeb.AuthController do
case WandererApp.Api.Character.by_eve_id(character_data.eve_id) do
{:ok, character} ->
character_update = %{
name: auth.info.name,
access_token: auth.credentials.token,
refresh_token: auth.credentials.refresh_token,
expires_at: auth.credentials.expires_at,

View File

@@ -1,17 +1,9 @@
<div>
<div class="flex mt-20px max-h-[calc(100vh-50px)] max-w-[100vw] flex-col items-center justify-start xl:flex-row xl:items-start xl:justify-between">
<div class="shrink xl:w-1/2">
<div class="flex min-h-[calc(100vh-100px)] items-center justify-center px-2 py-10 text-center xl:justify-start xl:pe-0 xl:ps-10 xl:text-start">
<div class="flex mt-20px max-h-[calc(100vh-50px)] max-w-[100vw] flex-col items-center">
<div class="shrink">
<div class="flex min-h-[calc(100vh-100px)] items-center justify-center px-2 py-10 text-center xl:pe-0 xl:ps-10">
<div>
<div class="flex flex-col items-center gap-6 xl:flex-row">
<div class="tooltip tooltip-accent" data-tip="copy">
<button class="btn btn-sm cursor-copy rounded-full font-mono font-light">
<pre><code>From: Wanderer Team</code></pre>
</button>
</div>
</div>
<div class="h-3"></div>
<h1 class="font-title text-center text-[clamp(2rem,6vw,4.2rem)] font-black leading-[1.1] [word-break:auto-phrase] xl:w-[115%] xl:text-start [:root[dir=rtl]_&amp;]:leading-[1.35]">
<h1 class="text-center text-[clamp(2rem,6vw,4rem)] font-black leading-[1.1] [word-break:auto-phrase] xl:w-[115%] xl:text-start [:root[dir=rtl]_&amp;]:leading-[1.35]">
<span class="[&amp;::selection]:text-base-content brightness-150 contrast-150 [&amp;::selection]:bg-blue-700/20">
Join or support us!
<!---->
@@ -19,7 +11,7 @@
</h1>
<div class="h-10"></div>
<div>
<div class="inline-flex w-full flex-col items-stretch justify-center gap-2 px-4 md:flex-row xl:justify-start xl:px-0">
<div class="inline-flex w-full items-stretch justify-center gap-2 px-4 flex-col">
<.link
href="https://discord.gg/cafERvDD2k"
class="btn md:btn-lg group shrink-0 rounded-full [@media(min-width:768px)]:px-10 bg-[oklch(64.74%_0.124_270.62)] border-[oklch(64.74%_0.124_270.62)] hover:bg-[oklch(60%_0.124_270.62)] hover:border-[oklch(60%_0.124_270.62)]"
@@ -226,6 +218,52 @@
</path>
</svg>
</a>
<a
href="https://www.youtube.com/@wanderer_ltd"
rel="noopener noreferrer"
aria-label="YouTube"
class="btn md:btn-lg group shrink-0 rounded-full [@media(min-width:768px)]:px-10 bg-[#ff0033] border-[#ff0033] hover:bg-[#ff0033] hover:border-[#ff0033] text-white"
>
<svg
width="48px"
height="48px"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="transition-opacity opacity-100 group-hover:opacity-0"
>
<path
d="M20.5245 6.00694C20.3025 5.81544 20.0333 5.70603 19.836 5.63863C19.6156 5.56337 19.3637 5.50148 19.0989 5.44892C18.5677 5.34348 17.9037 5.26005 17.1675 5.19491C15.6904 5.06419 13.8392 5 12 5C10.1608 5 8.30956 5.06419 6.83246 5.1949C6.09632 5.26005 5.43231 5.34348 4.9011 5.44891C4.63628 5.50147 4.38443 5.56337 4.16403 5.63863C3.96667 5.70603 3.69746 5.81544 3.47552 6.00694C3.26514 6.18846 3.14612 6.41237 3.07941 6.55976C3.00507 6.724 2.94831 6.90201 2.90314 7.07448C2.81255 7.42043 2.74448 7.83867 2.69272 8.28448C2.58852 9.18195 2.53846 10.299 2.53846 11.409C2.53846 12.5198 2.58859 13.6529 2.69218 14.5835C2.74378 15.047 2.81086 15.4809 2.89786 15.8453C2.97306 16.1603 3.09841 16.5895 3.35221 16.9023C3.58757 17.1925 3.92217 17.324 4.08755 17.3836C4.30223 17.461 4.55045 17.5218 4.80667 17.572C5.32337 17.6733 5.98609 17.7527 6.72664 17.8146C8.2145 17.9389 10.1134 18 12 18C13.8865 18 15.7855 17.9389 17.2733 17.8146C18.0139 17.7527 18.6766 17.6733 19.1933 17.572C19.4495 17.5218 19.6978 17.461 19.9124 17.3836C20.0778 17.324 20.4124 17.1925 20.6478 16.9023C20.9016 16.5895 21.0269 16.1603 21.1021 15.8453C21.1891 15.4809 21.2562 15.047 21.3078 14.5835C21.4114 13.6529 21.4615 12.5198 21.4615 11.409C21.4615 10.299 21.4115 9.18195 21.3073 8.28448C21.2555 7.83868 21.1874 7.42043 21.0969 7.07448C21.0517 6.90201 20.9949 6.72401 20.9206 6.55976C20.8539 6.41236 20.7349 6.18846 20.5245 6.00694Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14.5385 11.5L10.0962 14.3578L10.0962 8.64207L14.5385 11.5Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<span class="text-lg font-bold transition-opacity opacity-100 group-hover:opacity-0 ml-2">
YouTube
</span>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
class="inline-block absolute w-6 fill-white transition-all scale-90 group-hover:scale-100 opacity-0 group-hover:opacity-100"
>
<path
fill-rule="evenodd"
d="M19,14 L19,19 C19,20.1045695 18.1045695,21 17,21 L5,21 C3.8954305,21 3,20.1045695 3,19 L3,7 C3,5.8954305 3.8954305,5 5,5 L10,5 L10,7 L5,7 L5,19 L17,19 L17,14 L19,14 Z M18.9971001,6.41421356 L11.7042068,13.7071068 L10.2899933,12.2928932 L17.5828865,5 L12.9971001,5 L12.9971001,3 L20.9971001,3 L20.9971001,11 L18.9971001,11 L18.9971001,6.41421356 Z"
>
</path>
</svg>
</a>
</div>
</div>
</div>

View File

@@ -246,7 +246,6 @@ defmodule WandererAppWeb.MapAPIController do
in: :query,
description: "Map slug",
type: :string,
example: "my-map",
required: false
],
map_id: [
@@ -319,7 +318,7 @@ defmodule WandererAppWeb.MapAPIController do
end
@doc """
GET /api/map/structure_timers
GET /api/map/structure-timers
Returns structure timers for visible systems on the map or for a specific system.
"""
@@ -327,6 +326,7 @@ defmodule WandererAppWeb.MapAPIController do
operation :show_structure_timers,
summary: "Show Structure Timers",
description: "Retrieves structure timers for a map.",
deprecated: true,
parameters: [
map_id: [
in: :query,
@@ -342,7 +342,7 @@ defmodule WandererAppWeb.MapAPIController do
],
system_id: [
in: :query,
description: "System ID",
description: "Optional: System ID to filter timers for a specific system",
type: :string,
required: false
]
@@ -790,15 +790,13 @@ defmodule WandererAppWeb.MapAPIController do
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided",
type: :string,
required: false,
example: ""
required: false
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided",
type: :string,
required: false,
example: "map-name"
required: false
]
],
responses: [

View File

@@ -2,15 +2,9 @@ defmodule WandererAppWeb.MapAuditAPIController do
use WandererAppWeb, :controller
use OpenApiSpex.ControllerSpecs
import Ash.Query, only: [filter: 2]
require Logger
alias WandererApp.Api
alias WandererApp.Api.Character
alias WandererApp.MapSystemRepo
alias WandererApp.MapCharacterSettingsRepo
alias WandererApp.Zkb.KillsProvider.KillsCache
alias WandererAppWeb.Helpers.APIUtils
@@ -158,15 +152,4 @@ defmodule WandererAppWeb.MapAuditAPIController do
)
)
end
defp get_original_system_name(solar_system_id) do
# Fetch the original system name from the MapSolarSystem resource
case WandererApp.Api.MapSolarSystem.by_solar_system_id(solar_system_id) do
{:ok, system} ->
system.solar_system_name
_error ->
"Unknown System"
end
end
end

View File

@@ -129,23 +129,46 @@ defmodule WandererAppWeb.MapConnectionAPIController do
operation :index,
summary: "List Map Connections",
description: "Lists all connections for a map.",
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "map-slug or map UUID"
],
solar_system_source: [in: :query, type: :integer, required: false],
solar_system_target: [in: :query, type: :integer, required: false]
solar_system_source: [
in: :query,
description: "Filter connections by source system ID",
type: :integer,
required: false,
example: 30000142
],
solar_system_target: [
in: :query,
description: "Filter connections by target system ID",
type: :integer,
required: false,
example: 30000144
]
],
responses: [
ok: {
"List Map Connections",
"List of Map Connections",
"application/json",
@list_response_schema
}
},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Map not found"
}
}}
]
def index(%{assigns: %{map_id: map_id}} = conn, params) do
with {:ok, src_filter} <- parse_optional(params, "solar_system_source"),
@@ -187,7 +210,7 @@ defmodule WandererAppWeb.MapConnectionAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "map-slug or map UUID"
@@ -218,12 +241,11 @@ defmodule WandererAppWeb.MapConnectionAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "map-slug or map UUID"
],
system_id: [in: :path, type: :string, required: false]
]
],
request_body: {"Connection create", "application/json", @connection_request_schema},
responses: ResponseSchemas.create_responses(@detail_response_schema)
@@ -256,7 +278,7 @@ defmodule WandererAppWeb.MapConnectionAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "map-slug or map UUID"
@@ -344,7 +366,7 @@ defmodule WandererAppWeb.MapConnectionAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "map-slug or map UUID"
@@ -442,9 +464,49 @@ defmodule WandererAppWeb.MapConnectionAPIController do
@deprecated "Use GET /api/maps/:map_identifier/systems instead"
operation :list_all_connections,
summary: "List All Connections (Legacy)",
description: "Legacy endpoint for listing connections. Use GET /api/maps/:map_identifier/connections instead. Requires exactly one of map_id or slug as a query parameter. If both are provided, a 400 Bad Request will be returned.",
deprecated: true,
parameters: [map_id: [in: :query]],
responses: ResponseSchemas.standard_responses(@list_response_schema)
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Exactly one of map_id or slug must be provided",
type: :string,
required: false
],
slug: [
in: :query,
description: "Map slug - Exactly one of map_id or slug must be provided",
type: :string,
required: false
]
],
responses: [
ok: {
"List of Map Connections",
"application/json",
@list_response_schema
},
bad_request: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Must provide exactly one of map_id or slug as a query parameter"
}
}},
not_found: {"Error", "application/json", %OpenApiSpex.Schema{
type: :object,
properties: %{
error: %OpenApiSpex.Schema{type: :string}
},
required: ["error"],
example: %{
"error" => "Map not found. Please provide a valid map_id or slug as a query parameter."
}
}}
]
def list_all_connections(%{assigns: %{map_id: map_id}} = conn, _params) do
connections = Operations.list_connections(map_id)
data = Enum.map(connections, &APIUtils.connection_to_json/1)

View File

@@ -24,15 +24,18 @@ defmodule WandererAppWeb.MapSystemAPIController do
solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
region_name: %Schema{type: :string, description: "EVE region name"},
position_x: %Schema{type: :number, format: :float, description: "X coordinate"},
position_y: %Schema{type: :number, format: :float, description: "Y coordinate"},
status: %Schema{type: :string, description: "System status"},
position_x: %Schema{type: :integer, description: "X coordinate"},
position_y: %Schema{type: :integer, description: "Y coordinate"},
status: %Schema{
type: :integer,
description: "System status (0: unknown, 1: friendly, 2: warning, 3: targetPrimary, 4: targetSecondary, 5: dangerousPrimary, 6: dangerousSecondary, 7: lookingFor, 8: home)"
},
visible: %Schema{type: :boolean, description: "Visibility flag"},
description: %Schema{type: :string, nullable: true, description: "Custom description"},
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
locked: %Schema{type: :boolean, description: "Lock flag"},
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
labels: %Schema{type: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
labels: %Schema{type: :string, description: "Comma-separated list of labels"}
},
required: ~w(id map_id solar_system_id)a
}
@@ -42,23 +45,27 @@ defmodule WandererAppWeb.MapSystemAPIController do
properties: %{
solar_system_id: %Schema{type: :integer, description: "EVE solar system ID"},
solar_system_name: %Schema{type: :string, description: "EVE solar system name"},
position_x: %Schema{type: :number, format: :float, description: "X coordinate"},
position_y: %Schema{type: :number, format: :float, description: "Y coordinate"},
status: %Schema{type: :string, description: "System status"},
position_x: %Schema{type: :integer, description: "X coordinate"},
position_y: %Schema{type: :integer, description: "Y coordinate"},
status: %Schema{
type: :integer,
description: "System status (0: unknown, 1: friendly, 2: warning, 3: targetPrimary, 4: targetSecondary, 5: dangerousPrimary, 6: dangerousSecondary, 7: lookingFor, 8: home)"
},
visible: %Schema{type: :boolean, description: "Visibility flag"},
description: %Schema{type: :string, nullable: true, description: "Custom description"},
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
locked: %Schema{type: :boolean, description: "Lock flag"},
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
labels: %Schema{type: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
labels: %Schema{type: :string, description: "Comma-separated list of labels"}
},
required: ~w(solar_system_id)a,
example: %{
solar_system_id: 30_000_142,
solar_system_name: "Jita",
position_x: 100.5,
position_y: 200.3,
visible: true
position_x: 100,
position_y: 200,
visible: true,
labels: "market,hub"
}
}
@@ -66,24 +73,29 @@ defmodule WandererAppWeb.MapSystemAPIController do
type: :object,
properties: %{
solar_system_name: %Schema{type: :string, description: "EVE solar system name", nullable: true},
position_x: %Schema{type: :number, format: :float, description: "X coordinate", nullable: true},
position_y: %Schema{type: :number, format: :float, description: "Y coordinate", nullable: true},
status: %Schema{type: :string, description: "System status", nullable: true},
position_x: %Schema{type: :integer, description: "X coordinate", nullable: true},
position_y: %Schema{type: :integer, description: "Y coordinate", nullable: true},
status: %Schema{
type: :integer,
description: "System status (0: unknown, 1: friendly, 2: warning, 3: targetPrimary, 4: targetSecondary, 5: dangerousPrimary, 6: dangerousSecondary, 7: lookingFor, 8: home)",
nullable: true
},
visible: %Schema{type: :boolean, description: "Visibility flag", nullable: true},
description: %Schema{type: :string, nullable: true, description: "Custom description"},
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
locked: %Schema{type: :boolean, description: "Lock flag", nullable: true},
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
labels: %Schema{type: :array, items: %Schema{type: :string}, nullable: true, description: "Labels"}
labels: %Schema{type: :string, description: "Comma-separated list of labels"}
},
example: %{
solar_system_name: "Jita",
position_x: 101.0,
position_y: 202.0,
position_x: 101,
position_y: 202,
visible: false,
status: "active",
status: 0,
tag: "HQ",
locked: true
locked: true,
labels: "market,hub"
}
}
@@ -290,10 +302,10 @@ defmodule WandererAppWeb.MapSystemAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "my-map-slug or map UUID"
example: "map-slug or map UUID"
]
],
responses: [
@@ -314,12 +326,17 @@ defmodule WandererAppWeb.MapSystemAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "my-map-slug or map UUID"
example: "map-slug or map UUID"
],
id: [in: :path, type: :string, required: true]
id: [
in: :path,
description: "System ID",
type: :string,
required: true
]
],
responses: ResponseSchemas.standard_responses(@detail_response_schema)
def show(%{assigns: %{map_id: map_id}} = conn, %{"id" => id}) do
@@ -334,10 +351,10 @@ defmodule WandererAppWeb.MapSystemAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "my-map-slug or map UUID"
example: "map-slug or map UUID"
]
],
request_body: {"Systems+Connections upsert", "application/json", @batch_request_schema},
@@ -358,12 +375,17 @@ defmodule WandererAppWeb.MapSystemAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "my-map-slug or map UUID"
example: "map-slug or map UUID"
],
id: [in: :path, type: :string, required: true]
id: [
in: :path,
description: "System ID",
type: :string,
required: true
]
],
request_body: {"System update request", "application/json", @system_update_schema},
responses: ResponseSchemas.update_responses(@detail_response_schema)
@@ -381,10 +403,10 @@ defmodule WandererAppWeb.MapSystemAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "my-map-slug or map UUID"
example: "map-slug or map UUID"
]
],
request_body: {"Batch delete", "application/json", @batch_delete_schema},
@@ -428,12 +450,17 @@ defmodule WandererAppWeb.MapSystemAPIController do
parameters: [
map_identifier: [
in: :path,
description: "Map identifier (UUID or slug). Provide either a UUID or a slug.",
description: "Map identifier (UUID or slug)",
type: :string,
required: true,
example: "my-map-slug or map UUID"
example: "map-slug or map UUID"
],
id: [in: :path, type: :string, required: true]
id: [
in: :path,
description: "System ID",
type: :string,
required: true
]
],
responses: ResponseSchemas.standard_responses(@delete_response_schema)
def delete_single(conn, %{"id" => id}) do
@@ -462,7 +489,20 @@ defmodule WandererAppWeb.MapSystemAPIController do
summary: "List Map Systems (Legacy)",
deprecated: true,
description: "Deprecated, use GET /api/maps/:map_identifier/systems instead",
parameters: [map_id: [in: :query]],
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided, but not both",
type: :string,
required: false,
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided, but not both",
type: :string,
required: false,
]
],
responses: ResponseSchemas.standard_responses(@list_response_schema)
defdelegate list_systems(conn, params), to: __MODULE__, as: :index
@@ -470,7 +510,26 @@ defmodule WandererAppWeb.MapSystemAPIController do
summary: "Show Map System (Legacy)",
deprecated: true,
description: "Deprecated, use GET /api/maps/:map_identifier/systems/:id instead",
parameters: [map_id: [in: :query], id: [in: :query]],
parameters: [
map_id: [
in: :query,
description: "Map identifier (UUID) - Either map_id or slug must be provided, but not both",
type: :string,
required: false,
],
slug: [
in: :query,
description: "Map slug - Either map_id or slug must be provided, but not both",
type: :string,
required: false,
],
id: [
in: :query,
description: "System ID",
type: :string,
required: true
]
],
responses: ResponseSchemas.standard_responses(@detail_response_schema)
defdelegate show_system(conn, params), to: __MODULE__, as: :show

View File

@@ -8,7 +8,6 @@ defmodule WandererAppWeb.MapSystemStructureAPIController do
@moduledoc """
API controller for managing map system structures.
Includes legacy structure-timers endpoint (deprecated).
"""
# Inlined OpenAPI schema for a map system structure
@@ -174,16 +173,25 @@ defmodule WandererAppWeb.MapSystemStructureAPIController do
end
@doc """
@deprecated "Use /structures instead. This endpoint will be removed in a future release."
Legacy: Get structure timers for a map.
Get structure timers for a map.
"""
operation :structure_timers,
summary: "Get structure timers for a map (Legacy)",
deprecated: true,
summary: "Get structure timers for a map",
parameters: [
map_identifier: [in: :path, description: "Map identifier (UUID or slug)", type: :string, required: true]
],
responses: [ok: {"Structure timers", "application/json", %Schema{type: :array, items: %Schema{type: :object}}}]
responses: [ok: {"Structure timers", "application/json", %Schema{
type: :object,
properties: %{
data: %Schema{
type: :array,
items: @structure_schema
}
},
example: %{
data: [@structure_schema.example]
}
}}]
def structure_timers(conn, _params) do
map_id = conn.assigns.map_id
structures = MapOperations.list_structures(map_id)

View File

@@ -91,6 +91,8 @@ defmodule WandererAppWeb.CharactersTrackingLive do
character_setting
|> WandererApp.MapCharacterSettingsRepo.untrack!()
WandererApp.Map.Server.untrack_characters(selected_map.id, [character_setting.character_id])
_ ->
character_setting
|> WandererApp.MapCharacterSettingsRepo.track!()

View File

@@ -32,6 +32,16 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
)
end
def handle_server_event(%{event: :untrack_character, payload: character_id}, %{
assigns: %{
map_id: map_id
}
} = socket) do
:ok = WandererApp.Character.TrackingUtils.untrack([%{id: character_id}], map_id, self())
socket
end
def handle_server_event(
%{event: :characters_updated},
%{
@@ -342,9 +352,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
self()
)
:ok =
WandererApp.Character.TrackingUtils.add_characters(map_characters, map_id, track_character)
socket
end

View File

@@ -57,7 +57,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
case track_character do
false ->
:ok = WandererApp.Character.TrackingUtils.untrack(map_characters, map_id, self())
:ok = WandererApp.Character.TrackingUtils.remove_characters(map_characters, map_id)
_ ->
:ok =
@@ -67,13 +66,6 @@ defmodule WandererAppWeb.MapCoreEventHandler do
true,
self()
)
:ok =
WandererApp.Character.TrackingUtils.add_characters(
map_characters,
map_id,
track_character
)
end
socket

View File

@@ -51,21 +51,36 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
%{"system_id" => solar_system_id, "routes_settings" => routes_settings} = _event,
%{assigns: %{map_id: map_id, map_loaded?: true}} = socket
) do
Task.async(fn ->
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs()
{:ok, map} = map_id |> WandererApp.Map.get_map()
hubs_limit = map |> Map.get(:hubs_limit, 20)
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs()
is_hubs_limit_reached = hubs |> Enum.count() > hubs_limit
Task.async(fn ->
{:ok, routes} =
WandererApp.Maps.find_routes(
map_id,
hubs,
solar_system_id,
get_routes_settings(routes_settings)
get_routes_settings(routes_settings),
is_hubs_limit_reached
)
{:routes, {solar_system_id, routes}}
end)
{:noreply, socket}
if is_hubs_limit_reached do
{:noreply,
socket
|> put_flash(
:warning,
"The Map hubs limit has been reached, please try to remove some hubs first, or contact the map administrators."
)}
else
{:noreply, socket}
end
end
def handle_ui_event(
@@ -80,16 +95,22 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
}
} = socket
) do
{:ok, map} = map_id |> WandererApp.Map.get_map()
hubs_limit = map |> Map.get(:hubs_limit, 20)
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
is_hubs_limit_reached = hubs |> Enum.count() > hubs_limit
Task.async(fn ->
if is_subscription_active? do
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
{:ok, routes} =
WandererApp.Maps.find_routes(
map_id,
hubs,
solar_system_id,
get_routes_settings(routes_settings)
get_routes_settings(routes_settings),
is_hubs_limit_reached
)
{:user_routes, {solar_system_id, routes}}
@@ -98,9 +119,197 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
end
end)
if is_hubs_limit_reached do
{:noreply,
socket
|> put_flash(
:warning,
"The user hubs limit has been reached, please try to remove some hubs first, or contact the map administrators."
)}
else
{:noreply, socket}
end
end
def handle_ui_event(
"add_hub",
%{"system_id" => solar_system_id} = _event,
%{
assigns: %{
map_id: map_id,
current_user: current_user,
main_character_id: main_character_id,
has_tracked_characters?: true,
user_permissions: %{update_system: true}
}
} =
socket
)
when not is_nil(main_character_id) do
{:ok, map} = map_id |> WandererApp.Map.get_map()
hubs_limit = map |> Map.get(:hubs_limit, 20)
{:ok, hubs} = map_id |> WandererApp.Map.list_hubs()
if hubs |> Enum.count() < hubs_limit do
map_id
|> WandererApp.Map.Server.add_hub(%{
solar_system_id: solar_system_id
})
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:hub_added, %{
character_id: main_character_id,
user_id: current_user.id,
map_id: map_id,
solar_system_id: solar_system_id
})
{:noreply, socket}
else
{:noreply,
socket
|> put_flash(
:warning,
"The Map hubs limit has been reached, please try to remove some hubs first, or contact the map administrators."
)}
end
end
def handle_ui_event(
"delete_hub",
%{"system_id" => solar_system_id} = _event,
%{
assigns: %{
map_id: map_id,
current_user: current_user,
main_character_id: main_character_id,
has_tracked_characters?: true,
user_permissions: %{update_system: true}
}
} =
socket
)
when not is_nil(main_character_id) do
map_id
|> WandererApp.Map.Server.remove_hub(%{
solar_system_id: solar_system_id
})
{:ok, _} =
WandererApp.User.ActivityTracker.track_map_event(:hub_removed, %{
character_id: main_character_id,
user_id: current_user.id,
map_id: map_id,
solar_system_id: solar_system_id
})
{:noreply, socket}
end
def handle_ui_event(
"get_user_hubs",
_event,
%{
assigns: %{
map_id: map_id,
current_user: current_user
}
} =
socket
) do
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
{:reply, %{hubs: hubs}, socket}
end
def handle_ui_event(
"add_user_hub",
%{"system_id" => solar_system_id} = _event,
%{
assigns: %{
map_id: map_id,
current_user: current_user
}
} =
socket
) do
{:ok, map} = map_id |> WandererApp.Map.get_map()
hubs_limit = map |> Map.get(:hubs_limit, 20)
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
if hubs |> Enum.count() < hubs_limit do
hubs = hubs ++ ["#{solar_system_id}"]
{:ok, _} =
WandererApp.MapUserSettingsRepo.update_hubs(
map_id,
current_user.id,
hubs
)
{:noreply,
socket
|> MapEventHandler.push_map_event(
"map_updated",
%{user_hubs: hubs}
)}
else
{:noreply,
socket
|> MapEventHandler.push_map_event(
"map_updated",
%{user_hubs: hubs}
)
|> put_flash(
:warning,
"The user hubs limit has been reached, please try to remove some user hubs first, or contact the map administrators."
)}
end
end
def handle_ui_event(
"delete_user_hub",
%{"system_id" => solar_system_id} = _event,
%{
assigns: %{
map_id: map_id,
current_user: current_user
}
} =
socket
) do
{:ok, hubs} = WandererApp.MapUserSettingsRepo.get_hubs(map_id, current_user.id)
case hubs |> Enum.member?("#{solar_system_id}") do
true ->
hubs = hubs |> Enum.reject(fn hub -> hub == "#{solar_system_id}" end)
{:ok, _} =
WandererApp.MapUserSettingsRepo.update_hubs(
map_id,
current_user.id,
hubs
)
{:noreply,
socket
|> MapEventHandler.push_map_event(
"map_updated",
%{user_hubs: hubs}
)}
_ ->
{:noreply,
socket
|> MapEventHandler.push_map_event(
"map_updated",
%{user_hubs: hubs}
)}
end
end
def handle_ui_event(
"set_autopilot_waypoint",
%{

View File

@@ -211,10 +211,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
})
end
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
event: :signatures_updated,
payload: solar_system_source
})
WandererApp.Map.Server.Impl.broadcast!(map_id, :signatures_updated, solar_system_source)
{:noreply, socket}
@@ -261,10 +258,7 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
})
end)
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
event: :signatures_updated,
payload: solar_system_source
})
WandererApp.Map.Server.Impl.broadcast!(map_id, :signatures_updated, solar_system_source)
{:noreply, socket}
@@ -273,6 +267,39 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
end
end
def handle_ui_event(
"undo_delete_signatures",
%{"system_id" => solar_system_id, "eve_ids" => eve_ids} = payload,
%{
assigns: %{
map_id: map_id,
main_character_id: main_character_id,
user_permissions: %{update_system: true}
}
} = socket
)
when not is_nil(main_character_id) do
case WandererApp.Api.MapSystem.read_by_map_and_solar_system(%{
map_id: map_id,
solar_system_id: get_integer(solar_system_id)
}) do
{:ok, system} ->
restored =
WandererApp.Api.MapSystemSignature.by_system_id_all!(system.id)
|> Enum.filter(fn s -> s.eve_id in eve_ids end)
|> Enum.map(fn s ->
s |> WandererApp.Api.MapSystemSignature.update!(%{deleted: false})
end)
Phoenix.PubSub.broadcast!(WandererApp.PubSub, map_id, %{
event: :signatures_updated,
payload: system.solar_system_id
})
{:noreply, socket}
_ ->
{:noreply, socket}
end
end
def handle_ui_event(event, body, socket),
do: MapCoreEventHandler.handle_ui_event(event, body, socket)

View File

@@ -84,11 +84,7 @@ defmodule WandererAppWeb.MapCharactersLive do
character_setting ->
case character_setting.tracked do
true ->
{:ok, map_character_settings} =
character_setting
|> WandererApp.MapCharacterSettingsRepo.untrack()
WandererApp.Map.Server.remove_character(map_id, map_character_settings.character_id)
WandererApp.Map.Server.untrack_characters(map_id, [character_setting.character_id])
socket |> put_flash(:info, "Character untracked!") |> load_characters()

View File

@@ -23,7 +23,8 @@ defmodule WandererAppWeb.MapEventHandler do
:characters_updated,
:present_characters_updated,
:refresh_user_characters,
:show_tracking
:show_tracking,
:untrack_character
]
@map_characters_ui_events [
@@ -42,8 +43,6 @@ defmodule WandererAppWeb.MapEventHandler do
]
@map_system_ui_events [
"add_hub",
"delete_hub",
"delete_systems",
"get_system_static_infos",
"manual_add_system",
@@ -56,10 +55,7 @@ defmodule WandererAppWeb.MapEventHandler do
"update_system_locked",
"update_system_tag",
"update_system_temporary_name",
"update_system_status",
"get_user_hubs",
"add_user_hub",
"delete_user_hub"
"update_system_status"
]
@map_system_comments_events [
@@ -108,7 +104,12 @@ defmodule WandererAppWeb.MapEventHandler do
@map_routes_ui_events [
"get_routes",
"get_user_routes",
"set_autopilot_waypoint"
"set_autopilot_waypoint",
"add_hub",
"delete_hub",
"get_user_hubs",
"add_user_hub",
"delete_user_hub"
]
@map_signatures_events [
@@ -121,7 +122,8 @@ defmodule WandererAppWeb.MapEventHandler do
"update_signatures",
"get_signatures",
"link_signature_to_system",
"unlink_signature"
"unlink_signature",
"undo_delete_signatures"
]
@map_structures_events [

View File

@@ -4,6 +4,8 @@ defmodule WandererAppWeb.MapLive do
require Logger
@server_event_unsync_timeout :timer.minutes(2)
@impl true
def mount(%{"slug" => map_slug} = _params, _session, socket) when is_connected?(socket) do
Process.send_after(self(), %{event: :load_map}, Enum.random(10..800))
@@ -93,6 +95,19 @@ defmodule WandererAppWeb.MapLive do
)
|> push_navigate(to: ~p"/tracking/#{map_slug}")}
@impl true
def handle_info(%{timestamp: timestamp} = info, %{assigns: %{map_slug: map_slug}} = socket) do
duration = DateTime.diff(DateTime.utc_now(), timestamp, :millisecond)
if duration > @server_event_unsync_timeout do
{:noreply, socket |> push_navigate(to: ~p"/#{map_slug}")}
else
{:noreply,
socket
|> WandererAppWeb.MapEventHandler.handle_event(info)}
end
end
@impl true
def handle_info(info, socket),
do:

View File

@@ -212,7 +212,7 @@ defmodule WandererAppWeb.Router do
get "/system", MapSystemAPIController, :show_system
get "/connections", MapConnectionAPIController, :list_all_connections
get "/characters", MapAPIController, :list_tracked_characters
get "/structure-timers", MapSystemStructureAPIController, :structure_timers
get "/structure-timers", MapAPIController, :show_structure_timers
get "/character-activity", MapAPIController, :character_activity
get "/user_characters", MapAPIController, :user_characters

View File

@@ -38,14 +38,23 @@ defmodule WandererAppWeb.Schemas.ApiSchemas do
%Schema{
type: :object,
properties: %{
eve_id: %Schema{type: :string},
name: %Schema{type: :string},
corporation_id: %Schema{type: :string},
corporation_ticker: %Schema{type: :string},
alliance_id: %Schema{type: :string},
alliance_ticker: %Schema{type: :string}
id: %Schema{type: :string, description: "Character UUID"},
eve_id: %Schema{type: :string, description: "EVE Online character ID"},
name: %Schema{type: :string, description: "Character name"},
online: %Schema{type: :boolean, description: "Online status"},
corporation_id: %Schema{type: :integer, description: "Corporation ID"},
corporation_name: %Schema{type: :string, description: "Corporation name"},
corporation_ticker: %Schema{type: :string, description: "Corporation ticker"},
alliance_id: %Schema{type: :integer, description: "Alliance ID"},
alliance_name: %Schema{type: :string, description: "Alliance name"},
alliance_ticker: %Schema{type: :string, description: "Alliance ticker"},
solar_system_id: %Schema{type: :integer, description: "Current solar system ID"},
ship: %Schema{type: :integer, description: "Current ship type ID"},
ship_name: %Schema{type: :string, description: "Current ship name"},
inserted_at: %Schema{type: :string, format: :date_time, description: "Creation timestamp"},
updated_at: %Schema{type: :string, format: :date_time, description: "Last update timestamp"}
},
required: ["eve_id", "name"]
required: ~w(eve_id name)a
}
end

View File

@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.64.2"
@version "1.65.7"
def project do
[

View File

@@ -0,0 +1,70 @@
%{
title: "Instant Signature Deletion & Undo: A New Flow for Map Signatures",
author: "Wanderer Team",
cover_image_uri: "/images/news/05-08-undo/undo.png",
tags: ~w(signatures deletion undo map realtime guide),
description: "Learn about the new instant signature deletion flow, real-time updates, and the ability to undo removals in Wanderer maps."
}
---
### Introduction
Managing cosmic signatures is a core part of mapping and navigation in EVE Online. With our latest update, signature deletion is now **instant, real-time, and reversible**—making it easier than ever to keep your map up to date and error-free.
This guide covers the new signature deletion flow, how to use the undo feature, and what happens behind the scenes to keep your map clean and synchronized for all users.
---
### 1. The New User Flow: Instant, Real-Time, Reversible
- **Delete a signature:** When you remove a signature, it disappears from your map (and all other users' maps) instantly after a server roundtrip.
- **Undo:** If you make a mistake, you have a window of up to 30s to undo the deletion
---
### 2. How to Use the New Signature Deletion Flow
1. **Select and Delete:**
- Open the system signatures widget.
- Select one or more signatures and click delete (or paste and use lazy delete).
- The signatures will disappear for all users viewing the same system.
2. **Undo a Deletion:**
- After deleting, an **Undo** button appears for you (the user who deleted the signature) and remains visible based on your timeout settings.
- Click **Undo** to restore the removed signatures instantly for all users.
- If you don't click Undo in time, the deletion becomes permanent
3. **Real-Time Updates:**
- All users see signature changes (add, update, remove, undo) in real time—no need to refresh.
---
### 4. FAQ & Troubleshooting
**Q: Who sees the Undo button?**
- Only the user who deleted the signature sees the Undo button
**Q: Do all users see the same signature list in real time?**
- Yes! All changes are broadcast instantly to everyone viewing the same map.
**Q: Can I configure the undo timeout?**
- Yes, in the user inteface settings for the signatures widget
**Q: What about performance?**
- The new flow is optimized for real-time collaboration and efficient cleanup, ensuring your map stays fast and accurate.
---
### 5. Summary
The new signature deletion flow brings instant, real-time updates and a safety net for accidental removals. Enjoy a more collaborative, error-resistant mapping experience—now live for all Wanderer users!
---
Fly safe,
**The Wanderer Team**
---

View File

@@ -0,0 +1,23 @@
defmodule WandererApp.Repo.Migrations.AddDeletedSignature 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
remove :updated
add :deleted, :boolean, null: false, default: false
end
end
def down do
alter table(:map_system_signatures_v1) do
remove :deleted
add :updated, :bigint
end
end
end

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.AddSigUpdateForcedAt 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 :update_forced_at, :utc_datetime
end
end
def down do
alter table(:map_system_signatures_v1) do
remove :update_forced_at
end
end
end

View File

@@ -0,0 +1,31 @@
defmodule WandererApp.Repo.Migrations.AddMapCharacterTrackingInfo 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_character_settings_v1) do
add :encrypted_ship, :binary
add :encrypted_ship_name, :binary
add :encrypted_ship_item_id, :binary
add :encrypted_solar_system_id, :binary
add :encrypted_structure_id, :binary
add :encrypted_station_id, :binary
end
end
def down do
alter table(:map_character_settings_v1) do
remove :encrypted_station_id
remove :encrypted_structure_id
remove :encrypted_solar_system_id
remove :encrypted_ship_item_id
remove :encrypted_ship_name
remove :encrypted_ship
end
end
end

View File

@@ -0,0 +1,206 @@
{
"attributes": [
{
"allow_nil?": false,
"default": "fragment(\"gen_random_uuid()\")",
"generated?": false,
"primary_key?": true,
"references": null,
"size": null,
"source": "id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "tracked",
"type": "boolean"
},
{
"allow_nil?": true,
"default": "false",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "followed",
"type": "boolean"
},
{
"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?": false,
"default": "nil",
"generated?": false,
"primary_key?": true,
"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_character_settings_v1_map_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "maps_v1"
},
"size": null,
"source": "map_id",
"type": "uuid"
},
{
"allow_nil?": false,
"default": "nil",
"generated?": false,
"primary_key?": true,
"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_character_settings_v1_character_id_fkey",
"on_delete": null,
"on_update": null,
"primary_key?": true,
"schema": "public",
"table": "character_v1"
},
"size": null,
"source": "character_id",
"type": "uuid"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_ship",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_ship_name",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_ship_item_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_solar_system_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_structure_id",
"type": "binary"
},
{
"allow_nil?": true,
"default": "nil",
"generated?": false,
"primary_key?": false,
"references": null,
"size": null,
"source": "encrypted_station_id",
"type": "binary"
}
],
"base_filter": null,
"check_constraints": [],
"custom_indexes": [],
"custom_statements": [],
"has_create_action": true,
"hash": "3F585D1C545A5264AEFA05502C0F625F9B27B15CA36699DCF37E4F834E6339AE",
"identities": [
{
"all_tenants?": false,
"base_filter": null,
"index_name": "map_character_settings_v1_uniq_map_character_index",
"keys": [
{
"type": "atom",
"value": "map_id"
},
{
"type": "atom",
"value": "character_id"
}
],
"name": "uniq_map_character",
"nils_distinct?": true,
"where": null
}
],
"multitenancy": {
"attribute": null,
"global": null,
"strategy": null
},
"repo": "Elixir.WandererApp.Repo",
"schema": null,
"table": "map_character_settings_v1"
}

View File

@@ -0,0 +1,197 @@
{
"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": "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?": 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": "63D26445C9E67459C4D41CF31D61C3EE2356BE664F0D44AB5BC04C2100B701F3",
"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"
}

View File

@@ -0,0 +1,207 @@
{
"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": "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": "915C0896211ECCB6C38871664117E7D470C794825536E7F0887DC5B92681F17B",
"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"
}