mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-03 14:32:36 +00:00
Compare commits
64 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 | ||
|
|
854524a03c |
135
CHANGELOG.md
135
CHANGELOG.md
@@ -2,6 +2,141 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.78.0](https://github.com/wanderer-industries/wanderer/compare/v1.77.19...v1.78.0) (2025-09-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Core: added support for jumpgates connection type
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Add support for Bridge. Made all tooltips left and right paddings.
|
||||
|
||||
## [v1.77.19](https://github.com/wanderer-industries/wanderer/compare/v1.77.18...v1.77.19) (2025-09-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fixed for all Large wormholes jump mass from 300 to 375. Fixed jump mass and total mass for N290, K329. Fixed static for J005663 was H296 now Y790. Added J492 wormhole. Change lifetime for E587 from 16 to 48
|
||||
|
||||
## [v1.77.18](https://github.com/wanderer-industries/wanderer/compare/v1.77.17...v1.77.18) (2025-09-13)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.77.17](https://github.com/wanderer-industries/wanderer/compare/v1.77.16...v1.77.17) (2025-09-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated ACL create/update APIs
|
||||
|
||||
## [v1.77.16](https://github.com/wanderer-industries/wanderer/compare/v1.77.15...v1.77.16) (2025-09-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed issue with ACL add members button for managers. Added WANDERER_RESTRICT_ACLS_CREATION env support.
|
||||
|
||||
## [v1.77.15](https://github.com/wanderer-industries/wanderer/compare/v1.77.14...v1.77.15) (2025-09-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Fix problem with unnecessary rerenders and loads routes if move/positioning widgets.
|
||||
|
||||
## [v1.77.14](https://github.com/wanderer-industries/wanderer/compare/v1.77.13...v1.77.14) (2025-09-08)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed issue with loading connection info
|
||||
|
||||
## [v1.77.13](https://github.com/wanderer-industries/wanderer/compare/v1.77.12...v1.77.13) (2025-09-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking, added an extra check for offline characters to reduce errors
|
||||
|
||||
## [v1.77.12](https://github.com/wanderer-industries/wanderer/compare/v1.77.11...v1.77.12) (2025-09-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Decreased character tracking grace period
|
||||
|
||||
## [v1.77.11](https://github.com/wanderer-industries/wanderer/compare/v1.77.10...v1.77.11) (2025-09-07)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Fixed CSP errors
|
||||
|
||||
## [v1.77.10](https://github.com/wanderer-industries/wanderer/compare/v1.77.9...v1.77.10) (2025-09-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Removed invalid invite options
|
||||
|
||||
## [v1.77.9](https://github.com/wanderer-industries/wanderer/compare/v1.77.8...v1.77.9) (2025-09-04)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Auto select following char system on start
|
||||
|
||||
## [v1.77.8](https://github.com/wanderer-industries/wanderer/compare/v1.77.7...v1.77.8) (2025-09-03)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking
|
||||
|
||||
## [v1.77.7](https://github.com/wanderer-industries/wanderer/compare/v1.77.6...v1.77.7) (2025-09-03)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking
|
||||
|
||||
## [v1.77.6](https://github.com/wanderer-industries/wanderer/compare/v1.77.5...v1.77.6) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Updated character tracking, added grace period to reduce false-positive cases
|
||||
|
||||
## [v1.77.5](https://github.com/wanderer-industries/wanderer/compare/v1.77.4...v1.77.5) (2025-09-02)
|
||||
|
||||
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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]}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
import { useReactFlow } from 'reactflow';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
import { CommandSelectSystems } from '@/hooks/Mapper/types';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useReactFlow } from 'reactflow';
|
||||
|
||||
export const useSelectSystems = (onSelectionChange: OnMapSelectionChange) => {
|
||||
const rf = useReactFlow();
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { ForwardedRef, useImperativeHandle, useRef } from 'react';
|
||||
import {
|
||||
CommandAddConnections,
|
||||
CommandAddSystems,
|
||||
@@ -19,8 +18,11 @@ import {
|
||||
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,
|
||||
@@ -28,10 +30,8 @@ import {
|
||||
useMapInit,
|
||||
useMapRemoveSystems,
|
||||
useMapUpdateSystems,
|
||||
useCenterSystem,
|
||||
useSelectSystems,
|
||||
} from './api';
|
||||
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
|
||||
|
||||
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
|
||||
const mapInit = useMapInit();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -118,6 +118,7 @@ $homeDark30: color.adjust($homeBase, $lightness: -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);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@@ -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[]>([]);
|
||||
|
||||
@@ -185,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={() => {
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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"),
|
||||
@@ -150,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: [
|
||||
%{
|
||||
|
||||
@@ -168,6 +168,7 @@ defmodule WandererApp.Api.MapConnection do
|
||||
|
||||
# where 0 - Wormhole
|
||||
# where 1 - Gate
|
||||
# where 2 - Bridge
|
||||
attribute :type, :integer do
|
||||
default(0)
|
||||
|
||||
|
||||
@@ -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
|
||||
]
|
||||
|
||||
@@ -37,13 +37,14 @@ defmodule WandererApp.Character.Tracker do
|
||||
}
|
||||
|
||||
@pause_tracking_timeout :timer.minutes(60 * 10)
|
||||
@offline_timeout :timer.minutes(10)
|
||||
@offline_timeout :timer.minutes(5)
|
||||
@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)
|
||||
@@ -71,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
|
||||
@@ -186,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) ->
|
||||
@@ -197,8 +201,6 @@ 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
|
||||
@@ -211,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 #{inspect(error)}",
|
||||
character_id: character_id,
|
||||
tracking_pool: tracking_pool,
|
||||
error_type: error,
|
||||
endpoint: "character_online"
|
||||
)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character_id}:online_forbidden",
|
||||
true,
|
||||
@@ -301,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,
|
||||
@@ -332,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,
|
||||
@@ -417,21 +386,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_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,
|
||||
@@ -443,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,
|
||||
@@ -479,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,
|
||||
@@ -540,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,
|
||||
@@ -573,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,
|
||||
@@ -610,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,
|
||||
@@ -641,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,
|
||||
@@ -711,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,
|
||||
@@ -740,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,
|
||||
@@ -777,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,
|
||||
@@ -804,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,
|
||||
@@ -873,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,
|
||||
@@ -899,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,
|
||||
@@ -936,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,
|
||||
@@ -960,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,
|
||||
@@ -1073,6 +829,7 @@ defmodule WandererApp.Character.Tracker do
|
||||
)
|
||||
when old_corporation_id != corporation_id do
|
||||
(WandererApp.Cache.has_key?("character:#{character_id}:online_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:corporation_info_forbidden") ||
|
||||
WandererApp.Cache.has_key?("character:#{character_id}:tracking_paused"))
|
||||
|> case do
|
||||
true ->
|
||||
@@ -1112,6 +869,17 @@ defmodule WandererApp.Character.Tracker do
|
||||
state
|
||||
|> 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(
|
||||
"Failed to get corporation info for character #{character_id}: #{inspect(error)}",
|
||||
|
||||
@@ -13,10 +13,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
}
|
||||
|
||||
@check_start_queue_interval :timer.seconds(1)
|
||||
@garbage_collection_interval :timer.minutes(15)
|
||||
@untrack_characters_interval :timer.minutes(1)
|
||||
@inactive_character_timeout :timer.minutes(10)
|
||||
@untrack_character_timeout :timer.minutes(10)
|
||||
@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)
|
||||
|
||||
@@ -54,6 +53,8 @@ defmodule WandererApp.Character.TrackerManager.Impl do
|
||||
true
|
||||
)
|
||||
|
||||
Logger.debug(fn -> "Add character to track_characters_queue: #{inspect(character_id)}" end)
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"track_characters_queue",
|
||||
[character_id],
|
||||
@@ -69,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
|
||||
|
||||
@@ -118,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",
|
||||
[],
|
||||
@@ -240,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,
|
||||
|
||||
@@ -18,7 +18,7 @@ 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)
|
||||
@@ -124,7 +124,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
Process.send_after(self(), :check_online_errors, :timer.seconds(60))
|
||||
Process.send_after(self(), :check_ship_errors, :timer.seconds(90))
|
||||
Process.send_after(self(), :check_location_errors, :timer.seconds(120))
|
||||
# Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
|
||||
Process.send_after(self(), :check_offline_characters, @check_offline_characters_interval)
|
||||
Process.send_after(self(), :update_location, 300)
|
||||
Process.send_after(self(), :update_ship, 500)
|
||||
Process.send_after(self(), :update_info, 1500)
|
||||
@@ -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(),
|
||||
|
||||
@@ -49,6 +49,12 @@ defmodule WandererApp.Env do
|
||||
)
|
||||
def restrict_maps_creation?(), do: get_key(:restrict_maps_creation, false)
|
||||
|
||||
@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)
|
||||
|
||||
@@ -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,16 +67,10 @@ 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
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
@@ -86,8 +80,6 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
end
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("maps:#{map_id}:tracked_characters", map_active_tracked_characters)
|
||||
|
||||
:ok
|
||||
end)
|
||||
end
|
||||
@@ -95,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
|
||||
@@ -219,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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -32,7 +32,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
@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,7 +96,13 @@ 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)
|
||||
@@ -581,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!(
|
||||
|
||||
@@ -94,7 +94,7 @@ defmodule WandererApp.Maps do
|
||||
end
|
||||
end
|
||||
|
||||
def load_characters(map, 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})
|
||||
|
||||
@@ -117,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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> --%>
|
||||
|
||||
@@ -38,8 +38,6 @@ defmodule WandererAppWeb.UserAuth do
|
||||
{:halt, redirect_require_login(socket)}
|
||||
|
||||
%User{characters: characters} ->
|
||||
:ok = track_characters(characters)
|
||||
|
||||
{:cont, new_socket}
|
||||
end
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> assign(
|
||||
selected_acl: nil,
|
||||
selected_acl_id: "",
|
||||
allow_acl_creation: not WandererApp.Env.restrict_acls_creation?(),
|
||||
user_id: user_id,
|
||||
access_lists: access_lists |> Enum.map(fn acl -> map_ui_acl(acl, nil) end),
|
||||
characters: characters,
|
||||
@@ -34,6 +35,7 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> assign(
|
||||
selected_acl: nil,
|
||||
selected_acl_id: "",
|
||||
allow_acl_creation: false,
|
||||
access_lists: [],
|
||||
characters: [],
|
||||
members: []
|
||||
@@ -188,7 +190,11 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
{:noreply, assign(socket, form: form)}
|
||||
end
|
||||
|
||||
def handle_event("create", %{"form" => form}, socket) do
|
||||
def handle_event(
|
||||
"create",
|
||||
%{"form" => form},
|
||||
%{assigns: %{allow_acl_creation: true}} = socket
|
||||
) do
|
||||
case WandererApp.Api.AccessList.new(form) do
|
||||
{:ok, _acl} ->
|
||||
{:ok, access_lists} = WandererApp.Acls.get_available_acls(socket.assigns.current_user)
|
||||
@@ -408,9 +414,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
current_user_has_role?(socket.assigns.current_user, access_list, :admin) ->
|
||||
true
|
||||
|
||||
not is_nil(eve_character_id) and
|
||||
(characters_has_role?([eve_character_id], access_list, :admin) or
|
||||
characters_has_role?([eve_character_id], access_list, :manager)) and
|
||||
not is_nil(eve_character_id) &&
|
||||
characters_has_roles?([eve_character_id], access_list, [:admin, :manager]) &&
|
||||
not current_user_has_role?(socket.assigns.current_user, access_list, :admin) ->
|
||||
false
|
||||
|
||||
@@ -470,12 +475,12 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
|> put_flash(:info, "Only Characters can have Admin or Manager roles")
|
||||
|> push_navigate(to: ~p"/access-lists/#{socket.assigns.selected_acl_id}")
|
||||
|
||||
defp characters_has_role?(character_eve_ids, access_list, role_atom) do
|
||||
access_list.members
|
||||
|> Enum.any?(fn member ->
|
||||
member.eve_character_id in character_eve_ids and member.role == role_atom
|
||||
end)
|
||||
end
|
||||
defp characters_has_roles?(character_eve_ids, %{members: members} = _access_list, role_atoms),
|
||||
do:
|
||||
members
|
||||
|> Enum.any?(fn %{eve_character_id: eve_character_id, role: role} = _member ->
|
||||
eve_character_id in character_eve_ids and role in role_atoms
|
||||
end)
|
||||
|
||||
defp current_user_is_owner?(current_user, access_list) do
|
||||
character_ids = current_user.characters |> Enum.map(& &1.id)
|
||||
@@ -486,18 +491,16 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
defp current_user_has_role?(current_user, access_list, role_atom) do
|
||||
character_eve_ids = current_user.characters |> Enum.map(& &1.eve_id)
|
||||
|
||||
characters_has_role?(character_eve_ids, access_list, role_atom)
|
||||
characters_has_roles?(character_eve_ids, access_list, [role_atom])
|
||||
end
|
||||
|
||||
defp can_add_members?(nil, _current_user), do: false
|
||||
|
||||
defp can_add_members?(access_list, current_user) do
|
||||
character_eve_ids = current_user.characters |> Enum.map(& &1.eve_id)
|
||||
user_character_eve_ids = current_user.characters |> Enum.map(& &1.eve_id)
|
||||
|
||||
member = access_list.members |> Enum.find(&(&1.eve_character_id in character_eve_ids))
|
||||
|
||||
current_user_is_owner?(current_user, access_list) or
|
||||
(not is_nil(member) and member.role in [:admin, :manager])
|
||||
current_user_is_owner?(current_user, access_list) ||
|
||||
characters_has_roles?(user_character_eve_ids, access_list, [:admin, :manager])
|
||||
end
|
||||
|
||||
defp can_delete_member?(
|
||||
@@ -512,9 +515,8 @@ defmodule WandererAppWeb.AccessListsLive do
|
||||
current_user_has_role?(current_user, access_list, :admin) ->
|
||||
true
|
||||
|
||||
not is_nil(eve_character_id) and
|
||||
(characters_has_role?([eve_character_id], access_list, :admin) or
|
||||
characters_has_role?([eve_character_id], access_list, :manager)) and
|
||||
not is_nil(eve_character_id) &&
|
||||
characters_has_roles?([eve_character_id], access_list, [:admin, :manager]) &&
|
||||
not current_user_has_role?(current_user, access_list, :admin) ->
|
||||
false
|
||||
|
||||
|
||||
@@ -34,7 +34,11 @@
|
||||
</button>
|
||||
</:action>
|
||||
</.table>
|
||||
<.link class="btn mt-2 w-full btn-neutral rounded-none" patch={~p"/access-lists/new"}>
|
||||
<.link
|
||||
:if={@allow_acl_creation}
|
||||
class="btn mt-2 w-full btn-neutral rounded-none"
|
||||
patch={~p"/access-lists/new"}
|
||||
>
|
||||
<.icon name="hero-plus-solid" class="w-6 h-6" />
|
||||
<h3 class="card-title text-center text-md">New Access List</h3>
|
||||
</.link>
|
||||
@@ -195,78 +199,13 @@
|
||||
</.modal>
|
||||
|
||||
<.modal
|
||||
:if={@live_action in [:add_members]}
|
||||
:if={@live_action in [:add_members] && can_add_members?(@access_list, @current_user)}
|
||||
title="Add Member"
|
||||
class="!w-[500px]"
|
||||
id="add_member"
|
||||
show
|
||||
on_cancel={JS.patch(~p"/access-lists/#{@selected_acl_id}")}
|
||||
>
|
||||
<%!-- <div class="mt-4 mb-2 p-tabmenu p-component " data-pc-section="tabmenu">
|
||||
<ul
|
||||
class="p-tabmenu-nav border-none h-[25px] w-full flex"
|
||||
role="menubar"
|
||||
data-pc-section="menu"
|
||||
>
|
||||
<li
|
||||
id="pr_id_17_0"
|
||||
class="p-tabmenuitem p-highlight"
|
||||
role="presentation"
|
||||
data-p-highlight="true"
|
||||
data-p-disabled="false"
|
||||
data-pc-section="menuitem"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
role="menuitem"
|
||||
aria-label="Router Link"
|
||||
tabindex="0"
|
||||
class="p-menuitem-link"
|
||||
data-pc-section="action"
|
||||
>
|
||||
<span class="p-menuitem-text" data-pc-section="label">Character</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
id="pr_id_17_1"
|
||||
class="p-tabmenuitem"
|
||||
role="presentation"
|
||||
data-p-highlight="false"
|
||||
data-p-disabled="false"
|
||||
data-pc-section="menuitem"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
role="menuitem"
|
||||
aria-label="Programmatic"
|
||||
tabindex="-1"
|
||||
class="p-menuitem-link"
|
||||
data-pc-section="action"
|
||||
>
|
||||
<span class="p-menuitem-text" data-pc-section="label">Corporation</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
id="pr_id_17_2"
|
||||
class="p-tabmenuitem"
|
||||
role="presentation"
|
||||
data-p-highlight="false"
|
||||
data-p-disabled="false"
|
||||
data-pc-section="menuitem"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
role="menuitem"
|
||||
aria-label="External"
|
||||
tabindex="-1"
|
||||
class="p-menuitem-link"
|
||||
data-pc-section="action"
|
||||
>
|
||||
<span class="p-menuitem-text" data-pc-section="label">Alliance</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div> --%>
|
||||
<.form :let={f} for={@member_form} phx-submit={@live_action}>
|
||||
<.live_select
|
||||
field={f[:member_id]}
|
||||
|
||||
@@ -318,11 +318,18 @@ defmodule WandererAppWeb.AdminLive do
|
||||
end
|
||||
|
||||
defp apply_action(socket, :add_invite_link, _params, uri) do
|
||||
invite_types =
|
||||
if socket.assigns.map_subscriptions_enabled? do
|
||||
[%{label: "User", id: :user}, %{label: "Admin", id: :admin}]
|
||||
else
|
||||
[%{label: "User", id: :user}]
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(:active_page, :admin)
|
||||
|> assign(:uri, URI.parse(uri))
|
||||
|> assign(:page_title, "Add Invite Link")
|
||||
|> assign(:invite_types, [%{label: "User", id: :user}, %{label: "Admin", id: :admin}])
|
||||
|> assign(:invite_types, invite_types)
|
||||
|> assign(:valid_types, [
|
||||
%{label: "1D", id: 1},
|
||||
%{label: "1W", id: 7},
|
||||
|
||||
@@ -703,6 +703,18 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
|
||||
Process.send_after(self(), %{event: :load_map_pings}, 200)
|
||||
|
||||
Process.send_after(
|
||||
self(),
|
||||
%{
|
||||
event: :maybe_select_system,
|
||||
payload: %{
|
||||
character_id: nil,
|
||||
solar_system_id: nil
|
||||
}
|
||||
},
|
||||
200
|
||||
)
|
||||
|
||||
if needs_tracking_setup do
|
||||
Process.send_after(self(), %{event: :show_tracking}, 10)
|
||||
|
||||
|
||||
@@ -44,16 +44,24 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
current_user: current_user,
|
||||
tracked_characters: tracked_characters,
|
||||
map_id: map_id,
|
||||
map_user_settings: map_user_settings
|
||||
map_user_settings: map_user_settings,
|
||||
main_character_eve_id: main_character_eve_id,
|
||||
following_character_eve_id: following_character_eve_id
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
character =
|
||||
tracked_characters
|
||||
|> Enum.find(fn tracked_character -> tracked_character.id == character_id end)
|
||||
if is_nil(character_id) do
|
||||
tracked_characters
|
||||
|> Enum.find(fn tracked_character ->
|
||||
tracked_character.eve_id == (following_character_eve_id || main_character_eve_id)
|
||||
end)
|
||||
else
|
||||
tracked_characters
|
||||
|> Enum.find(fn tracked_character -> tracked_character.id == character_id end)
|
||||
end
|
||||
|
||||
is_user_character =
|
||||
not is_nil(character)
|
||||
is_user_character = not is_nil(character)
|
||||
|
||||
is_select_on_spash =
|
||||
map_user_settings
|
||||
@@ -61,10 +69,9 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
|> WandererApp.MapUserSettingsRepo.get_boolean_setting("select_on_spash")
|
||||
|
||||
is_following =
|
||||
case WandererApp.MapUserSettingsRepo.get(map_id, current_user.id) do
|
||||
{:ok, %{following_character_eve_id: following_character_eve_id}}
|
||||
when not is_nil(following_character_eve_id) ->
|
||||
is_user_character && following_character_eve_id == character.eve_id
|
||||
case is_user_character && not is_nil(following_character_eve_id) do
|
||||
true ->
|
||||
following_character_eve_id == character.eve_id
|
||||
|
||||
_ ->
|
||||
false
|
||||
@@ -77,8 +84,17 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
else
|
||||
# Always select the system when auto-select is enabled (following or select_on_spash).
|
||||
# The frontend will handle deselecting other systems
|
||||
#
|
||||
select_solar_system_id =
|
||||
if not is_nil(solar_system_id) do
|
||||
"#{solar_system_id}"
|
||||
else
|
||||
{:ok, character} = WandererApp.Character.get_map_character(map_id, character.id)
|
||||
"#{character.solar_system_id}"
|
||||
end
|
||||
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("select_system", solar_system_id)
|
||||
|> MapEventHandler.push_map_event("select_system", select_solar_system_id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -24,24 +24,8 @@ defmodule WandererAppWeb.Presence do
|
||||
%{character_id: character_id, tracked: any_tracked, from: from}
|
||||
end)
|
||||
|
||||
presence_tracked_character_ids =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|
||||
|> Enum.map(fn %{character_id: character_id} ->
|
||||
character_id
|
||||
end)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_data",
|
||||
presence_data
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
# Delegate all cache operations to the PresenceGracePeriodManager
|
||||
WandererAppWeb.PresenceGracePeriodManager.process_presence_change(map_id, presence_data)
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
233
lib/wanderer_app_web/presence_grace_period_manager.ex
Normal file
233
lib/wanderer_app_web/presence_grace_period_manager.ex
Normal file
@@ -0,0 +1,233 @@
|
||||
defmodule WandererAppWeb.PresenceGracePeriodManager do
|
||||
@moduledoc """
|
||||
Manages grace period for character presence tracking.
|
||||
|
||||
This module prevents rapid start/stop cycles of character tracking
|
||||
by introducing a 5-minute grace period before stopping tracking
|
||||
for characters that leave presence.
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
# 30 minutes
|
||||
@grace_period_ms :timer.minutes(10)
|
||||
@check_remove_queue_interval :timer.seconds(30)
|
||||
|
||||
defstruct pending_removals: %{}, timers: %{}, to_remove: []
|
||||
|
||||
def start_link(opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Process presence changes with grace period logic.
|
||||
|
||||
Updates the cache with the final list of character IDs that should be tracked,
|
||||
accounting for the grace period.
|
||||
"""
|
||||
def process_presence_change(map_id, presence_data) do
|
||||
GenServer.cast(__MODULE__, {:process_presence_change, map_id, presence_data})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(_opts) do
|
||||
Logger.info("#{__MODULE__} started")
|
||||
Process.send_after(self(), :check_remove_queue, @check_remove_queue_interval)
|
||||
|
||||
{:ok, %__MODULE__{}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_cast({:process_presence_change, map_id, presence_data}, state) do
|
||||
# Extract currently tracked character IDs from presence data
|
||||
current_tracked_character_ids =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} -> tracked end)
|
||||
|> Enum.map(fn %{character_id: character_id} -> character_id end)
|
||||
|
||||
# Get previous tracked character IDs from cache
|
||||
previous_tracked_character_ids = get_previous_character_ids(map_id)
|
||||
|
||||
current_set = MapSet.new(current_tracked_character_ids)
|
||||
previous_set = MapSet.new(previous_tracked_character_ids)
|
||||
|
||||
# Characters that just joined (not in previous, but in current)
|
||||
newly_joined = MapSet.difference(current_set, previous_set)
|
||||
|
||||
# Characters that just left (in previous, but not in current)
|
||||
newly_left = MapSet.difference(previous_set, current_set)
|
||||
|
||||
# Process newly joined characters - cancel any pending removals
|
||||
state =
|
||||
state
|
||||
|> cancel_pending_removals(map_id, current_set)
|
||||
|> schedule_removals(map_id, newly_left)
|
||||
|
||||
# Process newly left characters - schedule them for removal after grace period
|
||||
# Calculate the final character IDs (current + still pending removal)
|
||||
pending_for_map = get_pending_removals_for_map(state, map_id)
|
||||
|
||||
final_character_ids = MapSet.union(current_set, pending_for_map) |> MapSet.to_list()
|
||||
|
||||
# Update cache with final character IDs (includes grace period logic)
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_character_ids", final_character_ids)
|
||||
|
||||
# Only update presence_data if the character IDs actually changed
|
||||
if final_character_ids != previous_tracked_character_ids do
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_data", presence_data)
|
||||
end
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({:grace_period_expired, map_id, character_id}, state) do
|
||||
Logger.debug(fn -> "Grace period expired for character #{character_id} on map #{map_id}" end)
|
||||
|
||||
# Remove from pending removals and timers
|
||||
state =
|
||||
state
|
||||
|> remove_pending_removal(map_id, character_id)
|
||||
|> remove_after_grace_period(map_id, character_id)
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:check_remove_queue, state) do
|
||||
Process.send_after(self(), :check_remove_queue, @check_remove_queue_interval)
|
||||
|
||||
remove_from_cache_after_grace_period(state)
|
||||
{:noreply, %{state | to_remove: []}}
|
||||
end
|
||||
|
||||
defp cancel_pending_removals(state, map_id, character_ids) do
|
||||
Enum.reduce(character_ids, state, fn character_id, acc_state ->
|
||||
case get_timer_ref(acc_state, map_id, character_id) do
|
||||
nil ->
|
||||
acc_state
|
||||
|
||||
timer_ref ->
|
||||
Logger.debug(fn ->
|
||||
"Cancelling grace period for character #{character_id} on map #{map_id} (rejoined)"
|
||||
end)
|
||||
|
||||
Process.cancel_timer(timer_ref)
|
||||
remove_pending_removal(acc_state, map_id, character_id)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp schedule_removals(state, map_id, character_ids) do
|
||||
Enum.reduce(character_ids, state, fn character_id, acc_state ->
|
||||
# Only schedule if not already pending
|
||||
case get_timer_ref(acc_state, map_id, character_id) do
|
||||
nil ->
|
||||
Logger.debug(fn ->
|
||||
"Scheduling grace period for character #{character_id} on map #{map_id}"
|
||||
end)
|
||||
|
||||
timer_ref =
|
||||
Process.send_after(
|
||||
self(),
|
||||
{:grace_period_expired, map_id, character_id},
|
||||
@grace_period_ms
|
||||
)
|
||||
|
||||
add_pending_removal(acc_state, map_id, character_id, timer_ref)
|
||||
|
||||
_ ->
|
||||
acc_state
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp add_pending_removal(state, map_id, character_id, timer_ref) do
|
||||
pending_key = {map_id, character_id}
|
||||
|
||||
%{
|
||||
state
|
||||
| pending_removals: Map.put(state.pending_removals, pending_key, true),
|
||||
timers: Map.put(state.timers, pending_key, timer_ref)
|
||||
}
|
||||
end
|
||||
|
||||
defp remove_pending_removal(state, map_id, character_id) do
|
||||
pending_key = {map_id, character_id}
|
||||
|
||||
%{
|
||||
state
|
||||
| pending_removals: Map.delete(state.pending_removals, pending_key),
|
||||
timers: Map.delete(state.timers, pending_key)
|
||||
}
|
||||
end
|
||||
|
||||
defp get_timer_ref(state, map_id, character_id) do
|
||||
Map.get(state.timers, {map_id, character_id})
|
||||
end
|
||||
|
||||
defp get_previous_character_ids(map_id) do
|
||||
case WandererApp.Cache.get("map_#{map_id}:presence_character_ids") do
|
||||
nil -> []
|
||||
character_ids -> character_ids
|
||||
end
|
||||
end
|
||||
|
||||
defp get_pending_removals_for_map(state, map_id) do
|
||||
state.pending_removals
|
||||
|> Enum.filter(fn {{pending_map_id, _character_id}, _} -> pending_map_id == map_id end)
|
||||
|> Enum.map(fn {{_map_id, character_id}, _} -> character_id end)
|
||||
|> MapSet.new()
|
||||
end
|
||||
|
||||
defp remove_after_grace_period(%{to_remove: to_remove} = state, map_id, character_id_to_remove) do
|
||||
%{
|
||||
state
|
||||
| to_remove:
|
||||
(to_remove ++ [{map_id, character_id_to_remove}])
|
||||
|> Enum.uniq_by(fn {map_id, character_id} -> map_id <> character_id end)
|
||||
}
|
||||
end
|
||||
|
||||
defp remove_from_cache_after_grace_period(%{to_remove: to_remove} = state) do
|
||||
# Get current presence data to recalculate without the expired character
|
||||
to_remove
|
||||
|> Enum.each(fn {map_id, character_id_to_remove} ->
|
||||
case WandererApp.Cache.get("map_#{map_id}:presence_data") do
|
||||
nil ->
|
||||
:ok
|
||||
|
||||
presence_data ->
|
||||
# Recalculate tracked character IDs from current presence data
|
||||
updated_presence_data =
|
||||
presence_data
|
||||
|> Enum.filter(fn %{character_id: character_id} ->
|
||||
character_id != character_id_to_remove
|
||||
end)
|
||||
|
||||
presence_tracked_character_ids =
|
||||
updated_presence_data
|
||||
|> Enum.filter(fn %{tracked: tracked} ->
|
||||
tracked
|
||||
end)
|
||||
|> Enum.map(fn %{character_id: character_id} -> character_id end)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_data", updated_presence_data)
|
||||
# Update both caches
|
||||
WandererApp.Cache.insert(
|
||||
"map_#{map_id}:presence_character_ids",
|
||||
presence_tracked_character_ids
|
||||
)
|
||||
|
||||
WandererApp.Cache.insert("map_#{map_id}:presence_updated", true)
|
||||
|
||||
Logger.debug(fn ->
|
||||
"Updated cache after grace period for map #{map_id}, tracked characters: #{inspect(presence_tracked_character_ids)}"
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -70,7 +70,8 @@ defmodule WandererAppWeb.Router do
|
||||
"'self'",
|
||||
"https://api.appzi.io",
|
||||
"https://www.googletagmanager.com",
|
||||
"https://www.google-analytics.com"
|
||||
"https://www.google-analytics.com",
|
||||
"https://*.google-analytics.com"
|
||||
]
|
||||
|
||||
# Define sandbox values individually to ensure proper spacing
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.77.5"
|
||||
@version "1.78.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
%{
|
||||
title: "Guide: Systems and Connections API",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/05-07-systems/api-endpoints.png",
|
||||
cover_image_uri: "/images/news/03-05-api/swagger-ui.png",
|
||||
tags: ~w(api map systems connections documentation),
|
||||
description: "Detailed guide for Wanderer's systems and connections API endpoints, including batch operations, updates, and deletions."
|
||||
}
|
||||
@@ -912,4 +912,4 @@ If you have questions about these endpoints or need assistance, please reach out
|
||||
Fly safe,
|
||||
**The Wanderer Team**
|
||||
|
||||
----
|
||||
----
|
||||
|
||||
@@ -23286,7 +23286,7 @@
|
||||
"solarSystemID": 31002568,
|
||||
"statics": [
|
||||
"U210",
|
||||
"H296"
|
||||
"Y790"
|
||||
],
|
||||
"systemName": "J005663",
|
||||
"effectName": null
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"dest": "ls",
|
||||
"src": ["c2"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 2000000000,
|
||||
"name": "A239",
|
||||
@@ -37,7 +37,7 @@
|
||||
"dest": "c6",
|
||||
"src": ["c3", "thera"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 3000000000,
|
||||
"name": "A982",
|
||||
@@ -48,7 +48,7 @@
|
||||
"dest": "c6",
|
||||
"src": ["hs"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "48",
|
||||
"total_mass": 3000000000,
|
||||
"name": "B041",
|
||||
@@ -59,7 +59,7 @@
|
||||
"dest": "hs",
|
||||
"src": ["c2"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 2000000000,
|
||||
"name": "B274",
|
||||
@@ -81,7 +81,7 @@
|
||||
"dest": "hs",
|
||||
"src": ["c6"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "48",
|
||||
"total_mass": 3000000000,
|
||||
"name": "B520",
|
||||
@@ -92,7 +92,7 @@
|
||||
"dest": "barbican",
|
||||
"src": ["hs", "ls", "ns", "jove"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 750000000,
|
||||
"name": "B735",
|
||||
@@ -136,7 +136,7 @@
|
||||
"dest": "c3",
|
||||
"src": ["c4"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "C247",
|
||||
@@ -169,7 +169,7 @@
|
||||
"dest": "conflux",
|
||||
"src": ["hs", "ls", "ns", "jove"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 750000000,
|
||||
"name": "C414",
|
||||
@@ -191,7 +191,7 @@
|
||||
"dest": "c2",
|
||||
"src": ["c5"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 1000000000,
|
||||
"name": "D364",
|
||||
@@ -202,7 +202,7 @@
|
||||
"dest": "c2",
|
||||
"src": ["c2", "drifter"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "D382",
|
||||
@@ -224,7 +224,7 @@
|
||||
"dest": "hs",
|
||||
"src": ["c3", "c4-shattered"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 5000000000,
|
||||
"name": "D845",
|
||||
@@ -246,7 +246,7 @@
|
||||
"dest": "c4",
|
||||
"src": ["c5"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "E175",
|
||||
@@ -257,7 +257,7 @@
|
||||
"dest": "ns",
|
||||
"src": ["c2"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "E545",
|
||||
@@ -269,7 +269,7 @@
|
||||
"src": ["thera"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 1000000000,
|
||||
"lifetime": "16",
|
||||
"lifetime": "48",
|
||||
"total_mass": 3000000000,
|
||||
"name": "E587",
|
||||
"respawn": ["static"]
|
||||
@@ -279,7 +279,7 @@
|
||||
"dest": "thera",
|
||||
"src": ["c2", "c3", "c4", "c5", "c6"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 750000000,
|
||||
"name": "F135",
|
||||
@@ -323,7 +323,7 @@
|
||||
"dest": "c2",
|
||||
"src": ["c6"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "G024",
|
||||
@@ -356,7 +356,7 @@
|
||||
"dest": "c5",
|
||||
"src": ["c4"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 3000000000,
|
||||
"name": "H900",
|
||||
@@ -367,7 +367,7 @@
|
||||
"dest": "c2",
|
||||
"src": ["c3", "thera"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "I182",
|
||||
@@ -396,13 +396,13 @@
|
||||
"respawn": ["wandering", "reverse"]
|
||||
},
|
||||
{
|
||||
"mass_regen": 500000000,
|
||||
"mass_regen": 0,
|
||||
"dest": "ns",
|
||||
"src": ["c4"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 1800000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 5000000000,
|
||||
"total_mass": 3000000000,
|
||||
"name": "K329",
|
||||
"respawn": ["wandering"]
|
||||
},
|
||||
@@ -411,7 +411,7 @@
|
||||
"dest": "ns",
|
||||
"src": ["c3", "c4-shattered", "c5-shattered", "c6-shattered"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 3000000000,
|
||||
"name": "K346",
|
||||
@@ -444,7 +444,7 @@
|
||||
"dest": "c3",
|
||||
"src": ["c6"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "L477",
|
||||
@@ -477,7 +477,7 @@
|
||||
"dest": "thera",
|
||||
"src": ["ls"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "M164",
|
||||
@@ -488,7 +488,7 @@
|
||||
"dest": "c3",
|
||||
"src": ["c5"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 1000000000,
|
||||
"name": "M267",
|
||||
@@ -539,13 +539,13 @@
|
||||
"respawn": ["static"]
|
||||
},
|
||||
{
|
||||
"mass_regen": 500000000,
|
||||
"mass_regen": 0,
|
||||
"dest": "ls",
|
||||
"src": ["c4"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 2000000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 3300000000,
|
||||
"total_mass": 3000000000,
|
||||
"name": "N290",
|
||||
"respawn": ["wandering"]
|
||||
},
|
||||
@@ -565,7 +565,7 @@
|
||||
"dest": "c2",
|
||||
"src": ["c4"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "N766",
|
||||
@@ -576,7 +576,7 @@
|
||||
"dest": "c5",
|
||||
"src": ["c3", "thera"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 3000000000,
|
||||
"name": "N770",
|
||||
@@ -598,7 +598,7 @@
|
||||
"dest": "c3",
|
||||
"src": ["c3", "thera"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "N968",
|
||||
@@ -609,7 +609,7 @@
|
||||
"dest": "c4",
|
||||
"src": ["hs", "ls", "ns"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 1000000000,
|
||||
"name": "O128",
|
||||
@@ -620,7 +620,7 @@
|
||||
"dest": "c3",
|
||||
"src": ["c2", "drifter"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "O477",
|
||||
@@ -708,7 +708,7 @@
|
||||
"dest": "redoubt",
|
||||
"src": ["hs", "ls", "ns", "jove"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 750000000,
|
||||
"name": "R259",
|
||||
@@ -719,7 +719,7 @@
|
||||
"dest": "c6",
|
||||
"src": ["c2", "drifter"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 3000000000,
|
||||
"name": "R474",
|
||||
@@ -730,7 +730,7 @@
|
||||
"dest": "c2",
|
||||
"src": ["hs", "ls", "ns"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 750000000,
|
||||
"name": "R943",
|
||||
@@ -741,7 +741,7 @@
|
||||
"dest": "hs",
|
||||
"src": ["c4"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 3000000000,
|
||||
"name": "S047",
|
||||
@@ -774,7 +774,7 @@
|
||||
"dest": "sentinel",
|
||||
"src": ["hs", "ls", "ns", "jove"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 750000000,
|
||||
"name": "S877",
|
||||
@@ -785,7 +785,7 @@
|
||||
"dest": "c4",
|
||||
"src": ["c3", "thera"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "T405",
|
||||
@@ -807,7 +807,7 @@
|
||||
"dest": "ls",
|
||||
"src": ["c3", "c4-shattered", "c5-shattered"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 3000000000,
|
||||
"name": "U210",
|
||||
@@ -840,7 +840,7 @@
|
||||
"dest": "c6",
|
||||
"src": ["c4"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 3000000000,
|
||||
"name": "U574",
|
||||
@@ -884,7 +884,7 @@
|
||||
"dest": "ls",
|
||||
"src": ["thera"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "V898",
|
||||
@@ -906,7 +906,7 @@
|
||||
"dest": "vidette",
|
||||
"src": ["hs", "ls", "ns", "jove"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 750000000,
|
||||
"name": "V928",
|
||||
@@ -939,7 +939,7 @@
|
||||
"dest": "c3",
|
||||
"src": ["hs", "ls", "ns"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 1000000000,
|
||||
"name": "X702",
|
||||
@@ -950,7 +950,7 @@
|
||||
"dest": "c4",
|
||||
"src": ["c4"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "X877",
|
||||
@@ -961,7 +961,7 @@
|
||||
"dest": "c4",
|
||||
"src": ["c2", "drifter"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "Y683",
|
||||
@@ -1016,7 +1016,7 @@
|
||||
"dest": "c4",
|
||||
"src": ["c6"],
|
||||
"static": true,
|
||||
"max_mass_per_jump": 300000000,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"total_mass": 2000000000,
|
||||
"name": "Z457",
|
||||
@@ -1044,6 +1044,17 @@
|
||||
"name": "Z971",
|
||||
"respawn": ["wandering"]
|
||||
},
|
||||
{
|
||||
"mass_regen": 0,
|
||||
"dest": "ls",
|
||||
"src": ["c1", "c2", "c3", "c4", "c5", "c6"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 62000000,
|
||||
"lifetime": "24",
|
||||
"total_mass": 100000000,
|
||||
"name": "J492",
|
||||
"respawn": ["wandering", "reverse"]
|
||||
},
|
||||
{
|
||||
"mass_regen": null,
|
||||
"dest": null,
|
||||
|
||||
Reference in New Issue
Block a user