mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-11-02 23:47:04 +00:00
Compare commits
186 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f98e979a2 | ||
|
|
e0f46c4af7 | ||
|
|
bc8a9a2b85 | ||
|
|
c2b03f925d | ||
|
|
efa2e52054 | ||
|
|
cedf5761f8 | ||
|
|
f601bb8751 | ||
|
|
c39d2a56d2 | ||
|
|
805722bbe8 | ||
|
|
fe3e38343b | ||
|
|
616e82c497 | ||
|
|
71202a4a29 | ||
|
|
a7e0ceac4c | ||
|
|
6bce701aab | ||
|
|
f8b9e206a5 | ||
|
|
4c1ec2004b | ||
|
|
ebed74d239 | ||
|
|
24c32511d8 | ||
|
|
06e7b6e3eb | ||
|
|
dec82e89c2 | ||
|
|
f5ac5bc4ec | ||
|
|
b6c680e802 | ||
|
|
5fa57c13b4 | ||
|
|
acc81fda44 | ||
|
|
7ab5acf45f | ||
|
|
0d4ffbcc22 | ||
|
|
a9253ac2df | ||
|
|
d00b4843a7 | ||
|
|
6068de2c71 | ||
|
|
73da427c6b | ||
|
|
9b7ec0ddfe | ||
|
|
c2f5f14c44 | ||
|
|
0b7c3588d5 | ||
|
|
a51fac5736 | ||
|
|
726c3d0704 | ||
|
|
8dd564dbd0 | ||
|
|
e33c65cddc | ||
|
|
f2fbd2ead0 | ||
|
|
123a2e45eb | ||
|
|
f8d2d9c680 | ||
|
|
9dcbef9a79 | ||
|
|
0b14857a12 | ||
|
|
bd3d516f60 | ||
|
|
40d0bd8cea | ||
|
|
968deeb254 | ||
|
|
959041be52 | ||
|
|
3319520179 | ||
|
|
580fcf3657 | ||
|
|
53dae7c520 | ||
|
|
6d59d709f1 | ||
|
|
4343e9070c | ||
|
|
b62373fb5f | ||
|
|
3da98f8e56 | ||
|
|
494d24952e | ||
|
|
8a6b17bd7b | ||
|
|
d2e859a74e | ||
|
|
4a78d55d22 | ||
|
|
dc252b8c4b | ||
|
|
c433205e89 | ||
|
|
d6bc5b57b1 | ||
|
|
280a286266 | ||
|
|
d5c18b5de3 | ||
|
|
7452e5d011 | ||
|
|
71674b0d52 | ||
|
|
5b4824bd5d | ||
|
|
deda16a7da | ||
|
|
0b7c067de7 | ||
|
|
0d0db8c129 | ||
|
|
9f1b7994a3 | ||
|
|
378df0ac70 | ||
|
|
0e4a132f69 | ||
|
|
631746375d | ||
|
|
7dc01dad54 | ||
|
|
8a9807d3e5 | ||
|
|
39df3c97ce | ||
|
|
46c1ccdfcc | ||
|
|
8817536038 | ||
|
|
c3bb23a6ee | ||
|
|
7e9c4c575e | ||
|
|
5a70eee91e | ||
|
|
228f6990a1 | ||
|
|
d80ed0e70e | ||
|
|
4576c75737 | ||
|
|
67764faaa7 | ||
|
|
91dd0b27ae | ||
|
|
99dcf49fbc | ||
|
|
6fb3edbfd6 | ||
|
|
26f13ce857 | ||
|
|
e9b475c0a8 | ||
|
|
7752010092 | ||
|
|
d3705b3ed7 | ||
|
|
1394e2897e | ||
|
|
5117a1c5af | ||
|
|
3c62403f33 | ||
|
|
a4760f5162 | ||
|
|
b071070431 | ||
|
|
3bcb9628e7 | ||
|
|
e62c4cf5bf | ||
|
|
af46962ce4 | ||
|
|
0b0967830b | ||
|
|
172251a208 | ||
|
|
8a6fb63d55 | ||
|
|
9652959e5e | ||
|
|
825ef46d41 | ||
|
|
ad9f7c6b95 | ||
|
|
b960b5c149 | ||
|
|
0f092d21f9 | ||
|
|
031576caa6 | ||
|
|
7a97a96c42 | ||
|
|
2efb2daba0 | ||
|
|
4374c39924 | ||
|
|
15711495c7 | ||
|
|
236f803427 | ||
|
|
6772130f2a | ||
|
|
ddd72f3fac | ||
|
|
6e262835ef | ||
|
|
2f3b8ddc5f | ||
|
|
cea3a74b34 | ||
|
|
867941a233 | ||
|
|
3ff388a16d | ||
|
|
f4248e9ab9 | ||
|
|
507b3289c7 | ||
|
|
9e1dfc48d5 | ||
|
|
518cbc7b5d | ||
|
|
ccc8db0620 | ||
|
|
7cfb663efd | ||
|
|
e5103cc925 | ||
|
|
26458f5a19 | ||
|
|
79d5ec6caf | ||
|
|
034d461ab6 | ||
|
|
2e9c1c170c | ||
|
|
24ad3b2c61 | ||
|
|
288f55dc2f | ||
|
|
78dbea6267 | ||
|
|
6a9e53141d | ||
|
|
05e6994520 | ||
|
|
1a4dc67eb9 | ||
|
|
31d87a116b | ||
|
|
c47796d590 | ||
|
|
c7138a41ee | ||
|
|
96f04c70a9 | ||
|
|
87a8bc09ab | ||
|
|
5f5661d559 | ||
|
|
35ca87790e | ||
|
|
ae43e4a57c | ||
|
|
b91712a01a | ||
|
|
b20007b341 | ||
|
|
6a24e1188b | ||
|
|
5894efc1aa | ||
|
|
a05612d243 | ||
|
|
48de874d6b | ||
|
|
91e6da316f | ||
|
|
fa60bd81a1 | ||
|
|
a08a69c5be | ||
|
|
18d450a41a | ||
|
|
36cdee61c0 | ||
|
|
797e188259 | ||
|
|
91b581668a | ||
|
|
ad01fec28f | ||
|
|
357d3a0df6 | ||
|
|
5ce6022761 | ||
|
|
235a0c5aea | ||
|
|
9b81fa6ebb | ||
|
|
8792d5ab0e | ||
|
|
d46ed0c078 | ||
|
|
73c433fcd2 | ||
|
|
02b5239220 | ||
|
|
0ed3bdfcb0 | ||
|
|
bdeb89011f | ||
|
|
1523b625bc | ||
|
|
fb91eeb692 | ||
|
|
601d2e02cb | ||
|
|
0a662d34eb | ||
|
|
5cd4693e9d | ||
|
|
f3f0f860e3 | ||
|
|
93a5cf8a79 | ||
|
|
7cf15cbc21 | ||
|
|
30bc6d20b2 | ||
|
|
b39f99fde4 | ||
|
|
0e8aa9efa4 | ||
|
|
e1fcde36e3 | ||
|
|
7aafe077d3 | ||
|
|
74f7ad155d | ||
|
|
f58ebad0ec | ||
|
|
7ca4eb3b8f | ||
|
|
854524a03c |
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
@@ -53,6 +53,7 @@ jobs:
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
ssh-key: "${{ secrets.COMMIT_KEY }}"
|
||||
fetch-depth: 0
|
||||
- name: 😅 Cache deps
|
||||
id: cache-deps
|
||||
@@ -95,6 +96,7 @@ jobs:
|
||||
git config --global user.name 'CI'
|
||||
git config --global user.email 'ci@users.noreply.github.com'
|
||||
mix git_ops.release --force-patch --yes
|
||||
git commit --allow-empty -m 'chore: [skip ci]'
|
||||
git push --follow-tags
|
||||
echo "commit_hash=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
|
||||
|
||||
|
||||
13
.github/workflows/docker-arm.yml
vendored
13
.github/workflows/docker-arm.yml
vendored
@@ -3,7 +3,7 @@ name: Build Docker ARM Image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- '**'
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
@@ -43,6 +43,11 @@ jobs:
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
@@ -118,11 +123,9 @@ jobs:
|
||||
id: get-content
|
||||
with:
|
||||
stringToTruncate: |
|
||||
📣 Wanderer **ARM** release available 🎉
|
||||
📣 Wanderer **ARM** release available 🎉
|
||||
|
||||
[wandererltd/community-edition-arm:${{ steps.get-latest-tag.outputs.tag }}](https://hub.docker.com/r/wandererltd/community-edition-arm/tags)
|
||||
|
||||
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
**Version**: :${{ steps.get-latest-tag.outputs.tag }}
|
||||
|
||||
${{ steps.extract-changelog.outputs.body }}
|
||||
maxLength: 500
|
||||
|
||||
9
.github/workflows/docker.yml
vendored
9
.github/workflows/docker.yml
vendored
@@ -3,7 +3,7 @@ name: Build Docker Image
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- '**'
|
||||
|
||||
env:
|
||||
MIX_ENV: prod
|
||||
@@ -43,6 +43,11 @@ jobs:
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: ⬇️ Checkout repo
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get Release Tag
|
||||
id: get-latest-tag
|
||||
uses: "WyriHaximus/github-action-get-previous-tag@v1"
|
||||
@@ -120,8 +125,6 @@ jobs:
|
||||
stringToTruncate: |
|
||||
📣 Wanderer new release available 🎉
|
||||
|
||||
[wandererltd/community-edition:${{ steps.get-latest-tag.outputs.tag }}](https://hub.docker.com/r/wandererltd/community-edition/tags)
|
||||
|
||||
**Version**: ${{ steps.get-latest-tag.outputs.tag }}
|
||||
|
||||
${{ steps.extract-changelog.outputs.body }}
|
||||
|
||||
1990
CHANGELOG.md
1990
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -21,21 +21,17 @@ RUN mkdir config
|
||||
# to ensure any relevant config change will trigger the dependencies
|
||||
# to be re-compiled.
|
||||
COPY config/config.exs config/${MIX_ENV}.exs config/
|
||||
|
||||
COPY priv priv
|
||||
|
||||
COPY lib lib
|
||||
|
||||
COPY assets assets
|
||||
|
||||
RUN mix assets.deploy
|
||||
|
||||
RUN mix compile
|
||||
|
||||
# Changes to config/runtime.exs don't require recompiling the code
|
||||
COPY config/runtime.exs config/
|
||||
|
||||
COPY rel rel
|
||||
|
||||
RUN mix release
|
||||
|
||||
# start a new build stage so that the final image will only contain
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
// import './tailwind.css';
|
||||
//@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css';
|
||||
//@import 'primereact/resources/themes/lara-dark-purple/theme.css';
|
||||
//@import "prime-fixes";
|
||||
@import 'primereact/resources/primereact.min.css';
|
||||
//@import 'primeflex/primeflex.css';
|
||||
@import 'primeicons/primeicons.css';
|
||||
//@import 'primereact/resources/primereact.css';
|
||||
@use 'primereact/resources/primereact.min.css';
|
||||
@use 'primeicons/primeicons.css';
|
||||
|
||||
|
||||
@import "fixes";
|
||||
@import "prime-fixes";
|
||||
@import "custom-scrollbar";
|
||||
@import "tooltip";
|
||||
@import "context-menu";
|
||||
@use "fixes";
|
||||
@use "prime-fixes";
|
||||
@use "custom-scrollbar";
|
||||
@use "tooltip";
|
||||
@use "context-menu";
|
||||
|
||||
|
||||
.fixedImportant {
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
@import "fix-dialog";
|
||||
@import "fix-popup";
|
||||
@import "fix-tabs";
|
||||
//@import "fix-input";
|
||||
|
||||
//@import "theme";
|
||||
@use "fix-dialog";
|
||||
@use "fix-popup";
|
||||
@use "fix-tabs";
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
import clsx from 'clsx';
|
||||
import { ForwardedRef, forwardRef, MouseEvent, useCallback, useEffect, useMemo } from 'react';
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
@@ -16,8 +23,6 @@ import ReactFlow, {
|
||||
import 'reactflow/dist/style.css';
|
||||
import classes from './Map.module.scss';
|
||||
import { MapProvider, useMapState } from './MapProvider';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { MapHandlers, OutCommand, OutCommandHandler } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import {
|
||||
ContextMenuConnection,
|
||||
ContextMenuRoot,
|
||||
@@ -26,14 +31,9 @@ import {
|
||||
useContextMenuRootHandlers,
|
||||
} from './components';
|
||||
import { getBehaviorForTheme } from './helpers/getThemeBehavior';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
import { SESSION_KEY } from '@/hooks/Mapper/constants.ts';
|
||||
import { PingData, SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { ctxManager } from '@/hooks/Mapper/utils/contextManager.ts';
|
||||
import { NodeSelectionMouseHandler } from '@/hooks/Mapper/components/contexts/types.ts';
|
||||
import clsx from 'clsx';
|
||||
import { useEdgesState, useMapHandlers, useNodesState, useUpdateNodes } from './hooks';
|
||||
import { useBackgroundVars } from './hooks/useBackgroundVars';
|
||||
import type { PanelPosition } from '@reactflow/core';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from './map.types';
|
||||
|
||||
const DEFAULT_VIEW_PORT = { zoom: 1, x: 0, y: 0 };
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.ConnectionTimeEOL {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-time-eol));
|
||||
@@ -8,6 +8,10 @@
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-frigate));
|
||||
}
|
||||
|
||||
.ConnectionBridge {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-bridge));
|
||||
}
|
||||
|
||||
.ConnectionSave {
|
||||
background-image: linear-gradient(207deg, transparent, var(--conn-save));
|
||||
}
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import classes from './ContextMenuConnection.module.scss';
|
||||
import {
|
||||
MASS_STATE_NAMES,
|
||||
MASS_STATE_NAMES_ORDER,
|
||||
@@ -13,7 +6,16 @@ import {
|
||||
SHIP_SIZES_NAMES_SHORT,
|
||||
SHIP_SIZES_SIZE,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { ContextMenu } from 'primereact/contextmenu';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import { Edge } from 'reactflow';
|
||||
import classes from './ContextMenuConnection.module.scss';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { isNullsecSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
|
||||
|
||||
export interface ContextMenuConnectionProps {
|
||||
contextMenuRef: RefObject<ContextMenu>;
|
||||
@@ -21,6 +23,7 @@ export interface ContextMenuConnectionProps {
|
||||
onChangeTimeState(): void;
|
||||
onChangeMassState(state: MassState): void;
|
||||
onChangeShipSizeStatus(state: ShipSizeStatus): void;
|
||||
onChangeType(type: ConnectionType): void;
|
||||
onToggleMassSave(isLocked: boolean): void;
|
||||
onHide(): void;
|
||||
edge?: Edge<SolarSystemConnection>;
|
||||
@@ -32,6 +35,7 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
onChangeTimeState,
|
||||
onChangeMassState,
|
||||
onChangeShipSizeStatus,
|
||||
onChangeType,
|
||||
onToggleMassSave,
|
||||
onHide,
|
||||
edge,
|
||||
@@ -41,84 +45,127 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
return [];
|
||||
}
|
||||
|
||||
const sourceInfo = getSystemStaticInfo(edge.data?.source);
|
||||
const targetInfo = getSystemStaticInfo(edge.data?.target);
|
||||
|
||||
const bothNullsec =
|
||||
sourceInfo && targetInfo && isNullsecSpace(sourceInfo.system_class) && isNullsecSpace(targetInfo.system_class);
|
||||
|
||||
const isFrigateSize = edge.data?.ship_size_type === ShipSizeStatus.small;
|
||||
const isWormhole = edge.data?.type !== ConnectionType.gate;
|
||||
|
||||
if (edge.data?.type === ConnectionType.bridge) {
|
||||
return [
|
||||
{
|
||||
label: `Set as Wormhole`,
|
||||
icon: 'pi hero-arrow-uturn-left',
|
||||
command: () => onChangeType(ConnectionType.wormhole),
|
||||
},
|
||||
{
|
||||
label: 'Disconnect',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (edge.data?.type === ConnectionType.gate) {
|
||||
return [
|
||||
{
|
||||
label: 'Disconnect',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...(isWormhole
|
||||
{
|
||||
label: `EOL`,
|
||||
className: clsx({
|
||||
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
|
||||
}),
|
||||
icon: PrimeIcons.CLOCK,
|
||||
command: onChangeTimeState,
|
||||
},
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
className: clsx({
|
||||
[classes.ConnectionSave]: edge.data?.locked,
|
||||
}),
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: () => onToggleMassSave(!edge.data?.locked),
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
label: `EOL`,
|
||||
className: clsx({
|
||||
[classes.ConnectionTimeEOL]: edge.data?.time_status === TimeStatus.eol,
|
||||
}),
|
||||
icon: PrimeIcons.CLOCK,
|
||||
command: onChangeTimeState,
|
||||
},
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
className: clsx({
|
||||
[classes.ConnectionSave]: edge.data?.locked,
|
||||
}),
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: () => onToggleMassSave(!edge.data?.locked),
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
label: `Mass status`,
|
||||
icon: PrimeIcons.CHART_PIE,
|
||||
items: MASS_STATE_NAMES_ORDER.map(x => ({
|
||||
label: MASS_STATE_NAMES[x],
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.mass_status === x,
|
||||
}),
|
||||
command: () => onChangeMassState(x),
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
label: `Ship Size`,
|
||||
icon: PrimeIcons.CLOUD,
|
||||
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
|
||||
label: (
|
||||
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
|
||||
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
|
||||
<div>{SHIP_SIZES_NAMES[x]}</div>
|
||||
<div></div>
|
||||
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
|
||||
{SHIP_SIZES_SIZE[x]} t.
|
||||
</div>
|
||||
</div>
|
||||
) as unknown as string, // TODO my lovely kostyl
|
||||
label: `Mass status`,
|
||||
icon: PrimeIcons.CHART_PIE,
|
||||
items: MASS_STATE_NAMES_ORDER.map(x => ({
|
||||
label: MASS_STATE_NAMES[x],
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
[classes.SelectedItem]: edge.data?.mass_status === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
command: () => onChangeMassState(x),
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
label: `Ship Size`,
|
||||
icon: PrimeIcons.CLOUD,
|
||||
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
|
||||
label: (
|
||||
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
|
||||
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
|
||||
<div>{SHIP_SIZES_NAMES[x]}</div>
|
||||
<div></div>
|
||||
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
|
||||
{SHIP_SIZES_SIZE[x]} t.
|
||||
</div>
|
||||
</div>
|
||||
) as unknown as string, // TODO my lovely kostyl
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
})),
|
||||
},
|
||||
...(bothNullsec
|
||||
? [
|
||||
{
|
||||
label: `Set as Bridge`,
|
||||
icon: 'pi hero-forward',
|
||||
command: () => onChangeType(ConnectionType.bridge),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Disconnect',
|
||||
icon: PrimeIcons.TRASH,
|
||||
command: onDeleteConnection,
|
||||
},
|
||||
];
|
||||
}, [edge, onChangeTimeState, onDeleteConnection, onChangeShipSizeStatus, onToggleMassSave, onChangeMassState]);
|
||||
}, [
|
||||
edge,
|
||||
onChangeTimeState,
|
||||
onDeleteConnection,
|
||||
onChangeType,
|
||||
onChangeShipSizeStatus,
|
||||
onToggleMassSave,
|
||||
onChangeMassState,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -56,7 +56,8 @@ export const KillsCounter = ({
|
||||
className={className}
|
||||
tooltipClassName="!px-0"
|
||||
size={size}
|
||||
interactive={true}
|
||||
interactive
|
||||
smallPaddings
|
||||
>
|
||||
{children}
|
||||
</WdTooltipWrapper>
|
||||
|
||||
@@ -46,7 +46,13 @@ export const LocalCounter = ({ localCounterCharacters, hasUserCharacters, showIc
|
||||
[classes.Pathfinder]: theme === AvailableThemes.pathfinder,
|
||||
})}
|
||||
>
|
||||
<WdTooltipWrapper content={pilotTooltipContent} position={TooltipPosition.right} offset={0} interactive={true}>
|
||||
<WdTooltipWrapper
|
||||
content={pilotTooltipContent}
|
||||
position={TooltipPosition.right}
|
||||
offset={0}
|
||||
interactive={true}
|
||||
smallPaddings
|
||||
>
|
||||
<div className={clsx(classes.hoverTarget)}>
|
||||
<div
|
||||
className={clsx(classes.localCounter, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.EdgePathBack {
|
||||
fill: none;
|
||||
@@ -29,6 +29,13 @@
|
||||
&.Gate {
|
||||
stroke: #9aff40;
|
||||
}
|
||||
|
||||
&.Bridge {
|
||||
stroke: #9aff40;
|
||||
|
||||
stroke-dasharray: 10 5;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
}
|
||||
|
||||
.EdgePathFront {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { PrimeIcons } from 'primereact/api';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { SHIP_SIZES_DESCRIPTION, SHIP_SIZES_NAMES_SHORT } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
|
||||
const MAP_TRANSLATES: Record<string, string> = {
|
||||
[Position.Top]: 'translate(-48%, 0%)',
|
||||
@@ -42,7 +43,9 @@ export const SHIP_SIZES_COLORS = {
|
||||
export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }: EdgeProps<SolarSystemConnection>) => {
|
||||
const sourceNode = useStore(useCallback(store => store.nodeInternals.get(source), [source]));
|
||||
const targetNode = useStore(useCallback(store => store.nodeInternals.get(target), [target]));
|
||||
const isWormhole = data?.type !== ConnectionType.gate;
|
||||
const isWormhole = data?.type === ConnectionType.wormhole;
|
||||
const isGate = data?.type === ConnectionType.gate;
|
||||
const isBridge = data?.type === ConnectionType.bridge;
|
||||
|
||||
const {
|
||||
data: { isThickConnections },
|
||||
@@ -55,9 +58,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
|
||||
const offset = isThickConnections ? MAP_OFFSETS_TICK[targetPos] : MAP_OFFSETS[targetPos];
|
||||
|
||||
const method = isWormhole ? getBezierPath : getBezierPath;
|
||||
|
||||
const [edgePath, labelX, labelY] = method({
|
||||
const [edgePath, labelX, labelY] = getBezierPath({
|
||||
sourceX: sx - offset.x,
|
||||
sourceY: sy - offset.y,
|
||||
sourcePosition: sourcePos,
|
||||
@@ -67,7 +68,7 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
});
|
||||
|
||||
return [edgePath, labelX, labelY, sx, sy, tx, ty, sourcePos, targetPos];
|
||||
}, [isThickConnections, sourceNode, targetNode, isWormhole]);
|
||||
}, [isThickConnections, sourceNode, targetNode]);
|
||||
|
||||
if (!sourceNode || !targetNode || !data) {
|
||||
return null;
|
||||
@@ -81,7 +82,8 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
[classes.Tick]: isThickConnections,
|
||||
[classes.TimeCrit]: isWormhole && data.time_status === TimeStatus.eol,
|
||||
[classes.Hovered]: hovered,
|
||||
[classes.Gate]: !isWormhole,
|
||||
[classes.Gate]: isGate,
|
||||
[classes.Bridge]: isBridge,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
@@ -95,7 +97,8 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
[classes.MassVerge]: isWormhole && data.mass_status === MassState.verge,
|
||||
[classes.MassHalf]: isWormhole && data.mass_status === MassState.half,
|
||||
[classes.Frigate]: isWormhole && data.ship_size_type === ShipSizeStatus.small,
|
||||
[classes.Gate]: !isWormhole,
|
||||
[classes.Gate]: isGate,
|
||||
[classes.Bridge]: isBridge,
|
||||
})}
|
||||
d={path}
|
||||
markerEnd={markerEnd}
|
||||
@@ -147,6 +150,19 @@ export const SolarSystemEdge = ({ id, source, target, markerEnd, style, data }:
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{isBridge && (
|
||||
<WdTooltipWrapper
|
||||
content="Ansiblex Jump Bridge"
|
||||
position={TooltipPosition.top}
|
||||
className={clsx(
|
||||
classes.LinkLabel,
|
||||
'pointer-events-auto bg-lime-300 rounded opacity-100 cursor-auto text-neutral-900',
|
||||
)}
|
||||
>
|
||||
B
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{isWormhole && data.ship_size_type !== ShipSizeStatus.large && (
|
||||
<WdTooltipWrapper
|
||||
content={SHIP_SIZES_DESCRIPTION[data.ship_size_type]}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@use "sass:color";
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
$pastel-blue: #5a7d9a;
|
||||
$pastel-pink: rgb(30, 161, 255);
|
||||
@@ -34,7 +35,7 @@ $neon-color-3: rgba(27, 132, 236, 0.40);
|
||||
color: var(--rf-text-color, #ffffff);
|
||||
|
||||
box-shadow: 0 0 5px rgba($dark-bg, 0.5);
|
||||
border: 1px solid darken($pastel-blue, 10%);
|
||||
border: 1px solid color.adjust($pastel-blue, $lightness: -10%);
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
z-index: 3;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import './SolarSystemNodeDefault.module.scss';
|
||||
@use './SolarSystemNodeDefault.module.scss';
|
||||
|
||||
/* ---------------------------------------------
|
||||
Only override what's different from the base
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.Signature {
|
||||
position: relative;
|
||||
|
||||
@@ -58,6 +58,7 @@ export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) =>
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
}
|
||||
smallPaddings
|
||||
>
|
||||
<div className={clsx(classes.Box, whClassStyle)}>
|
||||
<svg width="13" height="8" viewBox="0 0 13 8" xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
@@ -716,11 +716,12 @@ export const STATUS_CLASSES: Record<number, string> = {
|
||||
[STATUSES.dangerous]: 'eve-system-status-dangerous',
|
||||
};
|
||||
|
||||
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate];
|
||||
export const TYPE_NAMES_ORDER = [ConnectionType.wormhole, ConnectionType.gate, ConnectionType.bridge];
|
||||
|
||||
export const TYPE_NAMES = {
|
||||
[ConnectionType.wormhole]: 'Wormhole',
|
||||
[ConnectionType.gate]: 'Gate',
|
||||
[ConnectionType.bridge]: 'Jumpgate',
|
||||
};
|
||||
|
||||
export const MASS_STATE_NAMES_ORDER = [MassState.verge, MassState.half, MassState.normal];
|
||||
|
||||
@@ -15,3 +15,12 @@ export const isKnownSpace = (wormholeClassID: number) => {
|
||||
export const isPossibleSpace = (spaces: number[], wormholeClassID: number) => {
|
||||
return spaces.includes(wormholeClassID);
|
||||
};
|
||||
|
||||
export const isNullsecSpace = (wormholeClassID: number) => {
|
||||
switch (wormholeClassID) {
|
||||
case SOLAR_SYSTEM_CLASS_IDS.ns:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -6,5 +6,5 @@ export * from './useCommandsCharacters';
|
||||
export * from './useCommandsConnections';
|
||||
export * from './useCommandsConnections';
|
||||
export * from './useCenterSystem';
|
||||
export * from './useSelectSystem';
|
||||
export * from './useSelectSystems';
|
||||
export * from './useMapCommands';
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { useEventBuffer } from '@/hooks/Mapper/hooks';
|
||||
import { SolarSystemConnection, SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
@@ -11,6 +13,20 @@ export const useMapInit = () => {
|
||||
const ref = useRef({ rf, data, update });
|
||||
ref.current = { update, data, rf };
|
||||
|
||||
const updateSystems = useCallback((systems: SolarSystemRawType[]) => {
|
||||
const { rf } = ref.current;
|
||||
rf.setNodes(systems.map(convertSystem2Node));
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleUpdateSystems } = useEventBuffer<any>(updateSystems);
|
||||
|
||||
const updateEdges = useCallback((connections: SolarSystemConnection[]) => {
|
||||
const { rf } = ref.current;
|
||||
rf.setEdges(connections.map(convertConnection2Edge));
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleUpdateConnections } = useEventBuffer<any>(updateEdges);
|
||||
|
||||
return useCallback(
|
||||
({
|
||||
systems,
|
||||
@@ -24,7 +40,6 @@ export const useMapInit = () => {
|
||||
hubs,
|
||||
}: CommandInit) => {
|
||||
const { update } = ref.current;
|
||||
const { rf } = ref.current;
|
||||
|
||||
const updateData: Partial<MapData> = {};
|
||||
|
||||
@@ -63,11 +78,13 @@ export const useMapInit = () => {
|
||||
update(updateData);
|
||||
|
||||
if (systems) {
|
||||
rf.setNodes(systems.map(convertSystem2Node));
|
||||
handleUpdateSystems(systems);
|
||||
// rf.setNodes(systems.map(convertSystem2Node));
|
||||
}
|
||||
|
||||
if (connections) {
|
||||
rf.setEdges(connections.map(convertConnection2Edge));
|
||||
handleUpdateConnections(connections);
|
||||
// rf.setEdges(connections.map(convertConnection2Edge));
|
||||
}
|
||||
},
|
||||
[],
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandSelectSystem } from '@/hooks/Mapper/types';
|
||||
|
||||
export const useSelectSystem = () => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
const ref = useRef({ rf });
|
||||
ref.current = { rf };
|
||||
|
||||
return useCallback((systemId: CommandSelectSystem) => {
|
||||
ref.current.rf.setNodes(nds =>
|
||||
nds.map(node => {
|
||||
return {
|
||||
...node,
|
||||
selected: node.id === systemId,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}, []);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
const ref = useRef({ rf, onSelectionChange });
|
||||
ref.current = { rf, onSelectionChange };
|
||||
|
||||
return useCallback(({ systems, delay }: CommandSelectSystems) => {
|
||||
const run = () => {
|
||||
ref.current.rf.setNodes(nds =>
|
||||
nds.map(node => {
|
||||
return {
|
||||
...node,
|
||||
selected: systems.includes(node.id),
|
||||
};
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
if (delay == null || delay === 0) {
|
||||
run();
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(run, delay);
|
||||
}, []);
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
|
||||
import {
|
||||
CommandAddConnections,
|
||||
CommandAddSystems,
|
||||
@@ -14,12 +13,16 @@ import {
|
||||
CommandRemoveSystems,
|
||||
Commands,
|
||||
CommandSelectSystem,
|
||||
CommandSelectSystems,
|
||||
CommandUpdateConnection,
|
||||
CommandUpdateSystems,
|
||||
MapHandlers,
|
||||
} from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
|
||||
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import {
|
||||
useCenterSystem,
|
||||
useCommandsCharacters,
|
||||
useCommandsConnections,
|
||||
useMapAddSystems,
|
||||
@@ -27,10 +30,8 @@ import {
|
||||
useMapInit,
|
||||
useMapRemoveSystems,
|
||||
useMapUpdateSystems,
|
||||
useCenterSystem,
|
||||
useSelectSystem,
|
||||
useSelectSystems,
|
||||
} from './api';
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
|
||||
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
|
||||
const mapInit = useMapInit();
|
||||
@@ -38,7 +39,7 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
const mapUpdateSystems = useMapUpdateSystems();
|
||||
const removeSystems = useMapRemoveSystems(onSelectionChange);
|
||||
const centerSystem = useCenterSystem();
|
||||
const selectSystem = useSelectSystem();
|
||||
const selectSystems = useSelectSystems(onSelectionChange);
|
||||
|
||||
const selectRef = useRef({ onSelectionChange });
|
||||
selectRef.current = { onSelectionChange };
|
||||
@@ -105,14 +106,11 @@ export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange
|
||||
break;
|
||||
|
||||
case Commands.selectSystem:
|
||||
setTimeout(() => {
|
||||
const systemId = `${data}`;
|
||||
selectRef.current.onSelectionChange({
|
||||
systems: [systemId],
|
||||
connections: [],
|
||||
});
|
||||
selectSystem(systemId as CommandSelectSystem);
|
||||
}, 500);
|
||||
selectSystems({ systems: [data as string], delay: 500 });
|
||||
break;
|
||||
|
||||
case Commands.selectSystems:
|
||||
selectSystems(data as CommandSelectSystems);
|
||||
break;
|
||||
|
||||
case Commands.pingAdded:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
|
||||
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
|
||||
import { SolarSystemRawType } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
|
||||
|
||||
const useThrottle = () => {
|
||||
const throttleSeed = useRef<number | null>(null);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@import './eve-common-variables';
|
||||
@import './eve-common';
|
||||
@use './eve-common-variables';
|
||||
@use './eve-common';
|
||||
|
||||
.default-theme {
|
||||
--rf-bg-color: #0C0A09;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
@use "sass:color";
|
||||
|
||||
$friendlyBase: #3bbd39;
|
||||
$friendlyAlpha: #3bbd3952;
|
||||
$friendlyDark20: darken($friendlyBase, 20%);
|
||||
$friendlyDark30: darken($friendlyBase, 30%);
|
||||
$friendlyDark5: darken($friendlyBase, 5%);
|
||||
$friendlyDark20: color.adjust($friendlyBase, $lightness: -20%);
|
||||
$friendlyDark30: color.adjust($friendlyBase, $lightness: -30%);
|
||||
$friendlyDark5: color.adjust($friendlyBase, $lightness: -5%);
|
||||
|
||||
$lookingForBase: #43c2fd;
|
||||
$lookingForAlpha: rgba(67, 176, 253, 0.48);
|
||||
$lookingForDark15: darken($lookingForBase, 15%);
|
||||
$lookingForDark15: color.adjust($lookingForBase, $lightness: -15%);
|
||||
|
||||
$homeBase: rgb(179, 253, 67);
|
||||
$homeAlpha: rgba(186, 248, 48, 0.32);
|
||||
$homeBackground: #a0fa5636;
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
$homeDark30: color.adjust($homeBase, $lightness: -30%);
|
||||
|
||||
:root {
|
||||
--pastel-blue: #5a7d9a;
|
||||
@@ -117,6 +118,7 @@ $homeDark30: darken($homeBase, 30%);
|
||||
|
||||
--conn-time-eol: #7452c3e3;
|
||||
--conn-frigate: #325d88;
|
||||
--conn-bridge: rgba(135, 185, 93, 0.85);
|
||||
--conn-save: rgba(155, 102, 45, 0.85);
|
||||
--selected-item-bg: rgba(98, 98, 98, 0.33);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@import './eve-common-variables';
|
||||
@use './eve-common-variables';
|
||||
|
||||
|
||||
.eve-wh-effect-color-pulsar {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
@import './default-theme.scss';
|
||||
@import './pathfinder-theme.scss';
|
||||
@use './default-theme.scss';
|
||||
@use './pathfinder-theme.scss';
|
||||
@@ -1,10 +1,11 @@
|
||||
@import './eve-common-variables';
|
||||
@import './eve-common';
|
||||
@use "sass:color";
|
||||
@use './eve-common-variables';
|
||||
@use './eve-common';
|
||||
@import url('https://fonts.googleapis.com/css2?family=Oxygen:wght@300;400;700&display=swap');
|
||||
|
||||
$homeBase: rgb(197, 253, 67);
|
||||
$homeAlpha: rgba(197, 253, 67, 0.32);
|
||||
$homeDark30: darken($homeBase, 30%);
|
||||
$homeDark30: color.adjust($homeBase, $lightness: -30%);
|
||||
|
||||
.pathfinder-theme {
|
||||
/* -- Override values from the default theme -- */
|
||||
|
||||
@@ -114,7 +114,7 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
<Dialog
|
||||
header="System settings"
|
||||
visible={visible}
|
||||
draggable={false}
|
||||
draggable={true}
|
||||
style={{ width: '450px' }}
|
||||
onShow={onShow}
|
||||
onHide={() => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { RoutesType } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { LoadRoutesCommand } from '@/hooks/Mapper/components/mapInterface/widgets/RoutesWidget/types.ts';
|
||||
import { RoutesList } from '@/hooks/Mapper/types/routes.ts';
|
||||
import { flattenValues } from '@/hooks/Mapper/utils/flattenValues.ts';
|
||||
|
||||
function usePrevious<T>(value: T): T | undefined {
|
||||
const ref = useRef<T>();
|
||||
@@ -64,12 +65,8 @@ export const useLoadRoutes = ({
|
||||
systems?.length,
|
||||
connections,
|
||||
hubs,
|
||||
routesSettings,
|
||||
...Object.keys(routesSettings)
|
||||
.sort()
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
.map(x => routesSettings[x]),
|
||||
// we need make it flat recursively
|
||||
...flattenValues(routesSettings),
|
||||
...deps,
|
||||
]);
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@ import {
|
||||
renderInfoColumn,
|
||||
renderUpdatedTimeLeft,
|
||||
} from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { useClipboard, useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { getSignatureRowClass } from '../helpers/rowStyles';
|
||||
import { useSystemSignaturesData } from '../hooks/useSystemSignaturesData';
|
||||
import { SETTINGS_KEYS, SIGNATURE_WINDOW_ID, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
const renderColIcon = (sig: SystemSignature) => renderIcon(sig);
|
||||
|
||||
@@ -157,9 +157,18 @@ export const SystemSignaturesContent = ({
|
||||
[onSelect, selectable, setSelectedSignatures, deletedSignatures],
|
||||
);
|
||||
|
||||
const { showDescriptionColumn, showUpdatedColumn, showCharacterColumn, showCharacterPortrait } = useMemo(
|
||||
const {
|
||||
showGroupColumn,
|
||||
showDescriptionColumn,
|
||||
showAddedColumn,
|
||||
showUpdatedColumn,
|
||||
showCharacterColumn,
|
||||
showCharacterPortrait,
|
||||
} = useMemo(
|
||||
() => ({
|
||||
showGroupColumn: settings[SETTINGS_KEYS.SHOW_GROUP_COLUMN] as boolean,
|
||||
showDescriptionColumn: settings[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN] as boolean,
|
||||
showAddedColumn: settings[SETTINGS_KEYS.SHOW_ADDED_COLUMN] as boolean,
|
||||
showUpdatedColumn: settings[SETTINGS_KEYS.SHOW_UPDATED_COLUMN] as boolean,
|
||||
showCharacterColumn: settings[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN] as boolean,
|
||||
showCharacterPortrait: settings[SETTINGS_KEYS.SHOW_CHARACTER_PORTRAIT] as boolean,
|
||||
@@ -309,15 +318,17 @@ export const SystemSignaturesContent = ({
|
||||
style={{ maxWidth: 72, minWidth: 72, width: 72 }}
|
||||
sortable
|
||||
/>
|
||||
<Column
|
||||
field="group"
|
||||
header="Group"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
|
||||
body={sig => sig.group ?? ''}
|
||||
hidden={isCompact}
|
||||
sortable
|
||||
/>
|
||||
{showGroupColumn && (
|
||||
<Column
|
||||
field="group"
|
||||
header="Group"
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
style={{ maxWidth: 110, minWidth: 110, width: 110 }}
|
||||
body={sig => sig.group ?? ''}
|
||||
hidden={isCompact}
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
<Column
|
||||
field="info"
|
||||
header="Info"
|
||||
@@ -336,15 +347,17 @@ export const SystemSignaturesContent = ({
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
<Column
|
||||
field="inserted_at"
|
||||
header="Added"
|
||||
dataType="date"
|
||||
body={renderAddedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
{showAddedColumn && (
|
||||
<Column
|
||||
field="inserted_at"
|
||||
header="Added"
|
||||
dataType="date"
|
||||
body={renderAddedTimeLeft}
|
||||
style={{ minWidth: 70, maxWidth: 80 }}
|
||||
bodyClassName="ssc-header text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
sortable
|
||||
/>
|
||||
)}
|
||||
{showUpdatedColumn && (
|
||||
<Column
|
||||
field="updated_at"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
import {
|
||||
GroupType,
|
||||
SignatureGroup,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
SignatureKindFR,
|
||||
SignatureKindRU,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import { SETTINGS_KEYS, SIGNATURES_DELETION_TIMING, SignatureSettingsType } from '@/hooks/Mapper/constants/signatures';
|
||||
|
||||
export const TIME_ONE_MINUTE = 1000 * 60;
|
||||
export const TIME_TEN_MINUTES = TIME_ONE_MINUTE * 10;
|
||||
@@ -130,6 +130,8 @@ export const SIGNATURE_SETTINGS = {
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.COMBAT_SITE, name: 'Show Combat Sites' },
|
||||
],
|
||||
uiFlags: [
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_GROUP_COLUMN, name: 'Show Group Column' },
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_ADDED_COLUMN, name: 'Show Added Column' },
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_UPDATED_COLUMN, name: 'Show Updated Column' },
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN, name: 'Show Description Column' },
|
||||
{ type: SettingsTypes.flag, key: SETTINGS_KEYS.SHOW_CHARACTER_COLUMN, name: 'Show Character Column' },
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import classes from './Connections.module.scss';
|
||||
import { Sidebar } from 'primereact/sidebar';
|
||||
import { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
ConnectionType,
|
||||
ConnectionOutput,
|
||||
ConnectionInfoOutput,
|
||||
ConnectionOutput,
|
||||
ConnectionType,
|
||||
OutCommand,
|
||||
Passage,
|
||||
SolarSystemConnection,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { Sidebar } from 'primereact/sidebar';
|
||||
import { VirtualScroller, VirtualScrollerTemplateOptions } from 'primereact/virtualscroller';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import classes from './Connections.module.scss';
|
||||
|
||||
import { PassageCard } from './PassageCard';
|
||||
import { InfoDrawer, SystemView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { InfoDrawer, SystemView, TimeAgo } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
|
||||
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PassageCard } from './PassageCard';
|
||||
|
||||
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
|
||||
|
||||
@@ -78,7 +77,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
}, [connections, selectedConnection]);
|
||||
|
||||
const isWormhole = useMemo(() => {
|
||||
return cnInfo?.type !== ConnectionType.gate;
|
||||
return cnInfo?.type === ConnectionType.wormhole;
|
||||
}, [cnInfo]);
|
||||
|
||||
const [passages, setPassages] = useState<Passage[]>([]);
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Button } from 'primereact/button';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import {
|
||||
DEFAULT_KILLS_WIDGET_SETTINGS,
|
||||
DEFAULT_ON_THE_MAP_SETTINGS,
|
||||
@@ -11,11 +9,13 @@ import {
|
||||
getDefaultWidgetProps,
|
||||
STORED_INTERFACE_DEFAULT_VALUES,
|
||||
} from '@/hooks/Mapper/mapRootProvider/constants.ts';
|
||||
import { DEFAULT_SIGNATURE_SETTINGS } from '@/hooks/Mapper/constants/signatures.ts';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { MapUserSettings } from '@/hooks/Mapper/mapRootProvider/types.ts';
|
||||
import { saveTextFile } from '@/hooks/Mapper/utils';
|
||||
import { useConfirmPopup } from '@/hooks/Mapper/hooks';
|
||||
import { Button } from 'primereact/button';
|
||||
import { ConfirmPopup } from 'primereact/confirmpopup';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Toast } from 'primereact/toast';
|
||||
import { useCallback, useRef } from 'react';
|
||||
|
||||
const createSettings = function <T>(lsSettings: string | null, defaultValues: T) {
|
||||
return {
|
||||
@@ -41,7 +41,7 @@ export const OldSettingsDialog = () => {
|
||||
const widgetKills = localStorage.getItem('kills:widget:settings');
|
||||
const onTheMapOld = localStorage.getItem('window:onTheMap:settings');
|
||||
const widgetsOld = localStorage.getItem('windows:settings:v2');
|
||||
const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_5');
|
||||
const signatures = localStorage.getItem('wanderer_system_signature_settings_v6_6');
|
||||
|
||||
const out: MapUserSettings = {
|
||||
killsWidget: createSettings(widgetKills, DEFAULT_KILLS_WIDGET_SETTINGS),
|
||||
@@ -118,7 +118,7 @@ export const OldSettingsDialog = () => {
|
||||
localStorage.removeItem('kills:widget:settings');
|
||||
localStorage.removeItem('window:onTheMap:settings');
|
||||
localStorage.removeItem('windows:settings:v2');
|
||||
localStorage.removeItem('wanderer_system_signature_settings_v6_5');
|
||||
localStorage.removeItem('wanderer_system_signature_settings_v6_6');
|
||||
|
||||
checkOldSettings();
|
||||
}, [checkOldSettings]);
|
||||
|
||||
@@ -94,6 +94,10 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
out = { ...out, type: values.type };
|
||||
}
|
||||
|
||||
if (values.temporary_name != null) {
|
||||
out = { ...out, temporary_name: values.temporary_name };
|
||||
}
|
||||
|
||||
if (signatureData.group !== SignatureGroup.Wormhole) {
|
||||
out = { ...out, name: '' };
|
||||
}
|
||||
@@ -181,7 +185,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
<Dialog
|
||||
header={`Signature Edit [${signatureData?.eve_id}]`}
|
||||
visible={show}
|
||||
draggable={false}
|
||||
draggable={true}
|
||||
style={{ width: '390px' }}
|
||||
onShow={handleShow}
|
||||
onHide={() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { SignatureWormholeTypeSelect } from '@/hooks/Mapper/components/mapRootCo
|
||||
import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
|
||||
import { SignatureEOLCheckbox } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureEOLCheckbox';
|
||||
import { SignatureTempName } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureTempName.tsx';
|
||||
|
||||
export const SignatureGroupContentWormholes = () => {
|
||||
const { watch } = useFormContext<SystemSignature>();
|
||||
@@ -32,6 +33,11 @@ export const SignatureGroupContentWormholes = () => {
|
||||
<span>EOL:</span>
|
||||
<SignatureEOLCheckbox name="isEOL" />
|
||||
</label>
|
||||
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>Temp. Name:</span>
|
||||
<SignatureTempName />
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types';
|
||||
|
||||
export const SignatureTempName = () => {
|
||||
const { control } = useFormContext<SystemSignature>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
name="temporary_name"
|
||||
control={control}
|
||||
render={({ field }) => <InputText placeholder="Temporary Name" value={field.value} onChange={field.onChange} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Map, MAP_ROOT_ID } from '@/hooks/Mapper/components/map/Map.tsx';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
|
||||
import { CommandSelectSystems, OutCommand, OutCommandHandler, SolarSystemConnection } from '@/hooks/Mapper/types';
|
||||
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OnMapAddSystemCallback, OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import isEqual from 'lodash.isequal';
|
||||
@@ -88,6 +88,18 @@ export const MapWrapper = () => {
|
||||
|
||||
useMapEventListener(event => {
|
||||
runCommand(event);
|
||||
|
||||
if (event.name === Commands.init) {
|
||||
const { selectedSystems } = ref.current;
|
||||
if (selectedSystems.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
runCommand({
|
||||
name: Commands.selectSystems,
|
||||
data: { systems: selectedSystems } as CommandSelectSystems,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const onSelectionChange: OnMapSelectionChange = useCallback(
|
||||
|
||||
@@ -18,6 +18,7 @@ export interface TooltipProps extends Omit<React.HTMLAttributes<HTMLDivElement>,
|
||||
content: (() => React.ReactNode) | React.ReactNode;
|
||||
targetSelector?: string;
|
||||
interactive?: boolean;
|
||||
smallPaddings?: boolean;
|
||||
}
|
||||
|
||||
export interface OffsetPosition {
|
||||
@@ -47,6 +48,7 @@ export const WdTooltip = forwardRef(
|
||||
position: tPosition = TooltipPosition.default,
|
||||
offset = 5,
|
||||
interactive = false,
|
||||
smallPaddings = false,
|
||||
className,
|
||||
...restProps
|
||||
}: TooltipProps,
|
||||
@@ -264,10 +266,14 @@ export const WdTooltip = forwardRef(
|
||||
ref={tooltipRef}
|
||||
className={clsx(
|
||||
classes.tooltip,
|
||||
interactive ? 'pointer-events-auto' : 'pointer-events-none',
|
||||
'absolute px-1 py-1',
|
||||
'absolute px-2 py-1',
|
||||
'border rounded-sm border-green-300 border-opacity-10 bg-stone-900 bg-opacity-90',
|
||||
pos == null && 'invisible',
|
||||
{
|
||||
'pointer-events-auto': interactive,
|
||||
'pointer-events-none': !interactive,
|
||||
invisible: pos == null,
|
||||
'!px-1': smallPaddings,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
|
||||
@@ -8,13 +8,26 @@ export type WdTooltipWrapperProps = {
|
||||
content?: (() => ReactNode) | ReactNode;
|
||||
size?: TooltipSize;
|
||||
interactive?: boolean;
|
||||
smallPaddings?: boolean;
|
||||
tooltipClassName?: string;
|
||||
} & Omit<HTMLProps<HTMLDivElement>, 'content' | 'size'> &
|
||||
Omit<TooltipProps, 'content'>;
|
||||
|
||||
export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperProps>(
|
||||
(
|
||||
{ className, children, content, offset, position, targetSelector, interactive, size, tooltipClassName, ...props },
|
||||
{
|
||||
className,
|
||||
children,
|
||||
content,
|
||||
offset,
|
||||
position,
|
||||
targetSelector,
|
||||
interactive,
|
||||
smallPaddings,
|
||||
size,
|
||||
tooltipClassName,
|
||||
...props
|
||||
},
|
||||
forwardedRef,
|
||||
) => {
|
||||
const suffix = useMemo(() => Math.random().toString(36).slice(2, 7), []);
|
||||
@@ -31,6 +44,7 @@ export const WdTooltipWrapper = forwardRef<WdTooltipHandlers, WdTooltipWrapperPr
|
||||
position={position}
|
||||
content={content}
|
||||
interactive={interactive}
|
||||
smallPaddings={smallPaddings}
|
||||
targetSelector={finalTargetSelector}
|
||||
className={clsx(size && sizeClass(size), tooltipClassName)}
|
||||
/>
|
||||
|
||||
@@ -12,14 +12,16 @@ export enum SETTINGS_KEYS {
|
||||
SORT_FIELD = 'sortField',
|
||||
SORT_ORDER = 'sortOrder',
|
||||
|
||||
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
|
||||
SHOW_UPDATED_COLUMN = 'show_updated_column',
|
||||
SHOW_ADDED_COLUMN = 'show_added_column',
|
||||
SHOW_CHARACTER_COLUMN = 'show_character_column',
|
||||
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
|
||||
SHOW_DESCRIPTION_COLUMN = 'show_description_column',
|
||||
SHOW_GROUP_COLUMN = 'show_group_column',
|
||||
SHOW_UPDATED_COLUMN = 'show_updated_column',
|
||||
LAZY_DELETE_SIGNATURES = 'lazy_delete_signatures',
|
||||
KEEP_LAZY_DELETE = 'keep_lazy_delete_enabled',
|
||||
DELETION_TIMING = 'deletion_timing',
|
||||
COLOR_BY_TYPE = 'color_by_type',
|
||||
SHOW_CHARACTER_PORTRAIT = 'show_character_portrait',
|
||||
|
||||
// From SignatureKind
|
||||
COSMIC_ANOMALY = SignatureKind.CosmicAnomaly,
|
||||
@@ -45,6 +47,8 @@ export const DEFAULT_SIGNATURE_SETTINGS: SignatureSettingsType = {
|
||||
[SETTINGS_KEYS.SORT_FIELD]: 'inserted_at',
|
||||
[SETTINGS_KEYS.SORT_ORDER]: -1,
|
||||
|
||||
[SETTINGS_KEYS.SHOW_GROUP_COLUMN]: true,
|
||||
[SETTINGS_KEYS.SHOW_ADDED_COLUMN]: true,
|
||||
[SETTINGS_KEYS.SHOW_UPDATED_COLUMN]: true,
|
||||
[SETTINGS_KEYS.SHOW_DESCRIPTION_COLUMN]: true,
|
||||
[SETTINGS_KEYS.SHOW_CHARACTER_COLUMN]: true,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export * from './useClipboard';
|
||||
export * from './useConfirmPopup';
|
||||
export * from './useEventBuffer';
|
||||
export * from './useHotkey';
|
||||
export * from './usePageVisibility';
|
||||
export * from './useSkipContextMenu';
|
||||
export * from './useThrottle';
|
||||
export * from './useConfirmPopup';
|
||||
|
||||
41
assets/js/hooks/Mapper/hooks/useEventBuffer.ts
Normal file
41
assets/js/hooks/Mapper/hooks/useEventBuffer.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import debounce from 'lodash.debounce';
|
||||
import { useCallback, useRef } from 'react';
|
||||
export type UseEventBufferHandler<T> = (event: T) => void;
|
||||
|
||||
export const useEventBuffer = <T>(handler: UseEventBufferHandler<T>) => {
|
||||
// @ts-ignore
|
||||
const eventsBufferRef = useRef<T[]>([]);
|
||||
|
||||
const eventTick = useCallback(
|
||||
debounce(() => {
|
||||
if (eventsBufferRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = eventsBufferRef.current.shift()!;
|
||||
handler(event);
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
|
||||
|
||||
if (eventsBufferRef.current.length > 0) {
|
||||
eventTick();
|
||||
}
|
||||
}, 10),
|
||||
[],
|
||||
);
|
||||
const eventTickRef = useRef(eventTick);
|
||||
eventTickRef.current = eventTick;
|
||||
|
||||
// @ts-ignore
|
||||
const handleEvent = useCallback(event => {
|
||||
if (!eventTickRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventsBufferRef.current.push(event);
|
||||
eventTickRef.current();
|
||||
}, []);
|
||||
|
||||
return { handleEvent };
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { CommandInit } from '@/hooks/Mapper/types';
|
||||
import { MapRootData, useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useLoadSystemStatic } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { CommandInit } from '@/hooks/Mapper/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useMapInit = () => {
|
||||
const { update } = useMapRootState();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
import {
|
||||
CommandAddConnections,
|
||||
CommandAddSystems,
|
||||
@@ -8,24 +7,25 @@ import {
|
||||
CommandCharactersUpdated,
|
||||
CommandCharacterUpdated,
|
||||
CommandCommentAdd,
|
||||
CommandCommentRemoved,
|
||||
CommandInit,
|
||||
CommandLinkSignatureToSystem,
|
||||
CommandMapUpdated,
|
||||
CommandPingAdded,
|
||||
CommandPingCancelled,
|
||||
CommandPresentCharacters,
|
||||
CommandRemoveConnections,
|
||||
CommandRemoveSystems,
|
||||
CommandRoutes,
|
||||
Commands,
|
||||
CommandSignaturesUpdated,
|
||||
CommandTrackingCharactersData,
|
||||
CommandUpdateConnection,
|
||||
CommandUpdateSystems,
|
||||
CommandUserSettingsUpdated,
|
||||
Commands,
|
||||
MapHandlers,
|
||||
CommandCommentRemoved,
|
||||
CommandPingAdded,
|
||||
CommandPingCancelled,
|
||||
} from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
|
||||
import {
|
||||
useCommandComments,
|
||||
@@ -39,9 +39,9 @@ import {
|
||||
useUserRoutes,
|
||||
} from './api';
|
||||
|
||||
import { useCommandsActivity } from './api/useCommandsActivity';
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { DetailedKill } from '../../types/kills';
|
||||
import { useCommandsActivity } from './api/useCommandsActivity';
|
||||
|
||||
export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapInit = useMapInit();
|
||||
@@ -63,127 +63,123 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const { pingAdded, pingCancelled } = useCommandPings();
|
||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
command(type, data) {
|
||||
switch (type) {
|
||||
case Commands.init: // USED
|
||||
mapInit(data as CommandInit);
|
||||
break;
|
||||
case Commands.addSystems: // USED
|
||||
addSystems(data as CommandAddSystems);
|
||||
break;
|
||||
case Commands.updateSystems: // USED
|
||||
updateSystems(data as CommandUpdateSystems);
|
||||
break;
|
||||
case Commands.removeSystems: // USED
|
||||
removeSystems(data as CommandRemoveSystems);
|
||||
break;
|
||||
case Commands.addConnections: // USED
|
||||
addConnections(data as CommandAddConnections);
|
||||
break;
|
||||
case Commands.removeConnections: // USED
|
||||
removeConnections(data as CommandRemoveConnections);
|
||||
break;
|
||||
case Commands.updateConnection: // USED
|
||||
updateConnection(data as CommandUpdateConnection);
|
||||
break;
|
||||
case Commands.charactersUpdated: // USED
|
||||
charactersUpdated(data as CommandCharactersUpdated);
|
||||
break;
|
||||
case Commands.characterAdded: // USED
|
||||
characterAdded(data as CommandCharacterAdded);
|
||||
break;
|
||||
case Commands.characterRemoved: // USED
|
||||
characterRemoved(data as CommandCharacterRemoved);
|
||||
break;
|
||||
case Commands.characterUpdated: // USED
|
||||
characterUpdated(data as CommandCharacterUpdated);
|
||||
break;
|
||||
case Commands.presentCharacters: // USED
|
||||
presentCharacters(data as CommandPresentCharacters);
|
||||
break;
|
||||
case Commands.mapUpdated: // USED
|
||||
mapUpdated(data as CommandMapUpdated);
|
||||
break;
|
||||
case Commands.routes:
|
||||
mapRoutes(data as CommandRoutes);
|
||||
break;
|
||||
case Commands.userRoutes:
|
||||
mapUserRoutes(data as CommandRoutes);
|
||||
break;
|
||||
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
case Commands.signaturesUpdated: // USED
|
||||
updateSystemSignatures(data as CommandSignaturesUpdated);
|
||||
break;
|
||||
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
case Commands.linkSignatureToSystem: // USED
|
||||
setTimeout(() => {
|
||||
updateLinkSignatureToSystem(data as CommandLinkSignatureToSystem);
|
||||
}, 200);
|
||||
break;
|
||||
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.centerSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.selectSystem: // USED
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
case Commands.killsUpdated:
|
||||
// do nothing here
|
||||
break;
|
||||
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
case Commands.detailedKillsUpdated:
|
||||
updateDetailedKills(data as Record<string, DetailedKill[]>);
|
||||
break;
|
||||
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
case Commands.characterActivityData:
|
||||
characterActivityData(data as CommandCharacterActivityData);
|
||||
break;
|
||||
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
case Commands.trackingCharactersData:
|
||||
trackingCharactersData(data as CommandTrackingCharactersData);
|
||||
break;
|
||||
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
case Commands.updateActivity:
|
||||
break;
|
||||
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
case Commands.updateTracking:
|
||||
break;
|
||||
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
case Commands.userSettingsUpdated:
|
||||
userSettingsUpdated(data as CommandUserSettingsUpdated);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
case Commands.systemCommentAdded:
|
||||
addComment(data as CommandCommentAdd);
|
||||
break;
|
||||
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
case Commands.systemCommentRemoved:
|
||||
removeComment(data as CommandCommentRemoved);
|
||||
break;
|
||||
|
||||
case Commands.pingAdded:
|
||||
pingAdded(data as CommandPingAdded);
|
||||
break;
|
||||
case Commands.pingAdded:
|
||||
pingAdded(data as CommandPingAdded);
|
||||
break;
|
||||
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
}
|
||||
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
emitMapEvent({ name: type, data });
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export enum ConnectionType {
|
||||
wormhole,
|
||||
gate,
|
||||
bridge,
|
||||
}
|
||||
|
||||
export enum MassState {
|
||||
|
||||
@@ -27,6 +27,7 @@ export enum Commands {
|
||||
userRoutes = 'user_routes',
|
||||
centerSystem = 'center_system',
|
||||
selectSystem = 'select_system',
|
||||
selectSystems = 'select_systems',
|
||||
linkSignatureToSystem = 'link_signature_to_system',
|
||||
signaturesUpdated = 'signatures_updated',
|
||||
systemCommentAdded = 'system_comment_added',
|
||||
@@ -60,6 +61,7 @@ export type Command =
|
||||
| Commands.routes
|
||||
| Commands.userRoutes
|
||||
| Commands.selectSystem
|
||||
| Commands.selectSystems
|
||||
| Commands.centerSystem
|
||||
| Commands.linkSignatureToSystem
|
||||
| Commands.signaturesUpdated
|
||||
@@ -118,6 +120,10 @@ export type CommandUserRoutes = RoutesList;
|
||||
export type CommandKillsUpdated = Kill[];
|
||||
export type CommandDetailedKillsUpdated = Record<string, DetailedKill[]>;
|
||||
export type CommandSelectSystem = string | undefined;
|
||||
export type CommandSelectSystems = {
|
||||
systems: string[];
|
||||
delay?: number;
|
||||
};
|
||||
export type CommandCenterSystem = string | undefined;
|
||||
export type CommandLinkSignatureToSystem = {
|
||||
solar_system_source: number;
|
||||
@@ -187,6 +193,7 @@ export interface CommandData {
|
||||
[Commands.killsUpdated]: CommandKillsUpdated;
|
||||
[Commands.detailedKillsUpdated]: CommandDetailedKillsUpdated;
|
||||
[Commands.selectSystem]: CommandSelectSystem;
|
||||
[Commands.selectSystems]: CommandSelectSystems;
|
||||
[Commands.centerSystem]: CommandCenterSystem;
|
||||
[Commands.linkSignatureToSystem]: CommandLinkSignatureToSystem;
|
||||
[Commands.signaturesUpdated]: CommandLinkSignaturesUpdated;
|
||||
|
||||
@@ -48,6 +48,7 @@ export type SystemSignature = {
|
||||
inserted_at?: string;
|
||||
updated_at?: string;
|
||||
deleted?: boolean;
|
||||
temporary_name?: string;
|
||||
};
|
||||
|
||||
export interface ExtendedSystemSignature extends SystemSignature {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEventBuffer } from '@/hooks/Mapper/hooks';
|
||||
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
|
||||
|
||||
import { MapHandlers } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { RefObject, useCallback, useEffect, useRef } from 'react';
|
||||
import debounce from 'lodash.debounce';
|
||||
import usePageVisibility from '@/hooks/Mapper/hooks/usePageVisibility.ts';
|
||||
|
||||
// const inIndex = 0;
|
||||
// const prevEventTime = +new Date();
|
||||
@@ -10,10 +11,28 @@ const LAST_VERSION_KEY = 'wandererLastVersion';
|
||||
// @ts-ignore
|
||||
export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRef: RefObject<any>) => {
|
||||
const visible = usePageVisibility();
|
||||
|
||||
const wasHiddenOnce = useRef(false);
|
||||
const visibleRef = useRef(visible);
|
||||
visibleRef.current = visible;
|
||||
|
||||
// @ts-ignore
|
||||
const handleBufferedEvent = useCallback(({ type, body }) => {
|
||||
if (!visibleRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
handlerRefs.forEach(ref => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current?.command(type, body);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const { handleEvent: handleMapEvent } = useEventBuffer<any>(handleBufferedEvent);
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// const [record, setRecord] = useLocalStorageState<boolean>('record', {
|
||||
// defaultValue: false,
|
||||
@@ -54,52 +73,6 @@ export const useMapperHandlers = (handlerRefs: RefObject<MapHandlers>[], hooksRe
|
||||
[hooksRef.current],
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const eventsBufferRef = useRef<{ type; body }[]>([]);
|
||||
|
||||
const eventTick = useCallback(
|
||||
debounce(() => {
|
||||
if (eventsBufferRef.current.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { type, body } = eventsBufferRef.current.shift()!;
|
||||
handlerRefs.forEach(ref => {
|
||||
if (!ref.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
ref.current?.command(type, body);
|
||||
});
|
||||
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// console.log('JOipP', `Tick Buff`, eventsBufferRef.current.length);
|
||||
|
||||
if (eventsBufferRef.current.length > 0) {
|
||||
eventTick();
|
||||
}
|
||||
}, 10),
|
||||
[],
|
||||
);
|
||||
const eventTickRef = useRef(eventTick);
|
||||
eventTickRef.current = eventTick;
|
||||
|
||||
// @ts-ignore
|
||||
const handleMapEvent = useCallback(({ type, body }) => {
|
||||
// TODO - do not delete THIS code it needs for debug
|
||||
// const currentTime = +new Date();
|
||||
// const timeDiff = currentTime - prevEventTime;
|
||||
// prevEventTime = currentTime;
|
||||
// console.log('JOipP', `IN [${inIndex++}] [${timeDiff}] ${getFormattedTime()}`, { type, body });
|
||||
|
||||
if (!eventTickRef.current || !visibleRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
eventsBufferRef.current.push({ type, body });
|
||||
eventTickRef.current();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible && !wasHiddenOnce.current) {
|
||||
wasHiddenOnce.current = true;
|
||||
|
||||
132
assets/js/hooks/Mapper/utils/flattenValues.ts
Normal file
132
assets/js/hooks/Mapper/utils/flattenValues.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
const TYPE_ORDER = [
|
||||
'undefined',
|
||||
'null',
|
||||
'boolean',
|
||||
'number',
|
||||
'bigint',
|
||||
'string',
|
||||
'symbol',
|
||||
'function',
|
||||
'date',
|
||||
'regexp',
|
||||
'other',
|
||||
] as const;
|
||||
type TypeTag = (typeof TYPE_ORDER)[number];
|
||||
|
||||
const getTypeTag = (v: unknown): TypeTag => {
|
||||
if (v === undefined) return 'undefined';
|
||||
if (v === null) return 'null';
|
||||
const t = typeof v;
|
||||
if (t === 'boolean' || t === 'number' || t === 'bigint' || t === 'string' || t === 'symbol' || t === 'function')
|
||||
return t as TypeTag;
|
||||
const tag = Object.prototype.toString.call(v);
|
||||
if (tag === '[object Date]') return 'date';
|
||||
if (tag === '[object RegExp]') return 'regexp';
|
||||
return 'other';
|
||||
};
|
||||
|
||||
const cmp = (a: unknown, b: unknown): number => {
|
||||
const ta = getTypeTag(a);
|
||||
const tb = getTypeTag(b);
|
||||
if (ta !== tb) return TYPE_ORDER.indexOf(ta) - TYPE_ORDER.indexOf(tb);
|
||||
|
||||
switch (ta) {
|
||||
case 'undefined':
|
||||
case 'null':
|
||||
return 0;
|
||||
case 'boolean':
|
||||
return (a as boolean) === (b as boolean) ? 0 : a ? 1 : -1;
|
||||
case 'number': {
|
||||
const na = a as number,
|
||||
nb = b as number;
|
||||
const aIsNaN = Number.isNaN(na),
|
||||
bIsNaN = Number.isNaN(nb);
|
||||
if (aIsNaN || bIsNaN) return aIsNaN && bIsNaN ? 0 : aIsNaN ? 1 : -1; // NaN в конец чисел
|
||||
return na === nb ? 0 : na < nb ? -1 : 1;
|
||||
}
|
||||
case 'bigint': {
|
||||
const ba = a as bigint,
|
||||
bb = b as bigint;
|
||||
return ba === bb ? 0 : ba < bb ? -1 : 1;
|
||||
}
|
||||
case 'string':
|
||||
return (a as string).localeCompare(b as string);
|
||||
case 'symbol': {
|
||||
const da = (a as symbol).description ?? '';
|
||||
const db = (b as symbol).description ?? '';
|
||||
return da.localeCompare(db);
|
||||
}
|
||||
case 'function':
|
||||
// @ts-ignore
|
||||
return ((a as Function).name || '').localeCompare((b as Function).name || '');
|
||||
case 'date':
|
||||
return (a as Date).getTime() - (b as Date).getTime();
|
||||
case 'regexp':
|
||||
return a!.toString().localeCompare(b!.toString());
|
||||
default:
|
||||
return String(a).localeCompare(String(b));
|
||||
}
|
||||
};
|
||||
|
||||
const isIterable = (v: unknown): v is Iterable<unknown> =>
|
||||
v != null && typeof (v as any)[Symbol.iterator] === 'function';
|
||||
|
||||
const pushTypedArrayValues = (v: unknown, out: unknown[]) => {
|
||||
if (ArrayBuffer.isView(v) && !(v instanceof DataView)) {
|
||||
// @ts-ignore
|
||||
out.push(...(v as ArrayLike<number> as any));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate this func with ChatGPT 5. Cause it pure func and looks like what i need
|
||||
* May be in net we can find smtng like that
|
||||
* @param input
|
||||
*/
|
||||
export const flattenValues = (input: unknown): unknown[] => {
|
||||
const out: unknown[] = [];
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
const visit = (v: unknown): void => {
|
||||
const tag = getTypeTag(v);
|
||||
if (tag !== 'other') {
|
||||
out.push(v);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v && typeof v === 'object') {
|
||||
if (seen.has(v)) return;
|
||||
seen.add(v);
|
||||
|
||||
if (pushTypedArrayValues(v, out)) return;
|
||||
|
||||
if (v instanceof Map) {
|
||||
for (const val of v.values()) visit(val);
|
||||
return;
|
||||
}
|
||||
|
||||
if (v instanceof Set) {
|
||||
for (const val of v.values()) visit(val);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(v) || isIterable(v)) {
|
||||
for (const item of v as Iterable<unknown>) visit(item);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const key of Object.keys(v)) {
|
||||
// @ts-ignore
|
||||
visit((v as never)[key]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
out.push(v);
|
||||
};
|
||||
|
||||
visit(input);
|
||||
return out.sort(cmp);
|
||||
};
|
||||
@@ -79,7 +79,7 @@
|
||||
"sass-loader": "^14.2.1",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^5.0.5",
|
||||
"vite": "^6.3.5",
|
||||
"vite-plugin-cdn-import": "^1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
5377
assets/yarn.lock
5377
assets/yarn.lock
File diff suppressed because it is too large
Load Diff
82
clean_changelog.py
Normal file
82
clean_changelog.py
Normal file
@@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to clean up CHANGELOG.md by removing empty version entries.
|
||||
An empty version entry has only a version header followed by empty lines,
|
||||
without any actual content (### Bug Fixes: or ### Features: sections).
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
def clean_changelog():
|
||||
with open('./CHANGELOG.md', 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Split content into sections based on version headers
|
||||
version_pattern = r'^## \[v\d+\.\d+\.\d+\].*?\([^)]+\)$'
|
||||
|
||||
# Find all version headers with their positions
|
||||
matches = list(re.finditer(version_pattern, content, re.MULTILINE))
|
||||
|
||||
# Build new content by keeping only non-empty versions
|
||||
new_content = ""
|
||||
|
||||
# Keep the header (everything before first version)
|
||||
if matches:
|
||||
new_content += content[:matches[0].start()]
|
||||
else:
|
||||
# No versions found, keep original
|
||||
return content
|
||||
|
||||
for i, match in enumerate(matches):
|
||||
version_start = match.start()
|
||||
|
||||
# Find the end of this version section (start of next version or end of file)
|
||||
if i + 1 < len(matches):
|
||||
version_end = matches[i + 1].start()
|
||||
else:
|
||||
version_end = len(content)
|
||||
|
||||
version_section = content[version_start:version_end]
|
||||
|
||||
# Check if this version has actual content
|
||||
# Look for ### Bug Fixes: or ### Features: followed by actual content
|
||||
has_content = False
|
||||
|
||||
# Split the section into lines
|
||||
lines = version_section.split('\n')
|
||||
|
||||
# Look for content sections
|
||||
in_content_section = False
|
||||
for line in lines:
|
||||
line_stripped = line.strip()
|
||||
|
||||
# Check if we're entering a content section
|
||||
if line_stripped.startswith('### Bug Fixes:') or line_stripped.startswith('### Features:'):
|
||||
in_content_section = True
|
||||
continue
|
||||
|
||||
# If we're in a content section and find non-empty content
|
||||
if in_content_section:
|
||||
if line_stripped and not line_stripped.startswith('###') and not line_stripped.startswith('##'):
|
||||
# This is actual content (not just another header)
|
||||
if line_stripped.startswith('*') or len(line_stripped) > 0:
|
||||
has_content = True
|
||||
break
|
||||
elif line_stripped.startswith('##'):
|
||||
# We've reached the next version, stop looking
|
||||
break
|
||||
|
||||
# Only keep versions with actual content
|
||||
if has_content:
|
||||
new_content += version_section
|
||||
|
||||
return new_content
|
||||
|
||||
if __name__ == "__main__":
|
||||
cleaned_content = clean_changelog()
|
||||
|
||||
# Write the cleaned content back to the file
|
||||
with open('./CHANGELOG.md', 'w') as f:
|
||||
f.write(cleaned_content)
|
||||
|
||||
print("CHANGELOG.md has been cleaned up successfully!")
|
||||
@@ -11,11 +11,13 @@ config :wanderer_app, WandererAppWeb.Endpoint,
|
||||
config :wanderer_app, WandererApp.Repo,
|
||||
ssl: false,
|
||||
stacktrace: true,
|
||||
show_sensitive_data_on_connection_error: true,
|
||||
show_sensitive_data_on_connection_error: false,
|
||||
pool_size: 15,
|
||||
migration_timestamps: [type: :utc_datetime_usec],
|
||||
migration_lock: nil,
|
||||
queue_target: 5000
|
||||
queue_target: 5000,
|
||||
queue_interval: 1000,
|
||||
checkout_timeout: 15000
|
||||
|
||||
# Configures Swoosh API Client
|
||||
config :swoosh, api_client: Swoosh.ApiClient.Finch, finch_name: WandererApp.Finch
|
||||
|
||||
@@ -121,6 +121,11 @@ restrict_maps_creation =
|
||||
|> get_var_from_path_or_env("WANDERER_RESTRICT_MAPS_CREATION", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
restrict_acls_creation =
|
||||
config_dir
|
||||
|> get_var_from_path_or_env("WANDERER_RESTRICT_ACLS_CREATION", "false")
|
||||
|> String.to_existing_atom()
|
||||
|
||||
config :wanderer_app,
|
||||
web_app_url: web_app_url,
|
||||
git_sha: System.get_env("GIT_SHA", "111"),
|
||||
@@ -129,6 +134,8 @@ config :wanderer_app,
|
||||
admin_username: System.get_env("WANDERER_ADMIN_USERNAME", "admin"),
|
||||
admin_password: System.get_env("WANDERER_ADMIN_PASSWORD"),
|
||||
admins: admins,
|
||||
base_metrics_only:
|
||||
System.get_env("WANDERER_BASE_METRICS_ONLY", "false") |> String.to_existing_atom(),
|
||||
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
|
||||
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
|
||||
corp_wallet_eve_id: System.get_env("WANDERER_CORP_WALLET_EVE_ID", "-1"),
|
||||
@@ -148,6 +155,7 @@ config :wanderer_app,
|
||||
map_connection_eol_expire_timeout_mins: map_connection_eol_expire_timeout_mins,
|
||||
wallet_tracking_enabled: wallet_tracking_enabled,
|
||||
restrict_maps_creation: restrict_maps_creation,
|
||||
restrict_acls_creation: restrict_acls_creation,
|
||||
subscription_settings: %{
|
||||
plans: [
|
||||
%{
|
||||
|
||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Api.Character do
|
||||
update :update_corporation do
|
||||
require_atomic? false
|
||||
|
||||
accept([:corporation_id, :corporation_name, :corporation_ticker, :alliance_id])
|
||||
accept([:corporation_id, :corporation_name, :corporation_ticker])
|
||||
end
|
||||
|
||||
update :update_alliance do
|
||||
|
||||
@@ -79,8 +79,7 @@ defmodule WandererApp.Api.MapCharacterSettings do
|
||||
accept [
|
||||
:map_id,
|
||||
:character_id,
|
||||
:tracked,
|
||||
:followed
|
||||
:tracked
|
||||
]
|
||||
|
||||
argument :map_id, :uuid, allow_nil?: false
|
||||
|
||||
@@ -168,6 +168,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
|
||||
# where 0 - Wormhole
|
||||
# where 1 - Gate
|
||||
# where 2 - Bridge
|
||||
attribute :type, :integer do
|
||||
default(0)
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
code_interface do
|
||||
define(:all_active, action: :all_active)
|
||||
define(:create, action: :create)
|
||||
define(:destroy, action: :destroy)
|
||||
define(:update, action: :update)
|
||||
define(:update_linked_system, action: :update_linked_system)
|
||||
define(:update_type, action: :update_type)
|
||||
@@ -62,6 +63,7 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:temporary_name,
|
||||
:description,
|
||||
:kind,
|
||||
:group,
|
||||
@@ -101,6 +103,7 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:temporary_name,
|
||||
:description,
|
||||
:kind,
|
||||
:group,
|
||||
@@ -120,6 +123,7 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:temporary_name,
|
||||
:description,
|
||||
:kind,
|
||||
:group,
|
||||
@@ -195,6 +199,10 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :temporary_name, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
|
||||
attribute :type, :string do
|
||||
allow_nil? true
|
||||
end
|
||||
@@ -241,6 +249,7 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:eve_id,
|
||||
:character_eve_id,
|
||||
:name,
|
||||
:temporary_name,
|
||||
:description,
|
||||
:type,
|
||||
:linked_system_id,
|
||||
|
||||
@@ -54,6 +54,7 @@ defmodule WandererApp.Application do
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Map.DynamicSupervisors},
|
||||
{PartitionSupervisor,
|
||||
child_spec: DynamicSupervisor, name: WandererApp.Character.DynamicSupervisors},
|
||||
WandererAppWeb.PresenceGracePeriodManager,
|
||||
WandererAppWeb.Presence,
|
||||
WandererAppWeb.Endpoint
|
||||
]
|
||||
|
||||
@@ -113,6 +113,63 @@ defmodule WandererApp.CachedInfo do
|
||||
end
|
||||
end
|
||||
|
||||
def get_solar_system_jumps() do
|
||||
case WandererApp.Cache.lookup(:solar_system_jumps) do
|
||||
{:ok, nil} ->
|
||||
data = WandererApp.EveDataService.get_solar_system_jumps_data()
|
||||
|
||||
cache_items(data, :solar_system_jumps)
|
||||
|
||||
{:ok, data}
|
||||
|
||||
{:ok, data} ->
|
||||
{:ok, data}
|
||||
end
|
||||
end
|
||||
|
||||
def get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
# Create normalized cache key (smaller ID first for bidirectional lookup)
|
||||
{id1, id2} =
|
||||
if from_solar_system_id < to_solar_system_id do
|
||||
{from_solar_system_id, to_solar_system_id}
|
||||
else
|
||||
{to_solar_system_id, from_solar_system_id}
|
||||
end
|
||||
|
||||
cache_key = "jump_#{id1}_#{id2}"
|
||||
|
||||
case WandererApp.Cache.lookup(cache_key) do
|
||||
{:ok, nil} ->
|
||||
# Build jump index if not exists
|
||||
build_jump_index()
|
||||
WandererApp.Cache.lookup(cache_key)
|
||||
|
||||
result ->
|
||||
result
|
||||
end
|
||||
end
|
||||
|
||||
defp build_jump_index() do
|
||||
case get_solar_system_jumps() do
|
||||
{:ok, jumps} ->
|
||||
jumps
|
||||
|> Enum.each(fn jump ->
|
||||
{id1, id2} =
|
||||
if jump.from_solar_system_id < jump.to_solar_system_id do
|
||||
{jump.from_solar_system_id, jump.to_solar_system_id}
|
||||
else
|
||||
{jump.to_solar_system_id, jump.from_solar_system_id}
|
||||
end
|
||||
|
||||
cache_key = "jump_#{id1}_#{id2}"
|
||||
WandererApp.Cache.put(cache_key, jump)
|
||||
end)
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def get_wormhole_types!() do
|
||||
case get_wormhole_types() do
|
||||
{:ok, wormhole_types} ->
|
||||
|
||||
@@ -263,7 +263,7 @@ defmodule WandererApp.Character do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_merge_map_character_settings(%{id: character_id} = character, map_id, true) do
|
||||
defp maybe_merge_map_character_settings(%{id: character_id} = character, _map_id, true) do
|
||||
{:ok, tracking_paused} =
|
||||
WandererApp.Cache.lookup("character:#{character_id}:tracking_paused", false)
|
||||
|
||||
|
||||
@@ -49,11 +49,13 @@ defmodule WandererApp.Character.Activity do
|
||||
"""
|
||||
def process_character_activity(map_id, current_user) do
|
||||
with {:ok, map_user_settings} <- get_map_user_settings(map_id, current_user.id),
|
||||
raw_activity <- WandererApp.Map.get_character_activity(map_id),
|
||||
{:ok, raw_activity} <- WandererApp.Map.get_character_activity(map_id),
|
||||
{:ok, user_characters} <-
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do
|
||||
result = process_activity_data(raw_activity, map_user_settings, user_characters)
|
||||
result
|
||||
process_activity_data(raw_activity, map_user_settings, user_characters)
|
||||
else
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
defstruct [
|
||||
:character_id,
|
||||
:alliance_id,
|
||||
:corporation_id,
|
||||
:opts,
|
||||
server_online: true,
|
||||
start_time: nil,
|
||||
@@ -21,6 +22,8 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
@type t :: %__MODULE__{
|
||||
character_id: integer,
|
||||
alliance_id: integer,
|
||||
corporation_id: integer,
|
||||
opts: map,
|
||||
server_online: boolean,
|
||||
start_time: DateTime.t(),
|
||||
@@ -35,12 +38,13 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
@pause_tracking_timeout :timer.minutes(60 * 10)
|
||||
@offline_timeout :timer.minutes(5)
|
||||
@online_error_timeout :timer.minutes(2)
|
||||
@ship_error_timeout :timer.minutes(2)
|
||||
@location_error_timeout :timer.minutes(2)
|
||||
@online_error_timeout :timer.minutes(10)
|
||||
@ship_error_timeout :timer.minutes(10)
|
||||
@location_error_timeout :timer.minutes(10)
|
||||
@online_forbidden_ttl :timer.seconds(7)
|
||||
@offline_check_delay_ttl :timer.seconds(15)
|
||||
@online_limit_ttl :timer.seconds(7)
|
||||
@forbidden_ttl :timer.seconds(5)
|
||||
@forbidden_ttl :timer.seconds(10)
|
||||
@limit_ttl :timer.seconds(5)
|
||||
@location_limit_ttl :timer.seconds(1)
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
@@ -49,8 +53,15 @@ defmodule WandererApp.Character.Tracker do
|
||||
def new(args), do: __struct__(args)
|
||||
|
||||
def init(args) do
|
||||
character_id = args[:character_id]
|
||||
|
||||
{:ok, %{corporation_id: corporation_id, alliance_id: alliance_id}} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
%{
|
||||
character_id: args[:character_id],
|
||||
character_id: character_id,
|
||||
corporation_id: corporation_id,
|
||||
alliance_id: alliance_id,
|
||||
start_time: DateTime.utc_now(),
|
||||
opts: args
|
||||
}
|
||||
@@ -61,18 +72,19 @@ defmodule WandererApp.Character.Tracker do
|
||||
WandererApp.Cache.lookup!("character:#{character_id}:last_online_time")
|
||||
|> case do
|
||||
nil ->
|
||||
WandererApp.Cache.insert(
|
||||
"character:#{character_id}:last_online_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
last_online_time ->
|
||||
duration = DateTime.diff(DateTime.utc_now(), last_online_time, :millisecond)
|
||||
|
||||
if duration >= @offline_timeout do
|
||||
pause_tracking(character_id)
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:last_online_time")
|
||||
|
||||
:ok
|
||||
else
|
||||
@@ -101,6 +113,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
|
||||
if duration >= timeout do
|
||||
pause_tracking(character_id)
|
||||
WandererApp.Cache.delete("character:#{character_id}:#{type}_error_time")
|
||||
|
||||
:ok
|
||||
else
|
||||
@@ -113,15 +126,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
if WandererApp.Character.can_pause_tracking?(character_id) &&
|
||||
not WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused") do
|
||||
# Log character tracking statistics before pausing
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
|
||||
Logger.debug(fn ->
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
|
||||
|
||||
Logger.warning(
|
||||
"CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors",
|
||||
character_id: character_id,
|
||||
"CHARACTER_TRACKING_PAUSED: Character tracking paused due to sustained errors: #{inspect(character_id: character_id,
|
||||
active_maps: length(character_state.active_maps),
|
||||
is_online: character_state.is_online,
|
||||
tracking_duration_minutes: get_tracking_duration_minutes(character_id)
|
||||
)
|
||||
tracking_duration_minutes: get_tracking_duration_minutes(character_id))}"
|
||||
end)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
|
||||
@@ -176,7 +188,9 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> WandererApp.Character.get_character_state!()
|
||||
|> update_online()
|
||||
|
||||
def update_online(%{track_online: true, character_id: character_id} = character_state) do
|
||||
def update_online(
|
||||
%{track_online: true, character_id: character_id, is_online: is_online} = character_state
|
||||
) do
|
||||
case WandererApp.Character.get_character(character_id) do
|
||||
{:ok, %{eve_id: eve_id, access_token: access_token, tracking_pool: tracking_pool}}
|
||||
when not is_nil(access_token) ->
|
||||
@@ -187,13 +201,11 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
_ ->
|
||||
# Monitor cache for potential evictions before ESI call
|
||||
|
||||
case WandererApp.Esi.get_character_online(eve_id,
|
||||
access_token: access_token,
|
||||
character_id: character_id
|
||||
) do
|
||||
{:ok, online} ->
|
||||
{:ok, online} when is_map(online) ->
|
||||
online = get_online(online)
|
||||
|
||||
if online.online == true do
|
||||
@@ -201,70 +213,67 @@ defmodule WandererApp.Character.Tracker do
|
||||
"character:#{character_id}:last_online_time",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
else
|
||||
# Delay next online updates for offline characters
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
ttl: @offline_check_delay_ttl
|
||||
)
|
||||
end
|
||||
|
||||
if online.online == true && online.online != is_online do
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:corporation_info_forbidden")
|
||||
end
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:online_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_error_time")
|
||||
WandererApp.Cache.delete("character:#{character_id}:info_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:ship_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:location_forbidden")
|
||||
WandererApp.Cache.delete("character:#{character_id}:wallet_forbidden")
|
||||
|
||||
try do
|
||||
WandererApp.Character.update_character(character_id, online)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("DB_ERROR: Failed to update character in database",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
operation: "update_character_online"
|
||||
)
|
||||
if online.online != is_online do
|
||||
try do
|
||||
WandererApp.Character.update_character(character_id, online)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("DB_ERROR: Failed to update character in database",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
operation: "update_character_online"
|
||||
)
|
||||
|
||||
# Re-raise to maintain existing error handling
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
# Re-raise to maintain existing error handling
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
|
||||
update = %{
|
||||
character_state
|
||||
| is_online: online.online,
|
||||
track_ship: online.online,
|
||||
track_location: online.online
|
||||
}
|
||||
try do
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
character_state
|
||||
| is_online: online.online,
|
||||
track_ship: online.online,
|
||||
track_location: online.online
|
||||
})
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("DB_ERROR: Failed to update character state in database",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
operation: "update_character_state"
|
||||
)
|
||||
|
||||
try do
|
||||
WandererApp.Character.update_character_state(character_id, update)
|
||||
rescue
|
||||
error ->
|
||||
Logger.error("DB_ERROR: Failed to update character state in database",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
operation: "update_character_state"
|
||||
)
|
||||
|
||||
# Re-raise to maintain existing error handling
|
||||
reraise error, __STACKTRACE__
|
||||
# Re-raise to maintain existing error handling
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_online",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character online tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
endpoint: "character_online"
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
@@ -291,28 +300,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
remaining =
|
||||
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_online",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character online tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_online",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
@@ -322,15 +309,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_online",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character online tracking failed",
|
||||
Logger.error("ESI_ERROR: Character online tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -388,31 +367,25 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:ok, %{eve_id: eve_id, tracking_pool: tracking_pool}} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
case WandererApp.Esi.get_character_info(eve_id) do
|
||||
{:ok, _info} ->
|
||||
character_eve_id = eve_id |> String.to_integer()
|
||||
|
||||
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
|
||||
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
|
||||
{:ok, character_state} = WandererApp.Character.get_character_state(character_id)
|
||||
|
||||
update = maybe_update_corporation(character_state, eve_id |> String.to_integer())
|
||||
WandererApp.Character.update_character_state(character_id, update)
|
||||
alliance_id = character_aff_info |> Map.get("alliance_id")
|
||||
corporation_id = character_aff_info |> Map.get("corporation_id")
|
||||
|
||||
updated_state =
|
||||
character_state
|
||||
|> maybe_update_corporation(corporation_id)
|
||||
|> maybe_update_alliance(alliance_id)
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, updated_state)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_info",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character info tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
endpoint: "character_info"
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:info_forbidden",
|
||||
true,
|
||||
@@ -424,33 +397,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
reset_seconds =
|
||||
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
|
||||
remaining = Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_info",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character info tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_info",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:info_forbidden",
|
||||
true,
|
||||
@@ -460,21 +406,13 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_info",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:info_forbidden",
|
||||
true,
|
||||
ttl: @forbidden_ttl
|
||||
)
|
||||
|
||||
Logger.error("ESI_ERROR: Character info tracking failed",
|
||||
Logger.error("ESI_ERROR: Character info tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -521,21 +459,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_ship",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character ship tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
endpoint: "character_ship"
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:ship_forbidden",
|
||||
true,
|
||||
@@ -554,34 +477,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
reset_seconds =
|
||||
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
|
||||
remaining =
|
||||
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_ship",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character ship tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_ship",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:ship_forbidden",
|
||||
true,
|
||||
@@ -591,15 +486,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_ship",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character ship tracking failed",
|
||||
Logger.error("ESI_ERROR: Character ship tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -622,14 +509,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, error}
|
||||
|
||||
_ ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_ship",
|
||||
error_type: "wrong_response",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character ship tracking failed - wrong response",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
@@ -692,14 +571,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_location",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character location tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
@@ -721,34 +592,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers, @location_limit_ttl)
|
||||
|
||||
reset_seconds =
|
||||
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
|
||||
remaining =
|
||||
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_location",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character location tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_location",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:location_forbidden",
|
||||
true,
|
||||
@@ -758,15 +601,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_location",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character location tracking failed",
|
||||
Logger.error("ESI_ERROR: Character location tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -785,14 +620,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
_ ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_location",
|
||||
error_type: "wrong_response",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character location tracking failed - wrong response",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
@@ -854,14 +681,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
:ok
|
||||
|
||||
{:error, error} when error in [:forbidden, :not_found, :timeout] ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_wallet",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.warning("ESI_ERROR: Character wallet tracking failed",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
@@ -880,34 +699,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
reset_seconds =
|
||||
Map.get(headers, "x-esi-error-limit-reset", ["unknown"]) |> List.first()
|
||||
|
||||
remaining =
|
||||
Map.get(headers, "x-esi-error-limit-remain", ["unknown"]) |> List.first()
|
||||
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :esi, :rate_limited],
|
||||
%{
|
||||
reset_duration: reset_timeout,
|
||||
count: 1
|
||||
},
|
||||
%{
|
||||
endpoint: "character_wallet",
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
}
|
||||
)
|
||||
|
||||
Logger.warning("ESI_RATE_LIMITED: Character wallet tracking rate limited",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
endpoint: "character_wallet",
|
||||
reset_seconds: reset_seconds,
|
||||
remaining_requests: remaining
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:wallet_forbidden",
|
||||
true,
|
||||
@@ -917,15 +708,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
{:error, error} ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_wallet",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character wallet tracking failed",
|
||||
Logger.error("ESI_ERROR: Character wallet tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -941,15 +724,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
{:error, :skipped}
|
||||
|
||||
error ->
|
||||
# Emit telemetry for tracking
|
||||
:telemetry.execute([:wanderer_app, :esi, :error], %{count: 1}, %{
|
||||
endpoint: "character_wallet",
|
||||
error_type: error,
|
||||
tracking_pool: tracking_pool,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
Logger.error("ESI_ERROR: Character wallet tracking failed",
|
||||
Logger.error("ESI_ERROR: Character wallet tracking failed: #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
@@ -975,7 +750,38 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
end
|
||||
|
||||
defp update_alliance(%{character_id: character_id} = state, alliance_id) do
|
||||
defp maybe_update_alliance(
|
||||
%{character_id: character_id, alliance_id: old_alliance_id} = state,
|
||||
alliance_id
|
||||
)
|
||||
when old_alliance_id != alliance_id and is_nil(alliance_id) do
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
character_update = %{
|
||||
alliance_id: nil,
|
||||
alliance_name: nil,
|
||||
alliance_ticker: nil
|
||||
}
|
||||
|
||||
{:ok, _character} =
|
||||
Character.update_alliance(character, character_update)
|
||||
|
||||
WandererApp.Character.update_character(character_id, character_update)
|
||||
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}:alliance",
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
defp maybe_update_alliance(
|
||||
%{character_id: character_id, alliance_id: old_alliance_id} = state,
|
||||
alliance_id
|
||||
)
|
||||
when old_alliance_id != alliance_id do
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
@@ -1015,8 +821,15 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
end
|
||||
|
||||
defp update_corporation(%{character_id: character_id} = state, corporation_id) do
|
||||
defp maybe_update_alliance(state, _alliance_id), do: state
|
||||
|
||||
defp maybe_update_corporation(
|
||||
%{character_id: character_id, corporation_id: old_corporation_id} = state,
|
||||
corporation_id
|
||||
)
|
||||
when old_corporation_id != corporation_id do
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:corporation_info_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
true ->
|
||||
@@ -1027,16 +840,13 @@ defmodule WandererApp.Character.Tracker do
|
||||
|> WandererApp.Esi.get_corporation_info()
|
||||
|> case do
|
||||
{:ok, %{"name" => corporation_name, "ticker" => corporation_ticker} = corporation_info} ->
|
||||
alliance_id = Map.get(corporation_info, "alliance_id")
|
||||
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
character_update = %{
|
||||
corporation_id: corporation_id,
|
||||
corporation_name: corporation_name,
|
||||
corporation_ticker: corporation_ticker,
|
||||
alliance_id: alliance_id
|
||||
corporation_ticker: corporation_ticker
|
||||
}
|
||||
|
||||
{:ok, _character} =
|
||||
@@ -1057,8 +867,18 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
|
||||
state
|
||||
|> Map.merge(%{alliance_id: alliance_id, corporation_id: corporation_id})
|
||||
|> maybe_update_alliance()
|
||||
|> Map.merge(%{corporation_id: corporation_id})
|
||||
|
||||
{:error, :error_limited, headers} ->
|
||||
reset_timeout = get_reset_timeout(headers)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:corporation_info_forbidden",
|
||||
true,
|
||||
ttl: reset_timeout
|
||||
)
|
||||
|
||||
state
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
@@ -1072,6 +892,8 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_corporation(state, _corporation_id), do: state
|
||||
|
||||
defp maybe_update_ship(
|
||||
%{
|
||||
character_id: character_id
|
||||
@@ -1153,58 +975,6 @@ defmodule WandererApp.Character.Tracker do
|
||||
structure_id != new_structure_id ||
|
||||
station_id != new_station_id
|
||||
|
||||
defp maybe_update_corporation(
|
||||
state,
|
||||
character_eve_id
|
||||
)
|
||||
when not is_nil(character_eve_id) and is_integer(character_eve_id) do
|
||||
case WandererApp.Esi.post_characters_affiliation([character_eve_id]) do
|
||||
{:ok, [character_aff_info]} when not is_nil(character_aff_info) ->
|
||||
update_corporation(state, character_aff_info |> Map.get("corporation_id"))
|
||||
|
||||
_error ->
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_corporation(
|
||||
state,
|
||||
_info
|
||||
),
|
||||
do: state
|
||||
|
||||
defp maybe_update_alliance(
|
||||
%{character_id: character_id, alliance_id: alliance_id} =
|
||||
state
|
||||
) do
|
||||
case alliance_id do
|
||||
nil ->
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
character_update = %{
|
||||
alliance_id: nil,
|
||||
alliance_name: nil,
|
||||
alliance_ticker: nil
|
||||
}
|
||||
|
||||
{:ok, _character} =
|
||||
Character.update_alliance(character, character_update)
|
||||
|
||||
WandererApp.Character.update_character(character_id, character_update)
|
||||
|
||||
@pubsub_client.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}:alliance",
|
||||
{:character_alliance, {character_id, character_update}}
|
||||
)
|
||||
|
||||
state
|
||||
|
||||
_ ->
|
||||
update_alliance(state, alliance_id)
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_update_wallet(
|
||||
%{character_id: character_id} =
|
||||
state,
|
||||
|
||||
@@ -12,10 +12,10 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
opts: map
|
||||
}
|
||||
|
||||
@garbage_collection_interval :timer.minutes(15)
|
||||
@untrack_characters_interval :timer.minutes(1)
|
||||
@inactive_character_timeout :timer.minutes(10)
|
||||
@untrack_character_timeout :timer.minutes(10)
|
||||
@check_start_queue_interval :timer.seconds(1)
|
||||
@garbage_collection_interval :timer.minutes(5)
|
||||
@untrack_characters_interval :timer.minutes(5)
|
||||
@inactive_character_timeout :timer.minutes(5)
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
|
||||
@@ -23,6 +23,7 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
def new(args), do: __struct__(args)
|
||||
|
||||
def init(args) do
|
||||
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
|
||||
Process.send_after(self(), :garbage_collect, @garbage_collection_interval)
|
||||
Process.send_after(self(), :untrack_characters, @untrack_characters_interval)
|
||||
|
||||
@@ -46,25 +47,21 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
end
|
||||
|
||||
def start_tracking(state, character_id, opts) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
false <- Enum.member?(characters, character_id) do
|
||||
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
|
||||
if not WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
||||
WandererApp.Cache.insert(
|
||||
"#{character_id}:track_requested",
|
||||
true
|
||||
)
|
||||
|
||||
tracked_characters = [character_id | characters] |> Enum.uniq()
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
Logger.debug(fn -> "Add character to track_characters_queue: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
|
||||
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
|
||||
character_id,
|
||||
%{opts: opts}
|
||||
])
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
[character_id],
|
||||
fn existing ->
|
||||
[character_id | existing] |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
state
|
||||
@@ -73,29 +70,25 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
def stop_tracking(state, character_id) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
true <- Enum.member?(characters, character_id),
|
||||
{:ok, %{start_time: start_time}} <-
|
||||
WandererApp.Character.get_character_state(character_id, false) do
|
||||
false <- WandererApp.Cache.has_key?("#{character_id}:track_requested") do
|
||||
Logger.debug(fn -> "Shutting down character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character_id}:last_active_time")
|
||||
WandererApp.Character.delete_character_state(character_id)
|
||||
|
||||
tracked_characters =
|
||||
characters |> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
|
||||
WandererApp.Cache.insert("tracked_characters", tracked_characters)
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.stop_tracking(character_id)
|
||||
|
||||
duration = DateTime.diff(DateTime.utc_now(), start_time, :second)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :running], %{
|
||||
duration: duration
|
||||
})
|
||||
|
||||
:telemetry.execute([:wanderer_app, :character, :tracker, :stopped], %{count: 1})
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"tracked_characters",
|
||||
[],
|
||||
fn tracked_characters ->
|
||||
tracked_characters
|
||||
|> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
end
|
||||
)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
@@ -122,25 +115,17 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
end
|
||||
|
||||
def add_to_untrack_queue(map_id, character_id) do
|
||||
if not WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
|
||||
WandererApp.Cache.insert(
|
||||
"#{map_id}:#{character_id}:untrack_requested",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"character_untrack_queue",
|
||||
[{map_id, character_id}],
|
||||
fn untrack_queue ->
|
||||
[{map_id, character_id} | untrack_queue] |> Enum.uniq()
|
||||
[{map_id, character_id} | untrack_queue]
|
||||
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
def remove_from_untrack_queue(map_id, character_id) do
|
||||
WandererApp.Cache.delete("#{map_id}:#{character_id}:untrack_requested")
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"character_untrack_queue",
|
||||
[],
|
||||
@@ -178,6 +163,21 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:check_start_queue,
|
||||
state
|
||||
) do
|
||||
Process.send_after(self(), :check_start_queue, @check_start_queue_interval)
|
||||
{:ok, track_characters_queue} = WandererApp.Cache.lookup("track_characters_queue", [])
|
||||
|
||||
track_characters_queue
|
||||
|> Enum.each(fn character_id ->
|
||||
track_character(character_id, %{})
|
||||
end)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_info(
|
||||
:garbage_collect,
|
||||
state
|
||||
@@ -229,50 +229,32 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
WandererApp.Cache.lookup!("character_untrack_queue", [])
|
||||
|> Task.async_stream(
|
||||
fn {map_id, character_id} ->
|
||||
untrack_timeout_reached =
|
||||
if WandererApp.Cache.has_key?("#{map_id}:#{character_id}:untrack_requested") do
|
||||
untrack_requested =
|
||||
WandererApp.Cache.lookup!(
|
||||
"#{map_id}:#{character_id}:untrack_requested",
|
||||
DateTime.utc_now()
|
||||
)
|
||||
remove_from_untrack_queue(map_id, character_id)
|
||||
|
||||
duration = DateTime.diff(DateTime.utc_now(), untrack_requested, :millisecond)
|
||||
duration >= @untrack_character_timeout
|
||||
else
|
||||
false
|
||||
end
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
|
||||
|
||||
Logger.debug(fn -> "Untrack timeout reached: #{inspect(untrack_timeout_reached)}" end)
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: false
|
||||
})
|
||||
|
||||
if untrack_timeout_reached do
|
||||
remove_from_untrack_queue(map_id, character_id)
|
||||
{:ok, character} = WandererApp.Character.get_character(character_id)
|
||||
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:solar_system_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:station_id")
|
||||
WandererApp.Cache.delete("map:#{map_id}:character:#{character_id}:structure_id")
|
||||
{: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
|
||||
})
|
||||
|
||||
{:ok, character_state} =
|
||||
WandererApp.Character.Tracker.update_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
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)
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
|
||||
end
|
||||
WandererApp.Character.update_character_state(character_id, character_state)
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :untrack_character, character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
@@ -294,8 +276,56 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
state
|
||||
end
|
||||
|
||||
def handle_info(_event, state),
|
||||
do: state
|
||||
def track_character(character_id, opts) do
|
||||
with {:ok, characters} <- WandererApp.Cache.lookup("tracked_characters", []),
|
||||
false <- Enum.member?(characters, character_id) do
|
||||
Logger.debug(fn -> "Start character tracker: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"tracked_characters",
|
||||
[character_id],
|
||||
fn existing ->
|
||||
[character_id | existing] |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
[],
|
||||
fn existing ->
|
||||
existing
|
||||
|> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.delete("#{character_id}:track_requested")
|
||||
|
||||
WandererApp.Character.update_character(character_id, %{online: false})
|
||||
|
||||
WandererApp.Character.update_character_state(character_id, %{
|
||||
is_online: false
|
||||
})
|
||||
|
||||
WandererApp.Character.TrackerPoolDynamicSupervisor.start_tracking(character_id)
|
||||
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character, :update_character_state, [
|
||||
character_id,
|
||||
%{opts: opts}
|
||||
])
|
||||
else
|
||||
_ ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
[],
|
||||
fn existing ->
|
||||
existing
|
||||
|> Enum.reject(fn c_id -> c_id == character_id end)
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.delete("#{character_id}:track_requested")
|
||||
end
|
||||
end
|
||||
|
||||
def character_is_present(map_id, character_id) do
|
||||
{:ok, presence_character_ids} =
|
||||
|
||||
@@ -18,12 +18,12 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
@update_location_interval :timer.seconds(1)
|
||||
@update_online_interval :timer.seconds(5)
|
||||
@check_offline_characters_interval :timer.minutes(2)
|
||||
@check_offline_characters_interval :timer.minutes(5)
|
||||
@check_online_errors_interval :timer.minutes(1)
|
||||
@check_ship_errors_interval :timer.minutes(1)
|
||||
@check_location_errors_interval :timer.minutes(1)
|
||||
@update_ship_interval :timer.seconds(2)
|
||||
@update_info_interval :timer.minutes(1)
|
||||
@update_info_interval :timer.minutes(2)
|
||||
@update_wallet_interval :timer.minutes(1)
|
||||
|
||||
@logger Application.compile_env(:wanderer_app, :logger)
|
||||
@@ -176,11 +176,15 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Enum.each(fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_online, [
|
||||
character_id
|
||||
])
|
||||
end)
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_online(character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
@@ -234,17 +238,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
if WandererApp.Character.can_pause_tracking?(character_id) do
|
||||
WandererApp.TaskWrapper.start_link(
|
||||
WandererApp.Character.Tracker,
|
||||
:check_offline,
|
||||
[
|
||||
character_id
|
||||
]
|
||||
)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
WandererApp.Character.Tracker.check_offline(character_id)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
@@ -397,11 +391,15 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Enum.each(fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_location, [
|
||||
character_id
|
||||
])
|
||||
end)
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_location(character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
@@ -434,11 +432,15 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
|
||||
try do
|
||||
characters
|
||||
|> Enum.each(fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_ship, [
|
||||
character_id
|
||||
])
|
||||
end)
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.Character.Tracker.update_ship(character_id)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(5)
|
||||
)
|
||||
|> Enum.each(fn _result -> :ok end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
@@ -473,9 +475,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_info, [
|
||||
character_id
|
||||
])
|
||||
WandererApp.Character.Tracker.update_info(character_id)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
@@ -519,9 +519,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
characters
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
WandererApp.TaskWrapper.start_link(WandererApp.Character.Tracker, :update_wallet, [
|
||||
character_id
|
||||
])
|
||||
WandererApp.Character.Tracker.update_wallet(character_id)
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
)
|
||||
when not is_nil(caller_pid) do
|
||||
with {:ok, character} <-
|
||||
WandererApp.Character.get_by_eve_id(character_eve_id),
|
||||
WandererApp.Character.get_by_eve_id("#{character_eve_id}"),
|
||||
{:ok, %{tracked: is_tracked}} <-
|
||||
do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||
# Determine which event to send based on tracking mode and previous state
|
||||
@@ -55,15 +55,19 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
Builds tracking data for all characters with access to a map.
|
||||
"""
|
||||
def build_tracking_data(map_id, current_user_id) do
|
||||
with {:ok, map} <- WandererApp.MapRepo.get(map_id, [:acls]),
|
||||
{:ok, character_settings} <-
|
||||
WandererApp.Character.Activity.get_map_character_settings(map_id),
|
||||
with {:ok, map} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
),
|
||||
{:ok, user_settings} <- WandererApp.MapUserSettingsRepo.get(map_id, current_user_id),
|
||||
{:ok, %{characters: characters_with_access}} <-
|
||||
WandererApp.Maps.load_characters(map, character_settings, current_user_id) do
|
||||
WandererApp.Maps.load_characters(map, current_user_id) do
|
||||
# Map characters to tracking data
|
||||
{:ok, characters_data} =
|
||||
build_character_tracking_data(characters_with_access, character_settings)
|
||||
build_character_tracking_data(characters_with_access)
|
||||
|
||||
{:ok, main_character} =
|
||||
get_main_character(user_settings, characters_with_access, characters_with_access)
|
||||
@@ -98,21 +102,19 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
|
||||
# Helper to build tracking data for each character
|
||||
defp build_character_tracking_data(characters, character_settings) do
|
||||
defp build_character_tracking_data(characters) do
|
||||
{:ok,
|
||||
Enum.map(characters, fn char ->
|
||||
setting = Enum.find(character_settings, &(&1.character_id == char.id))
|
||||
|
||||
%{
|
||||
character: char |> WandererAppWeb.MapEventHandler.map_ui_character_stat(),
|
||||
tracked: (setting && setting.tracked) || false
|
||||
tracked: char.tracked
|
||||
}
|
||||
end)}
|
||||
end
|
||||
|
||||
# Private implementation of update character tracking
|
||||
defp do_update_character_tracking(character, map_id, track, caller_pid) do
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map(map_id, character.id)
|
||||
WandererApp.MapCharacterSettingsRepo.get(map_id, character.id)
|
||||
|> case do
|
||||
# Untracking flow
|
||||
{:ok, %{tracked: true} = existing_settings} ->
|
||||
|
||||
@@ -11,49 +11,56 @@ defmodule WandererApp.Env do
|
||||
def vsn(), do: Application.spec(@app)[:vsn]
|
||||
|
||||
def git_sha(), do: get_key(:git_sha, "<GIT_SHA>")
|
||||
def base_url, do: get_key(:web_app_url, "<BASE_URL>")
|
||||
def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
|
||||
def invites, do: get_key(:invites, false)
|
||||
def base_url(), do: get_key(:web_app_url, "<BASE_URL>")
|
||||
def base_metrics_only(), do: get_key(:base_metrics_only, false)
|
||||
def custom_route_base_url(), do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
|
||||
def invites(), do: get_key(:invites, false)
|
||||
|
||||
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
|
||||
def websocket_events_enabled?, do: get_key(:websocket_events_enabled, false)
|
||||
def public_api_disabled?, do: get_key(:public_api_disabled, false)
|
||||
def map_subscriptions_enabled?(), do: get_key(:map_subscriptions_enabled, false)
|
||||
def websocket_events_enabled?(), do: get_key(:websocket_events_enabled, false)
|
||||
def public_api_disabled?(), do: get_key(:public_api_disabled, false)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "active_tracking_pool"
|
||||
)
|
||||
def active_tracking_pool, do: get_key(:active_tracking_pool, "default")
|
||||
def active_tracking_pool(), do: get_key(:active_tracking_pool, "default")
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "tracking_pool_max_size"
|
||||
)
|
||||
def tracking_pool_max_size, do: get_key(:tracking_pool_max_size, 300)
|
||||
def character_tracking_pause_disabled?, do: get_key(:character_tracking_pause_disabled, true)
|
||||
def character_api_disabled?, do: get_key(:character_api_disabled, false)
|
||||
def wanderer_kills_service_enabled?, do: get_key(:wanderer_kills_service_enabled, false)
|
||||
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
|
||||
def admins, do: get_key(:admins, [])
|
||||
def admin_username, do: get_key(:admin_username)
|
||||
def admin_password, do: get_key(:admin_password)
|
||||
def corp_wallet, do: get_key(:corp_wallet, "")
|
||||
def corp_wallet_eve_id, do: get_key(:corp_wallet_eve_id, "-1")
|
||||
def corp_eve_id, do: get_key(:corp_id, -1)
|
||||
def subscription_settings, do: get_key(:subscription_settings)
|
||||
def tracking_pool_max_size(), do: get_key(:tracking_pool_max_size, 300)
|
||||
def character_tracking_pause_disabled?(), do: get_key(:character_tracking_pause_disabled, true)
|
||||
def character_api_disabled?(), do: get_key(:character_api_disabled, false)
|
||||
def wanderer_kills_service_enabled?(), do: get_key(:wanderer_kills_service_enabled, false)
|
||||
def wallet_tracking_enabled?(), do: get_key(:wallet_tracking_enabled, false)
|
||||
def admins(), do: get_key(:admins, [])
|
||||
def admin_username(), do: get_key(:admin_username)
|
||||
def admin_password(), do: get_key(:admin_password)
|
||||
def corp_wallet(), do: get_key(:corp_wallet, "")
|
||||
def corp_wallet_eve_id(), do: get_key(:corp_wallet_eve_id, "-1")
|
||||
def corp_eve_id(), do: get_key(:corp_id, -1)
|
||||
def subscription_settings(), do: get_key(:subscription_settings)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "restrict_maps_creation"
|
||||
)
|
||||
def restrict_maps_creation?, do: get_key(:restrict_maps_creation, false)
|
||||
def restrict_maps_creation?(), do: get_key(:restrict_maps_creation, false)
|
||||
|
||||
def sse_enabled? do
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "restrict_acls_creation"
|
||||
)
|
||||
def restrict_acls_creation?(), do: get_key(:restrict_acls_creation, false)
|
||||
|
||||
def sse_enabled?() do
|
||||
Application.get_env(@app, :sse, [])
|
||||
|> Keyword.get(:enabled, false)
|
||||
end
|
||||
|
||||
def webhooks_enabled? do
|
||||
def webhooks_enabled?() do
|
||||
Application.get_env(@app, :external_events, [])
|
||||
|> Keyword.get(:webhooks_enabled, false)
|
||||
end
|
||||
@@ -62,19 +69,19 @@ defmodule WandererApp.Env do
|
||||
cache: WandererApp.Cache,
|
||||
key: "map-connection-auto-expire-hours"
|
||||
)
|
||||
def map_connection_auto_expire_hours, do: get_key(:map_connection_auto_expire_hours)
|
||||
def map_connection_auto_expire_hours(), do: get_key(:map_connection_auto_expire_hours)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map-connection-auto-eol-hours"
|
||||
)
|
||||
def map_connection_auto_eol_hours, do: get_key(:map_connection_auto_eol_hours)
|
||||
def map_connection_auto_eol_hours(), do: get_key(:map_connection_auto_eol_hours)
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map-connection-eol-expire-timeout-mins"
|
||||
)
|
||||
def map_connection_eol_expire_timeout_mins,
|
||||
def map_connection_eol_expire_timeout_mins(),
|
||||
do: get_key(:map_connection_eol_expire_timeout_mins)
|
||||
|
||||
def get_key(key, default \\ nil), do: Application.get_env(@app, key, default)
|
||||
@@ -83,7 +90,7 @@ defmodule WandererApp.Env do
|
||||
A single map containing environment variables
|
||||
made available to react
|
||||
"""
|
||||
def to_client_env do
|
||||
def to_client_env() do
|
||||
%{detailedKillsDisabled: not wanderer_kills_service_enabled?()}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -287,8 +287,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_alliance_info(eve_id, opts \\ []) do
|
||||
case _get_alliance_info(eve_id, "", opts) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
case get_alliance_info(eve_id, "", opts) do
|
||||
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
error -> error
|
||||
end
|
||||
@@ -309,8 +309,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts: [ttl: @ttl]
|
||||
)
|
||||
def get_corporation_info(eve_id, opts \\ []) do
|
||||
case _get_corporation_info(eve_id, "", opts) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
case get_corporation_info(eve_id, "", opts) do
|
||||
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
error -> error
|
||||
end
|
||||
@@ -327,7 +327,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
opts,
|
||||
@cache_opts
|
||||
) do
|
||||
{:ok, result} -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:ok, result} when is_map(result) -> {:ok, result |> Map.put("eve_id", eve_id)}
|
||||
{:error, error} -> {:error, error}
|
||||
error -> error
|
||||
end
|
||||
@@ -434,7 +434,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp get_auth_opts(opts), do: [auth: {:bearer, opts[:access_token]}]
|
||||
|
||||
defp _get_alliance_info(alliance_eve_id, info_path, opts),
|
||||
defp get_alliance_info(alliance_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
"/alliances/#{alliance_eve_id}/#{info_path}",
|
||||
@@ -442,7 +442,7 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
@cache_opts
|
||||
)
|
||||
|
||||
defp _get_corporation_info(corporation_eve_id, info_path, opts),
|
||||
defp get_corporation_info(corporation_eve_id, info_path, opts),
|
||||
do:
|
||||
get(
|
||||
"/corporations/#{corporation_eve_id}/#{info_path}",
|
||||
@@ -830,7 +830,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
expires_at,
|
||||
scopes
|
||||
) do
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at, :second)
|
||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Invalid grant error during token refresh",
|
||||
character_id: character_id,
|
||||
@@ -857,7 +858,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
expires_at,
|
||||
scopes
|
||||
) do
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at, :second)
|
||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Connection refused during token refresh",
|
||||
character_id: character_id,
|
||||
|
||||
@@ -51,7 +51,7 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
def new(map_id, event_type, payload) when is_binary(map_id) and is_map(payload) do
|
||||
if valid_event_type?(event_type) do
|
||||
%__MODULE__{
|
||||
id: Ulid.generate(System.system_time(:millisecond)),
|
||||
id: Ecto.ULID.generate(System.system_time(:millisecond)),
|
||||
map_id: map_id,
|
||||
type: event_type,
|
||||
payload: payload,
|
||||
@@ -97,7 +97,7 @@ defmodule WandererApp.ExternalEvents.Event do
|
||||
:locked,
|
||||
# ADD
|
||||
:temporary_name,
|
||||
# ADD
|
||||
# ADD
|
||||
:labels,
|
||||
# ADD
|
||||
:description,
|
||||
|
||||
@@ -448,7 +448,7 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
|
||||
"connected" ->
|
||||
%{
|
||||
"type" => "connection_status",
|
||||
"id" => event["id"] || Ulid.generate(),
|
||||
"id" => event["id"] || Ecto.ULID.generate(),
|
||||
"attributes" => %{
|
||||
"status" => "connected",
|
||||
"server_time" => payload["server_time"],
|
||||
@@ -465,7 +465,7 @@ defmodule WandererApp.ExternalEvents.JsonApiFormatter do
|
||||
# Use existing payload structure but wrap it in JSON:API format
|
||||
%{
|
||||
"type" => "events",
|
||||
"id" => event["id"] || Ulid.generate(),
|
||||
"id" => event["id"] || Ecto.ULID.generate(),
|
||||
"attributes" => payload,
|
||||
"relationships" => %{
|
||||
"map" => %{
|
||||
|
||||
@@ -248,6 +248,6 @@ defmodule WandererApp.ExternalEvents.MapEventRelay do
|
||||
defp datetime_to_ulid(datetime) do
|
||||
timestamp = DateTime.to_unix(datetime, :millisecond)
|
||||
# Create a ULID with the timestamp (rest will be zeros for comparison)
|
||||
Ulid.generate(timestamp)
|
||||
Ecto.ULID.generate(timestamp)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,8 +9,6 @@ defmodule WandererApp.Map.Audit do
|
||||
require Ash.Query
|
||||
require Logger
|
||||
|
||||
alias WandererApp.SecurityAudit
|
||||
|
||||
@week_seconds :timer.hours(24 * 7)
|
||||
@month_seconds @week_seconds * 4
|
||||
@audit_expired_seconds @month_seconds * 3
|
||||
@@ -38,17 +36,14 @@ defmodule WandererApp.Map.Audit do
|
||||
:ok
|
||||
end
|
||||
|
||||
def get_activity_query(map_id, period, activity) do
|
||||
SecurityAudit.get_map_activity_query(map_id, period, activity)
|
||||
end
|
||||
defdelegate get_map_activity_query(map_id, period, activity),
|
||||
to: WandererApp.SecurityAudit
|
||||
|
||||
def track_acl_event(event_type, metadata) do
|
||||
SecurityAudit.track_acl_event(event_type, metadata)
|
||||
end
|
||||
defdelegate track_acl_event(event_type, metadata),
|
||||
to: WandererApp.SecurityAudit
|
||||
|
||||
def track_map_event(event_type, metadata) do
|
||||
SecurityAudit.track_map_event(event_type, metadata)
|
||||
end
|
||||
defdelegate track_map_event(event_type, metadata),
|
||||
to: WandererApp.SecurityAudit
|
||||
|
||||
defp get_expired_at(), do: DateTime.utc_now() |> DateTime.add(-@audit_expired_seconds, :second)
|
||||
end
|
||||
|
||||
@@ -8,6 +8,19 @@ defmodule WandererApp.Map.Manager do
|
||||
require Logger
|
||||
|
||||
alias WandererApp.Map.Server
|
||||
alias WandererApp.Map.ServerSupervisor
|
||||
alias WandererApp.Api.MapSystemSignature
|
||||
|
||||
@maps_start_per_second 10
|
||||
@maps_start_interval 1000
|
||||
@maps_queue :maps_queue
|
||||
@garbage_collection_interval :timer.hours(1)
|
||||
@check_maps_queue_interval :timer.seconds(1)
|
||||
@signatures_cleanup_interval :timer.minutes(30)
|
||||
@delete_after_minutes 30
|
||||
|
||||
@pings_cleanup_interval :timer.minutes(10)
|
||||
@pings_expire_minutes 60
|
||||
|
||||
# Test-aware async task runner
|
||||
defp safe_async_task(fun) do
|
||||
@@ -25,20 +38,6 @@ defmodule WandererApp.Map.Manager do
|
||||
end
|
||||
end
|
||||
|
||||
alias WandererApp.Map.ServerSupervisor
|
||||
alias WandererApp.Api.MapSystemSignature
|
||||
|
||||
@maps_start_per_second 5
|
||||
@maps_start_interval 1000
|
||||
@maps_queue :maps_queue
|
||||
@garbage_collection_interval :timer.hours(1)
|
||||
@check_maps_queue_interval :timer.seconds(1)
|
||||
@signatures_cleanup_interval :timer.minutes(30)
|
||||
@delete_after_minutes 30
|
||||
|
||||
@pings_cleanup_interval :timer.minutes(10)
|
||||
@pings_expire_minutes 60
|
||||
|
||||
def start_map(map_id) when is_binary(map_id),
|
||||
do: WandererApp.Queue.push_uniq(@maps_queue, map_id)
|
||||
|
||||
@@ -247,22 +246,29 @@ defmodule WandererApp.Map.Manager do
|
||||
Logger.debug(fn -> "All maps started" end)
|
||||
else
|
||||
# In production, run async as normal
|
||||
tasks =
|
||||
for chunk <- chunks do
|
||||
task =
|
||||
Task.async(fn ->
|
||||
chunk
|
||||
|> Enum.map(&start_map_server/1)
|
||||
end)
|
||||
chunks
|
||||
|> Task.async_stream(
|
||||
fn chunk ->
|
||||
chunk
|
||||
|> Enum.map(&start_map_server/1)
|
||||
|
||||
:timer.sleep(@maps_start_interval)
|
||||
end,
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task,
|
||||
timeout: :timer.seconds(60)
|
||||
)
|
||||
|> Enum.each(fn result ->
|
||||
case result do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
task
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
Logger.debug(fn -> "Waiting for maps to start" end)
|
||||
Task.await_many(tasks)
|
||||
Logger.debug(fn -> "All maps started" end)
|
||||
Logger.info(fn -> "All maps started" end)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -88,11 +88,18 @@ defmodule WandererApp.Map.Server do
|
||||
|> map_pid!
|
||||
|> GenServer.cast({&Impl.remove_character/2, [character_id]})
|
||||
|
||||
def untrack_characters(map_id, character_ids) when is_binary(map_id),
|
||||
do:
|
||||
map_id
|
||||
|> map_pid!
|
||||
|> GenServer.cast({&Impl.untrack_characters/2, [character_ids]})
|
||||
def untrack_characters(map_id, character_ids) when is_binary(map_id) do
|
||||
map_id
|
||||
|> map_pid()
|
||||
|> case do
|
||||
pid when is_pid(pid) ->
|
||||
GenServer.cast(pid, {&Impl.untrack_characters/2, [character_ids]})
|
||||
|
||||
_ ->
|
||||
WandererApp.Cache.insert("map_#{map_id}:started", false)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
def add_system(map_id, system_info, user_id, character_id) when is_binary(map_id),
|
||||
do:
|
||||
|
||||
@@ -16,7 +16,13 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
systems
|
||||
|> Enum.flat_map(fn sys ->
|
||||
with {:ok, sigs} <- MapSystemSignature.by_system_id(sys.id) do
|
||||
sigs
|
||||
# Add solar_system_id to each signature and remove system_id
|
||||
Enum.map(sigs, fn sig ->
|
||||
sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, sys.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
end)
|
||||
else
|
||||
err ->
|
||||
Logger.error("[list_signatures] error: #{inspect(err)}")
|
||||
@@ -32,28 +38,70 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
def create_signature(
|
||||
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
|
||||
_conn,
|
||||
%{"solar_system_id" => _solar_system_id} = params
|
||||
) do
|
||||
attrs = Map.put(params, "character_eve_id", char_id)
|
||||
%{"solar_system_id" => solar_system_id} = params
|
||||
)
|
||||
when is_integer(solar_system_id) do
|
||||
# Convert solar_system_id to system_id for internal use
|
||||
with {:ok, system} <- MapSystem.by_map_id_and_solar_system_id(map_id, solar_system_id) do
|
||||
attrs =
|
||||
params
|
||||
|> Map.put("character_eve_id", char_id)
|
||||
|> Map.put("system_id", system.id)
|
||||
|> Map.delete("solar_system_id")
|
||||
|
||||
case Server.update_signatures(map_id, %{
|
||||
added_signatures: [attrs],
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: params["solar_system_id"],
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok ->
|
||||
{:ok, attrs}
|
||||
case Server.update_signatures(map_id, %{
|
||||
added_signatures: [attrs],
|
||||
updated_signatures: [],
|
||||
removed_signatures: [],
|
||||
solar_system_id: solar_system_id,
|
||||
character_id: char_id,
|
||||
user_id: user_id,
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok ->
|
||||
# Try to fetch the created signature to return with proper fields
|
||||
with {:ok, sigs} <-
|
||||
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
|
||||
sig when not is_nil(sig) <- List.first(sigs) do
|
||||
result =
|
||||
sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
{:ok, result}
|
||||
else
|
||||
_ ->
|
||||
# Fallback: return attrs with solar_system_id added
|
||||
attrs_result =
|
||||
attrs
|
||||
|> Map.put(:solar_system_id, solar_system_id)
|
||||
|> Map.drop(["system_id"])
|
||||
|
||||
{:ok, attrs_result}
|
||||
end
|
||||
|
||||
err ->
|
||||
Logger.error("[create_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
Logger.error(
|
||||
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
|
||||
)
|
||||
|
||||
{:error, :system_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
def create_signature(
|
||||
%{assigns: %{map_id: _map_id, owner_character_id: _char_id, owner_user_id: _user_id}} =
|
||||
_conn,
|
||||
%{"solar_system_id" => _invalid} = _params
|
||||
),
|
||||
do: {:error, :missing_params}
|
||||
|
||||
def create_signature(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
@@ -90,7 +138,18 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
delete_connection_with_sigs: false
|
||||
})
|
||||
|
||||
{:ok, attrs}
|
||||
# Fetch the updated signature to return with proper fields
|
||||
with {:ok, updated_sig} <- MapSystemSignature.by_id(sig_id) do
|
||||
result =
|
||||
updated_sig
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
{:ok, result}
|
||||
else
|
||||
_ -> {:ok, attrs}
|
||||
end
|
||||
else
|
||||
err ->
|
||||
Logger.error("[update_signature] Unexpected error: #{inspect(err)}")
|
||||
|
||||
@@ -59,6 +59,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
map_update = %{acls: map.acls, scope: map.scope}
|
||||
|
||||
WandererApp.Map.update_map(map_id, map_update)
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
broadcast_acl_updates({:ok, result}, map_id)
|
||||
|
||||
@@ -66,7 +67,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
|
||||
def handle_acl_updated(map_id, acl_id) do
|
||||
{:ok, map} =
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
@@ -74,8 +75,9 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
]
|
||||
)
|
||||
|
||||
if map.acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
|
||||
WandererApp.Map.update_map(map_id, %{acls: map.acls})
|
||||
if acls |> Enum.map(& &1.id) |> Enum.member?(acl_id) do
|
||||
WandererApp.Map.update_map(map_id, %{acls: acls})
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
:ok =
|
||||
acl_id
|
||||
@@ -85,7 +87,7 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
end
|
||||
|
||||
def handle_acl_deleted(map_id, _acl_id) do
|
||||
{:ok, map} =
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
@@ -93,7 +95,8 @@ defmodule WandererApp.Map.Server.AclsImpl do
|
||||
]
|
||||
)
|
||||
|
||||
WandererApp.Map.update_map(map_id, %{acls: map.acls})
|
||||
WandererApp.Map.update_map(map_id, %{acls: acls})
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
character_ids =
|
||||
map_id
|
||||
|
||||
@@ -59,7 +59,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
def update_tracked_characters(map_id) do
|
||||
Task.start_link(fn ->
|
||||
{:ok, map_tracked_character_ids} =
|
||||
{:ok, all_map_tracked_character_ids} =
|
||||
map_id
|
||||
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all()
|
||||
|> case do
|
||||
@@ -67,30 +67,19 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
|
||||
{:ok, tracked_characters} = WandererApp.Cache.lookup("tracked_characters", [])
|
||||
|
||||
map_active_tracked_characters =
|
||||
map_tracked_character_ids
|
||||
|> Enum.filter(fn character -> character in tracked_characters end)
|
||||
|
||||
{:ok, old_map_tracked_characters} =
|
||||
{:ok, actual_map_tracked_characters} =
|
||||
WandererApp.Cache.lookup("maps:#{map_id}:tracked_characters", [])
|
||||
|
||||
characters_to_remove = old_map_tracked_characters -- map_active_tracked_characters
|
||||
characters_to_remove = actual_map_tracked_characters -- all_map_tracked_character_ids
|
||||
|
||||
{:ok, invalidate_character_ids} =
|
||||
WandererApp.Cache.lookup(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
(invalidate_character_ids ++ characters_to_remove) |> Enum.uniq()
|
||||
characters_to_remove,
|
||||
fn ids ->
|
||||
(ids ++ characters_to_remove) |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
|
||||
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
@@ -98,7 +87,9 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
def untrack_characters(map_id, character_ids) do
|
||||
character_ids
|
||||
|> Enum.each(fn character_id ->
|
||||
is_character_map_active?(map_id, character_id)
|
||||
character_map_active = is_character_map_active?(map_id, character_id)
|
||||
|
||||
character_map_active
|
||||
|> untrack_character(map_id, character_id)
|
||||
end)
|
||||
end
|
||||
@@ -126,15 +117,18 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
def cleanup_characters(map_id, owner_id) do
|
||||
{:ok, invalidate_character_ids} =
|
||||
WandererApp.Cache.lookup(
|
||||
WandererApp.Cache.get_and_remove(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
|
||||
acls =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:acls, [])
|
||||
{:ok, %{acls: acls}} =
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
:owner_id,
|
||||
members: [:role, :eve_character_id, :eve_corporation_id, :eve_alliance_id]
|
||||
]
|
||||
)
|
||||
|
||||
invalidate_character_ids
|
||||
|> Task.async_stream(
|
||||
@@ -186,11 +180,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:error, reason} ->
|
||||
Logger.error("Error in cleanup_characters: #{inspect(reason)}")
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[]
|
||||
)
|
||||
end
|
||||
|
||||
defp remove_and_untrack_characters(map_id, character_ids) do
|
||||
@@ -224,86 +213,100 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
|
||||
def update_characters(%{map_id: map_id} = state) do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
try do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
|
||||
WandererApp.Cache.lookup!("maps:#{map_id}:tracked_characters", [])
|
||||
|> Enum.filter(fn character_id -> character_id in presence_character_ids end)
|
||||
|> Enum.map(fn character_id ->
|
||||
Task.start_link(fn ->
|
||||
character_updates =
|
||||
maybe_update_online(map_id, character_id) ++
|
||||
maybe_update_tracking_status(map_id, character_id) ++
|
||||
maybe_update_location(map_id, character_id) ++
|
||||
maybe_update_ship(map_id, character_id) ++
|
||||
maybe_update_alliance(map_id, character_id) ++
|
||||
maybe_update_corporation(map_id, character_id)
|
||||
presence_character_ids
|
||||
|> Task.async_stream(
|
||||
fn character_id ->
|
||||
character_updates =
|
||||
maybe_update_online(map_id, character_id) ++
|
||||
maybe_update_tracking_status(map_id, character_id) ++
|
||||
maybe_update_location(map_id, character_id) ++
|
||||
maybe_update_ship(map_id, character_id) ++
|
||||
maybe_update_alliance(map_id, character_id) ++
|
||||
maybe_update_corporation(map_id, character_id)
|
||||
|
||||
character_updates
|
||||
|> Enum.filter(fn update -> update != :skip end)
|
||||
|> Enum.map(fn update ->
|
||||
update
|
||||
|> case do
|
||||
{:character_location, location_info, old_location_info} ->
|
||||
update_location(
|
||||
character_id,
|
||||
location_info,
|
||||
old_location_info,
|
||||
state
|
||||
)
|
||||
character_updates
|
||||
|> Enum.filter(fn update -> update != :skip end)
|
||||
|> Enum.map(fn update ->
|
||||
update
|
||||
|> case do
|
||||
{:character_location, location_info, old_location_info} ->
|
||||
update_location(
|
||||
character_id,
|
||||
location_info,
|
||||
old_location_info,
|
||||
state
|
||||
)
|
||||
|
||||
:broadcast
|
||||
:broadcast
|
||||
|
||||
{:character_ship, _info} ->
|
||||
:broadcast
|
||||
{:character_ship, _info} ->
|
||||
:broadcast
|
||||
|
||||
{:character_online, _info} ->
|
||||
:broadcast
|
||||
{:character_online, _info} ->
|
||||
:broadcast
|
||||
|
||||
{:character_tracking, _info} ->
|
||||
:broadcast
|
||||
{:character_tracking, _info} ->
|
||||
:broadcast
|
||||
|
||||
{:character_alliance, _info} ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[character_id],
|
||||
fn ids ->
|
||||
[character_id | ids]
|
||||
end
|
||||
)
|
||||
{:character_alliance, _info} ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[character_id],
|
||||
fn ids ->
|
||||
[character_id | ids] |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
|
||||
:broadcast
|
||||
:broadcast
|
||||
|
||||
{:character_corporation, _info} ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[character_id],
|
||||
fn ids ->
|
||||
[character_id | ids]
|
||||
end
|
||||
)
|
||||
{:character_corporation, _info} ->
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
[character_id],
|
||||
fn ids ->
|
||||
[character_id | ids] |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
|
||||
:broadcast
|
||||
:broadcast
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(fn update -> update != :skip end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.each(fn update ->
|
||||
case update do
|
||||
:broadcast ->
|
||||
update_character(map_id, character_id)
|
||||
_ ->
|
||||
:skip
|
||||
end
|
||||
end)
|
||||
|> Enum.filter(fn update -> update != :skip end)
|
||||
|> Enum.uniq()
|
||||
|> Enum.each(fn update ->
|
||||
case update do
|
||||
:broadcast ->
|
||||
update_character(map_id, character_id)
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end)
|
||||
|
||||
:ok
|
||||
:ok
|
||||
end,
|
||||
timeout: :timer.seconds(15),
|
||||
max_concurrency: System.schedulers_online(),
|
||||
on_timeout: :kill_task
|
||||
)
|
||||
|> Enum.each(fn
|
||||
{:ok, _result} -> :ok
|
||||
{:error, reason} -> Logger.error("Error in update_characters: #{inspect(reason)}")
|
||||
end)
|
||||
end)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("""
|
||||
[Map Server] update_characters => exception: #{Exception.message(e)}
|
||||
#{Exception.format_stacktrace(__STACKTRACE__)}
|
||||
""")
|
||||
end
|
||||
end
|
||||
|
||||
defp update_character(map_id, character_id) do
|
||||
@@ -373,6 +376,8 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_map_character(map_id, character_id, not_present: true)
|
||||
|
||||
WandererApp.Cache.delete("character:#{character.id}:tracking_paused")
|
||||
|
||||
add_character(%{map_id: map_id}, character, true)
|
||||
|
||||
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
|
||||
|
||||
@@ -72,6 +72,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
@connection_time_status_eol 1
|
||||
@connection_type_wormhole 0
|
||||
@connection_type_stargate 1
|
||||
@connection_type_bridge 2
|
||||
@medium_ship_size 1
|
||||
|
||||
def get_connection_auto_expire_hours(), do: WandererApp.Env.map_connection_auto_expire_hours()
|
||||
@@ -146,6 +147,18 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
state
|
||||
end
|
||||
|
||||
def update_connection_type(
|
||||
%{map_id: map_id} = state,
|
||||
%{
|
||||
solar_system_source_id: solar_system_source_id,
|
||||
solar_system_target_id: solar_system_target_id,
|
||||
character_id: character_id
|
||||
} = _connection_info,
|
||||
type
|
||||
) do
|
||||
state
|
||||
end
|
||||
|
||||
def get_connection_info(
|
||||
%{map_id: map_id} = _state,
|
||||
%{
|
||||
@@ -239,7 +252,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
} ->
|
||||
connection_start_time = get_start_time(map_id, connection_id)
|
||||
|
||||
type != @connection_type_stargate &&
|
||||
type == @connection_type_wormhole &&
|
||||
DateTime.diff(DateTime.utc_now(), connection_start_time, :hour) >=
|
||||
connection_auto_eol_hours &&
|
||||
is_connection_valid(
|
||||
@@ -295,7 +308,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
)
|
||||
|
||||
not is_connection_exist ||
|
||||
(type != @connection_type_stargate && is_connection_valid &&
|
||||
(type == @connection_type_wormhole && is_connection_valid &&
|
||||
DateTime.diff(DateTime.utc_now(), connection_mark_eol_time, :hour) >=
|
||||
connection_auto_expire_hours - connection_auto_eol_hours +
|
||||
+connection_eol_expire_timeout_hours)
|
||||
@@ -527,33 +540,30 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
|
||||
def is_connection_valid(scope, from_solar_system_id, to_solar_system_id)
|
||||
when not is_nil(from_solar_system_id) and not is_nil(to_solar_system_id) do
|
||||
{:ok, known_jumps} =
|
||||
WandererApp.Api.MapSolarSystemJumps.find(%{
|
||||
before_system_id: from_solar_system_id,
|
||||
current_system_id: to_solar_system_id
|
||||
})
|
||||
|
||||
{:ok, from_system_static_info} = get_system_static_info(from_solar_system_id)
|
||||
{:ok, to_system_static_info} = get_system_static_info(to_solar_system_id)
|
||||
|
||||
case scope do
|
||||
:wormholes ->
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
|
||||
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
|
||||
known_jumps |> Enum.empty?()
|
||||
|
||||
:stargates ->
|
||||
# For stargates, we need to check:
|
||||
# 1. Both systems are in known space (HS, LS, NS)
|
||||
# 2. There is a known jump between them
|
||||
# 3. Neither system is prohibited
|
||||
from_system_static_info.system_class in @known_space and
|
||||
to_system_static_info.system_class in @known_space and
|
||||
with {:ok, known_jumps} <- find_solar_system_jump(from_solar_system_id, to_solar_system_id),
|
||||
{:ok, from_system_static_info} <- get_system_static_info(from_solar_system_id),
|
||||
{:ok, to_system_static_info} <- get_system_static_info(to_solar_system_id) do
|
||||
case scope do
|
||||
:wormholes ->
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (known_jumps |> Enum.empty?())
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (@prohibited_systems |> Enum.member?(from_solar_system_id)) and
|
||||
not (@prohibited_systems |> Enum.member?(to_solar_system_id)) and
|
||||
known_jumps |> Enum.empty?()
|
||||
|
||||
:stargates ->
|
||||
# For stargates, we need to check:
|
||||
# 1. Both systems are in known space (HS, LS, NS)
|
||||
# 2. There is a known jump between them
|
||||
# 3. Neither system is prohibited
|
||||
from_system_static_info.system_class in @known_space and
|
||||
to_system_static_info.system_class in @known_space and
|
||||
not is_prohibited_system_class?(from_system_static_info.system_class) and
|
||||
not is_prohibited_system_class?(to_system_static_info.system_class) and
|
||||
not (known_jumps |> Enum.empty?())
|
||||
end
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
@@ -570,6 +580,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
end
|
||||
end
|
||||
|
||||
defp find_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
case WandererApp.CachedInfo.get_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
{:ok, jump} when not is_nil(jump) -> {:ok, [jump]}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_system_static_info(solar_system_id) do
|
||||
case WandererApp.CachedInfo.get_system_static_info(solar_system_id) do
|
||||
{:ok, system_static_info} when not is_nil(system_static_info) ->
|
||||
|
||||
@@ -25,14 +25,14 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
]
|
||||
|
||||
@systems_cleanup_timeout :timer.minutes(30)
|
||||
@characters_cleanup_timeout :timer.minutes(1)
|
||||
@characters_cleanup_timeout :timer.minutes(5)
|
||||
@connections_cleanup_timeout :timer.minutes(2)
|
||||
|
||||
@pubsub_client Application.compile_env(:wanderer_app, :pubsub_client)
|
||||
@backup_state_timeout :timer.minutes(1)
|
||||
@update_presence_timeout :timer.seconds(5)
|
||||
@update_characters_timeout :timer.seconds(1)
|
||||
@update_tracked_characters_timeout :timer.seconds(1)
|
||||
@update_tracked_characters_timeout :timer.minutes(1)
|
||||
|
||||
def new(), do: __struct__()
|
||||
def new(args), do: __struct__(args)
|
||||
@@ -96,11 +96,17 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
)
|
||||
|
||||
Process.send_after(self(), :update_characters, @update_characters_timeout)
|
||||
Process.send_after(self(), :update_tracked_characters, 100)
|
||||
|
||||
Process.send_after(
|
||||
self(),
|
||||
:update_tracked_characters,
|
||||
@update_tracked_characters_timeout
|
||||
)
|
||||
|
||||
Process.send_after(self(), :update_presence, @update_presence_timeout)
|
||||
Process.send_after(self(), :cleanup_connections, 5_000)
|
||||
Process.send_after(self(), :cleanup_systems, 10_000)
|
||||
Process.send_after(self(), :cleanup_characters, :timer.minutes(5))
|
||||
Process.send_after(self(), :cleanup_characters, @characters_cleanup_timeout)
|
||||
Process.send_after(self(), :backup_state, @backup_state_timeout)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:started", true)
|
||||
@@ -127,6 +133,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
Logger.debug(fn -> "Stopping map server for #{map_id}" end)
|
||||
|
||||
WandererApp.Cache.delete("map_#{map_id}:started")
|
||||
WandererApp.Cache.delete("map_characters-#{map_id}")
|
||||
|
||||
:telemetry.execute([:wanderer_app, :map, :stopped], %{count: 1})
|
||||
|
||||
@@ -278,7 +285,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
end
|
||||
|
||||
def handle_event({:acl_deleted, %{acl_id: acl_id}}, %{map_id: map_id} = state) do
|
||||
AclsImpl.handle_acl_updated(map_id, acl_id)
|
||||
AclsImpl.handle_acl_deleted(map_id, acl_id)
|
||||
|
||||
state
|
||||
end
|
||||
@@ -580,18 +587,27 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
{:ok, presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", [])
|
||||
|
||||
characters_ids =
|
||||
map_id
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:characters, [])
|
||||
{:ok, old_presence_character_ids} =
|
||||
WandererApp.Cache.lookup("map_#{map_id}:old_presence_character_ids", [])
|
||||
|
||||
new_present_character_ids =
|
||||
presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(old_presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
not_present_character_ids =
|
||||
characters_ids
|
||||
old_presence_character_ids
|
||||
|> Enum.filter(fn character_id ->
|
||||
not Enum.member?(presence_character_ids, character_id)
|
||||
end)
|
||||
|
||||
CharactersImpl.track_characters(map_id, presence_character_ids)
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:old_presence_character_ids",
|
||||
presence_character_ids
|
||||
)
|
||||
|
||||
CharactersImpl.track_characters(map_id, new_present_character_ids)
|
||||
CharactersImpl.untrack_characters(map_id, not_present_character_ids)
|
||||
|
||||
broadcast!(
|
||||
|
||||
@@ -114,6 +114,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
deleted_sig,
|
||||
Map.take(sig, [
|
||||
:name,
|
||||
:temporary_name,
|
||||
:description,
|
||||
:kind,
|
||||
:group,
|
||||
@@ -239,6 +240,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
system_id: system_id,
|
||||
eve_id: sig["eve_id"],
|
||||
name: sig["name"],
|
||||
temporary_name: sig["temporary_name"],
|
||||
description: Map.get(sig, "description"),
|
||||
kind: sig["kind"],
|
||||
group: sig["group"],
|
||||
|
||||
@@ -94,13 +94,22 @@ defmodule WandererApp.Maps do
|
||||
end
|
||||
end
|
||||
|
||||
def load_characters(map, character_settings, user_id) do
|
||||
def load_characters(map, user_id) when not is_nil(map) do
|
||||
{:ok, user_characters} =
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: user_id})
|
||||
|
||||
characters =
|
||||
map_available_characters =
|
||||
map
|
||||
|> get_map_available_characters(user_characters)
|
||||
|
||||
{:ok, character_settings} =
|
||||
WandererApp.MapCharacterSettingsRepo.get_by_map_filtered(
|
||||
map.id,
|
||||
map_available_characters |> Enum.map(& &1.id)
|
||||
)
|
||||
|
||||
characters =
|
||||
map_available_characters
|
||||
|> Enum.map(fn c ->
|
||||
map_character(c, character_settings |> Enum.find(&(&1.character_id == c.id)))
|
||||
end)
|
||||
@@ -108,6 +117,8 @@ defmodule WandererApp.Maps do
|
||||
{:ok, %{characters: characters}}
|
||||
end
|
||||
|
||||
def load_characters(_map, _user_id), do: {:ok, %{characters: []}}
|
||||
|
||||
def map_character(
|
||||
%{
|
||||
name: name,
|
||||
@@ -176,48 +187,57 @@ defmodule WandererApp.Maps do
|
||||
tracked: tracked
|
||||
}
|
||||
|
||||
@decorate cacheable(
|
||||
cache: WandererApp.Cache,
|
||||
key: "map_characters-#{map_id}",
|
||||
opts: [ttl: :timer.seconds(2)]
|
||||
)
|
||||
defp _get_map_characters(%{id: map_id} = map) do
|
||||
map_acls =
|
||||
map.acls
|
||||
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
|
||||
defp get_map_characters(%{id: map_id} = map) do
|
||||
WandererApp.Cache.lookup!("map_characters-#{map_id}")
|
||||
|> case do
|
||||
nil ->
|
||||
map_acls =
|
||||
map.acls
|
||||
|> Enum.map(fn acl -> acl |> Ash.load!(:members) end)
|
||||
|
||||
map_acl_owner_ids =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.owner_id end)
|
||||
map_acl_owner_ids =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.owner_id end)
|
||||
|
||||
map_members =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.members end)
|
||||
|> List.flatten()
|
||||
|> Enum.filter(fn member -> member.role != :blocked end)
|
||||
map_members =
|
||||
map_acls
|
||||
|> Enum.map(fn acl -> acl.members end)
|
||||
|> List.flatten()
|
||||
|> Enum.filter(fn member -> member.role != :blocked end)
|
||||
|
||||
map_member_eve_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|
||||
|> Enum.map(fn member -> member.eve_character_id end)
|
||||
map_member_eve_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_character_id) end)
|
||||
|> Enum.map(fn member -> member.eve_character_id end)
|
||||
|
||||
map_member_corporation_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|
||||
|> Enum.map(fn member -> member.eve_corporation_id end)
|
||||
map_member_corporation_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_corporation_id) end)
|
||||
|> Enum.map(fn member -> member.eve_corporation_id end)
|
||||
|
||||
map_member_alliance_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|
||||
|> Enum.map(fn member -> member.eve_alliance_id end)
|
||||
map_member_alliance_ids =
|
||||
map_members
|
||||
|> Enum.filter(fn member -> not is_nil(member.eve_alliance_id) end)
|
||||
|> Enum.map(fn member -> member.eve_alliance_id end)
|
||||
|
||||
{:ok,
|
||||
%{
|
||||
map_acl_owner_ids: map_acl_owner_ids,
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}}
|
||||
map_characters =
|
||||
%{
|
||||
map_acl_owner_ids: map_acl_owner_ids,
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_characters-#{map_id}",
|
||||
map_characters
|
||||
)
|
||||
|
||||
{:ok, map_characters}
|
||||
|
||||
map_characters ->
|
||||
{:ok, map_characters}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_map_available_characters(map, user_characters) do
|
||||
@@ -227,7 +247,7 @@ defmodule WandererApp.Maps do
|
||||
map_member_eve_ids: map_member_eve_ids,
|
||||
map_member_corporation_ids: map_member_corporation_ids,
|
||||
map_member_alliance_ids: map_member_alliance_ids
|
||||
}} = _get_map_characters(map)
|
||||
}} = get_map_characters(map)
|
||||
|
||||
user_characters
|
||||
|> Enum.filter(fn c ->
|
||||
|
||||
@@ -29,15 +29,24 @@ defmodule WandererApp.Metrics.PromExPlugin do
|
||||
|
||||
@impl true
|
||||
def event_metrics(_opts) do
|
||||
[
|
||||
base_metrics = [
|
||||
user_event_metrics(),
|
||||
character_event_metrics(),
|
||||
map_event_metrics(),
|
||||
map_subscription_metrics(),
|
||||
map_subscription_metrics()
|
||||
]
|
||||
|
||||
advanced_metrics = [
|
||||
character_event_metrics(),
|
||||
characters_distribution_event_metrics(),
|
||||
esi_event_metrics(),
|
||||
json_api_metrics()
|
||||
]
|
||||
|
||||
if WandererApp.Env.base_metrics_only() do
|
||||
base_metrics
|
||||
else
|
||||
base_metrics ++ advanced_metrics
|
||||
end
|
||||
end
|
||||
|
||||
defp user_event_metrics do
|
||||
@@ -227,8 +236,8 @@ defmodule WandererApp.Metrics.PromExPlugin do
|
||||
defp get_esi_error_tag_values(metadata) do
|
||||
%{
|
||||
endpoint: Map.get(metadata, :endpoint, "unknown"),
|
||||
error_type: to_string(Map.get(metadata, :error_type, "unknown")),
|
||||
tracking_pool: Map.get(metadata, :tracking_pool, "unknown")
|
||||
error_type: inspect(Map.get(metadata, :error_type, "unknown")),
|
||||
tracking_pool: Map.get(metadata, :tracking_pool, "default")
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ defmodule WandererApp.MapChainPassagesRepo do
|
||||
to
|
||||
)
|
||||
|> case do
|
||||
{:ok, connection} ->
|
||||
{:ok, %{inserted_at: inserted_at} = _connection} when not is_nil(inserted_at) ->
|
||||
{:ok, from_passages} =
|
||||
WandererApp.Api.MapChainPassages.by_connection(%{
|
||||
map_id: map_id,
|
||||
from: from,
|
||||
to: to,
|
||||
after: connection.inserted_at
|
||||
after: inserted_at
|
||||
})
|
||||
|
||||
{:ok, to_passages} =
|
||||
@@ -22,7 +22,7 @@ defmodule WandererApp.MapChainPassagesRepo do
|
||||
map_id: map_id,
|
||||
from: to,
|
||||
to: from,
|
||||
after: connection.inserted_at
|
||||
after: inserted_at
|
||||
})
|
||||
|
||||
from_passages =
|
||||
@@ -39,7 +39,7 @@ defmodule WandererApp.MapChainPassagesRepo do
|
||||
|
||||
{:ok, passages}
|
||||
|
||||
{:error, _error} ->
|
||||
_error ->
|
||||
{:ok, []}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -53,20 +53,8 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
def get_tracked_by_map_all(map_id),
|
||||
do: WandererApp.Api.MapCharacterSettings.tracked_by_map_all(%{map_id: map_id})
|
||||
|
||||
def get_by_map(map_id, character_id) do
|
||||
case get_by_map_filtered(map_id, [character_id]) do
|
||||
{:ok, [setting | _]} ->
|
||||
{:ok, setting}
|
||||
|
||||
{:ok, []} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
def track(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: settings.map_id,
|
||||
@@ -75,6 +63,7 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def untrack(settings) do
|
||||
{:ok, _} = get(settings.map_id, settings.character_id)
|
||||
# Only update the tracked field, preserving other fields
|
||||
WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: settings.map_id,
|
||||
@@ -83,22 +72,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def track!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.track(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case track(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to track: #{inspect(error)}"
|
||||
error -> raise "Failed to track: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
def untrack!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.untrack(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case untrack(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to untrack: #{inspect(error)}"
|
||||
error -> raise "Failed to untrack: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -117,22 +100,16 @@ defmodule WandererApp.MapCharacterSettingsRepo do
|
||||
end
|
||||
|
||||
def follow!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.follow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case follow(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to follow: #{inspect(error)}"
|
||||
error -> raise "Failed to follow: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
def unfollow!(settings) do
|
||||
case WandererApp.Api.MapCharacterSettings.unfollow(%{
|
||||
map_id: settings.map_id,
|
||||
character_id: settings.character_id
|
||||
}) do
|
||||
case unfollow(settings) do
|
||||
{:ok, result} -> result
|
||||
{:error, error} -> raise "Failed to unfollow: #{inspect(error)}"
|
||||
error -> raise "Failed to unfollow: #{inspect(error)}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ defmodule WandererApp.SecurityAudit do
|
||||
}
|
||||
|
||||
# Store in database
|
||||
store_audit_entry(audit_entry)
|
||||
# store_audit_entry(audit_entry)
|
||||
|
||||
# Send to telemetry for monitoring
|
||||
emit_telemetry_event(audit_entry)
|
||||
@@ -489,11 +489,11 @@ defmodule WandererApp.SecurityAudit do
|
||||
|
||||
defp store_audit_entry(audit_entry) do
|
||||
# Handle async processing if enabled
|
||||
if async_enabled?() do
|
||||
WandererApp.SecurityAudit.AsyncProcessor.log_event(audit_entry)
|
||||
else
|
||||
do_store_audit_entry(audit_entry)
|
||||
end
|
||||
# if async_enabled?() do
|
||||
# WandererApp.SecurityAudit.AsyncProcessor.log_event(audit_entry)
|
||||
# else
|
||||
# do_store_audit_entry(audit_entry)
|
||||
# end
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -195,7 +195,7 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
|
||||
tracking_pool = WandererApp.Character.TrackingConfigUtils.get_active_pool!()
|
||||
|
||||
base_options = [
|
||||
redirect_uri: callback_url(conn),
|
||||
redirect_uri: "#{WandererApp.Env.base_url()}/auth/eve/callback",
|
||||
with_wallet: with_wallet,
|
||||
is_admin?: is_admin?,
|
||||
tracking_pool: tracking_pool
|
||||
|
||||
@@ -596,10 +596,24 @@ defmodule WandererAppWeb.MapAccessListAPIController do
|
||||
acl -> acl.id
|
||||
end)
|
||||
|
||||
updated_acls = current_acl_ids ++ [new_acl_id]
|
||||
updated_acls =
|
||||
if new_acl_id in current_acl_ids do
|
||||
current_acl_ids
|
||||
else
|
||||
current_acl_ids ++ [new_acl_id]
|
||||
end
|
||||
|
||||
case WandererApp.Api.Map.update_acls(loaded_map, %{acls: updated_acls}) do
|
||||
{:ok, updated_map} ->
|
||||
# Only broadcast if we actually added a new ACL
|
||||
unless new_acl_id in current_acl_ids do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"maps:#{loaded_map.id}",
|
||||
{:map_acl_updated, [new_acl_id], []}
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, updated_map}
|
||||
|
||||
{:error, error} ->
|
||||
|
||||
@@ -192,6 +192,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_added
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -199,6 +201,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member added event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(new_member)})
|
||||
end
|
||||
|
||||
@@ -300,6 +305,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_updated
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -307,6 +314,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member updated event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{data: member_to_json(updated_membership)})
|
||||
end
|
||||
|
||||
@@ -385,6 +395,8 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
:acl_member_removed
|
||||
) do
|
||||
:ok ->
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
|
||||
{:error, broadcast_error} ->
|
||||
@@ -392,6 +404,9 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
"Failed to broadcast ACL member removed event: #{inspect(broadcast_error)}"
|
||||
)
|
||||
|
||||
# Still broadcast internal message even if external broadcast fails
|
||||
broadcast_acl_updated(acl_id)
|
||||
|
||||
json(conn, %{ok: true})
|
||||
end
|
||||
|
||||
@@ -417,6 +432,14 @@ defmodule WandererAppWeb.AccessListMemberAPIController do
|
||||
# Private Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
defp broadcast_acl_updated(acl_id) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"acls:#{acl_id}",
|
||||
{:acl_updated, %{acl_id: acl_id}}
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
defp member_to_json(member) do
|
||||
base = %{
|
||||
|
||||
@@ -52,11 +52,7 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
|
||||
defp establish_sse_connection(conn, map_id, api_key, params) do
|
||||
# Parse event filter if provided
|
||||
event_filter =
|
||||
case Map.get(params, "events") do
|
||||
nil -> :all
|
||||
events -> EventFilter.parse(events)
|
||||
end
|
||||
event_filter = EventFilter.parse(Map.get(params, "events"))
|
||||
|
||||
# Parse format parameter
|
||||
event_format = Map.get(params, "format", "legacy")
|
||||
@@ -82,7 +78,7 @@ defmodule WandererAppWeb.Api.EventsController do
|
||||
send_event(
|
||||
conn,
|
||||
%{
|
||||
id: Ulid.generate(),
|
||||
id: Ecto.ULID.generate(),
|
||||
event: "connected",
|
||||
data: %{
|
||||
map_id: map_id,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<section class="prose prose-lg max-w-full w-full leading-normal tracking-normal text-indigo-400 bg-cover bg-fixed flex items-center justify-center">
|
||||
<section class="prose prose-lg max-w-full w-full leading-normal tracking-normal text-indigo-400 bg-cover bg-fixed flex items-center justify-center">
|
||||
<canvas id="bg-canvas"></canvas>
|
||||
<div class="h-full w-full flex flex-col items-center">
|
||||
<!--Main-->
|
||||
@@ -26,23 +26,71 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="carousel carousel-center bg-neutral rounded-box max-w-[80%] space-x-4 p-4">
|
||||
<%= for post <- @posts do %>
|
||||
<.link class="group carousel-item relative" navigate={~p"/news/#{post.id}"}>
|
||||
<div class="artboard-horizontal phone-1 relative hover:text-white mt-10">
|
||||
<img
|
||||
class="rounded-lg shadow-lg block !w-[400px] !h-[200px] opacity-75"
|
||||
src={post.cover_image_uri}
|
||||
/>
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black opacity-75 group-hover:opacity-25 transition-opacity duration-300">
|
||||
<div id="posts-container" class="bg-neutral rounded-box max-w-[90%] p-4 max-h-[60vh] overflow-y-auto">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||||
<%= for post <- @posts do %>
|
||||
<.link class="group carousel-item relative" navigate={~p"/news/#{post.id}"}>
|
||||
<div class="artboard-horizontal phone-1 relative hover:text-white">
|
||||
<img
|
||||
class="rounded-lg shadow-lg block !w-[300px] !h-[180px] opacity-75"
|
||||
src={post.cover_image_uri}
|
||||
/>
|
||||
<div class="absolute rounded-lg top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black opacity-75 group-hover:opacity-25 transition-opacity duration-300">
|
||||
</div>
|
||||
<h3 class="absolute bottom-4 left-14 !text-md font-bold break-normal pt-6 pb-2 ccp-font text-white">
|
||||
{post.title}
|
||||
</h3>
|
||||
</div>
|
||||
<h3 class="absolute bottom-4 left-14 font-bold break-normal pt-6 pb-2 ccp-font text-white">
|
||||
{post.title}
|
||||
</h3>
|
||||
</div>
|
||||
</.link>
|
||||
<% end %>
|
||||
</.link>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const postsContainer = document.getElementById('posts-container');
|
||||
if (!postsContainer) return;
|
||||
|
||||
let scrollSpeed = 0.5; // pixels per frame
|
||||
let isScrolling = true;
|
||||
let scrollDirection = 1; // 1 for down, -1 for up
|
||||
|
||||
function autoScroll() {
|
||||
if (!isScrolling) return;
|
||||
|
||||
const maxScroll = postsContainer.scrollHeight - postsContainer.clientHeight;
|
||||
|
||||
if (maxScroll <= 0) return; // No need to scroll if content fits
|
||||
|
||||
postsContainer.scrollTop += scrollSpeed * scrollDirection;
|
||||
|
||||
// Reverse direction when reaching top or bottom
|
||||
if (postsContainer.scrollTop >= maxScroll) {
|
||||
scrollDirection = -1;
|
||||
} else if (postsContainer.scrollTop <= 0) {
|
||||
scrollDirection = 1;
|
||||
}
|
||||
|
||||
requestAnimationFrame(autoScroll);
|
||||
}
|
||||
|
||||
// Pause scrolling on hover
|
||||
postsContainer.addEventListener('mouseenter', () => {
|
||||
isScrolling = false;
|
||||
});
|
||||
|
||||
// Resume scrolling when mouse leaves
|
||||
postsContainer.addEventListener('mouseleave', () => {
|
||||
isScrolling = true;
|
||||
requestAnimationFrame(autoScroll);
|
||||
});
|
||||
|
||||
// Start autoscroll after a delay
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(autoScroll);
|
||||
}, 2000);
|
||||
});
|
||||
</script>
|
||||
<%!-- <div class="carousel carousel-center !bg-neutral rounded-box max-w-4xl space-x-6 p-4">
|
||||
|
||||
</div> --%>
|
||||
|
||||
@@ -113,7 +113,7 @@ defmodule WandererAppWeb.MapAuditAPIController do
|
||||
def index(conn, params) do
|
||||
with {:ok, map_id} <- APIUtils.fetch_map_id(params),
|
||||
{:ok, period} <- APIUtils.require_param(params, "period"),
|
||||
query <- WandererApp.Map.Audit.get_activity_query(map_id, period, "all"),
|
||||
query <- WandererApp.Map.Audit.get_map_activity_query(map_id, period, "all"),
|
||||
{:ok, data} <-
|
||||
Ash.read(query) do
|
||||
data = Enum.map(data, &map_audit_event_to_json/1)
|
||||
|
||||
@@ -15,7 +15,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
type: :object,
|
||||
properties: %{
|
||||
id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
system_id: %OpenApiSpex.Schema{type: :string, format: :uuid},
|
||||
solar_system_id: %OpenApiSpex.Schema{type: :integer},
|
||||
eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
character_eve_id: %OpenApiSpex.Schema{type: :string},
|
||||
name: %OpenApiSpex.Schema{type: :string, nullable: true},
|
||||
@@ -31,13 +31,13 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
},
|
||||
required: [
|
||||
:id,
|
||||
:system_id,
|
||||
:solar_system_id,
|
||||
:eve_id,
|
||||
:character_eve_id
|
||||
],
|
||||
example: %{
|
||||
id: "sig-uuid-1",
|
||||
system_id: "sys-uuid-1",
|
||||
solar_system_id: 30_000_142,
|
||||
eve_id: "ABC-123",
|
||||
character_eve_id: "123456789",
|
||||
name: "Wormhole K162",
|
||||
@@ -122,7 +122,15 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
{:ok, signature} ->
|
||||
case WandererApp.Api.MapSystem.by_id(signature.system_id) do
|
||||
{:ok, system} when system.map_id == map_id ->
|
||||
json(conn, %{data: signature})
|
||||
# Add solar_system_id and remove system_id
|
||||
# Convert to a plain map to avoid encoder issues
|
||||
signature_data =
|
||||
signature
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
json(conn, %{data: signature_data})
|
||||
|
||||
_ ->
|
||||
conn |> put_status(:not_found) |> json(%{error: "Signature not found"})
|
||||
|
||||
@@ -13,6 +13,7 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
|
||||
import Plug.Conn
|
||||
|
||||
alias Plug.Crypto
|
||||
alias WandererApp.Api.User
|
||||
alias WandererApp.SecurityAudit
|
||||
alias WandererApp.Audit.RequestContext
|
||||
@@ -140,43 +141,60 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
|
||||
defp authenticate_bearer_token(conn) do
|
||||
case get_req_header(conn, "authorization") do
|
||||
["Bearer " <> token] ->
|
||||
validate_api_token(token)
|
||||
validate_api_token(conn, token)
|
||||
|
||||
_ ->
|
||||
{:error, "Missing or invalid authorization header"}
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_api_token(token) do
|
||||
# Look up the map by its public API key
|
||||
case find_map_by_api_key(token) do
|
||||
{:ok, map} when not is_nil(map) ->
|
||||
# Get the actual owner of the map
|
||||
case User.by_id(map.owner_id, load: :characters) do
|
||||
{:ok, user} ->
|
||||
# Return the map owner as the authenticated user
|
||||
{:ok, user, map}
|
||||
defp validate_api_token(conn, token) do
|
||||
# Check for map identifier in path params
|
||||
# According to PR feedback, routes supply params["map_identifier"]
|
||||
case conn.params["map_identifier"] do
|
||||
nil ->
|
||||
# No map identifier in path - this might be a general API endpoint
|
||||
# For now, we'll return an error since we need to validate against a specific map
|
||||
{:error, "Authentication failed", :no_map_context}
|
||||
|
||||
identifier ->
|
||||
# Resolve the identifier (could be UUID or slug)
|
||||
case resolve_map_identifier(identifier) do
|
||||
{:ok, map} ->
|
||||
# Validate the token matches this specific map's API key
|
||||
if is_binary(map.public_api_key) &&
|
||||
Crypto.secure_compare(map.public_api_key, token) do
|
||||
# Get the map owner
|
||||
case User.by_id(map.owner_id, load: :characters) do
|
||||
{:ok, user} ->
|
||||
{:ok, user, map}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, "Authentication failed", :map_owner_not_found}
|
||||
end
|
||||
else
|
||||
{:error, "Authentication failed", :invalid_token_for_map}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
# Return generic error with specific reason for internal logging
|
||||
{:error, "Authentication failed", :map_owner_not_found}
|
||||
{:error, "Authentication failed", :map_not_found}
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Return generic error with specific reason for internal logging
|
||||
{:error, "Authentication failed", :invalid_api_key}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_map_by_api_key(api_key) do
|
||||
# Import necessary modules
|
||||
import Ash.Query
|
||||
# Helper to resolve map by ID or slug
|
||||
defp resolve_map_identifier(identifier) do
|
||||
alias WandererApp.Api.Map
|
||||
|
||||
# Query for map with matching public API key
|
||||
Map
|
||||
|> filter(public_api_key == ^api_key)
|
||||
|> Ash.read_one()
|
||||
# Try as UUID first
|
||||
case Map.by_id(identifier) do
|
||||
{:ok, map} ->
|
||||
{:ok, map}
|
||||
|
||||
_ ->
|
||||
# Try as slug
|
||||
Map.get_map_by_slug(identifier)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_user_role(user) do
|
||||
|
||||
@@ -38,8 +38,6 @@ defmodule WandererAppWeb.UserAuth do
|
||||
{:halt, redirect_require_login(socket)}
|
||||
|
||||
%User{characters: characters} ->
|
||||
:ok = track_characters(characters)
|
||||
|
||||
{:cont, new_socket}
|
||||
end
|
||||
|
||||
|
||||
@@ -15,30 +15,6 @@ defmodule WandererAppWeb.Endpoint do
|
||||
max_age: 24 * 60 * 60 * 180
|
||||
]
|
||||
|
||||
# @impl SiteEncrypt
|
||||
# def certification do
|
||||
# SiteEncrypt.configure(
|
||||
# client: :native,
|
||||
# mode: :auto,
|
||||
# days_to_renew: 30,
|
||||
# domains: ["dev.wanderer.deadly-w.space"],
|
||||
# emails: ["dmitriypopovsamara@gmail.com"],
|
||||
# db_folder: System.get_env("SITE_ENCRYPT_DB", Path.join("tmp", "site_encrypt_db")),
|
||||
# backup: Path.join(Path.join("tmp", "site_encrypt_db"), "site_encrypt_backup.tgz"),
|
||||
# directory_url:
|
||||
# case System.get_env("CERT_MODE", "local") do
|
||||
# "local" ->
|
||||
# {:internal, port: 4001}
|
||||
|
||||
# "staging" ->
|
||||
# "https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
|
||||
# "production" ->
|
||||
# "https://acme-v02.api.letsencrypt.org/directory"
|
||||
# end
|
||||
# )
|
||||
# end
|
||||
|
||||
socket "/live", Phoenix.LiveView.Socket,
|
||||
websocket: [compress: true, connect_info: [session: @session_options]]
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user