Compare commits

...

49 Commits

Author SHA1 Message Date
CI
cf20be8a77 chore: release version v1.78.1 2025-09-24 16:38:54 +00:00
Dmitry Popov
450bcb649c Merge pull request #521 from wanderer-industries/kills-fix
Kills fix
2025-09-24 20:38:29 +04:00
Dmitry Popov
a00395351e Merge pull request #513 from guarzo/guarzo/killfilter
fix: removed wormhole only logic error
2025-09-24 20:33:00 +04:00
CI
602b1028c3 chore: [skip ci] 2025-09-23 16:27:20 +00:00
CI
4f98e979a2 chore: release version v1.78.0 2025-09-23 16:27:20 +00:00
Dmitry Popov
e0f46c4af7 Merge pull request #519 from wanderer-industries/jumpgates
Jumpgates
2025-09-23 20:23:25 +04:00
Dmitry Popov
bc8a9a2b85 Merge branch 'main' into jumpgates 2025-09-23 18:23:11 +02:00
Dmitry Popov
c2b03f925d Merge pull request #518 from leesolway/drag-dialog
Drag signature dialog
2025-09-23 20:22:47 +04:00
Dmitry Popov
efa2e52054 Merge branch 'main' into jumpgates 2025-09-23 18:18:32 +02:00
Dmitry Popov
cedf5761f8 Merge branch 'main' into jumpgates 2025-09-23 17:51:45 +02:00
Dmitry Popov
f601bb8751 Merge branch 'main' into jumpgates 2025-09-23 17:48:59 +02:00
Dmitry Popov
c39d2a56d2 Merge branch 'main' into jumpgates 2025-09-23 17:19:05 +02:00
Lee Solway
805722bbe8 Merge branch 'wanderer-industries:main' into drag-dialog 2025-09-20 16:05:34 +01:00
Lee Solway
fe3e38343b SystemSettingsDialog & SignatureSettings draggable 2025-09-19 17:35:28 +01:00
DanSylvest
616e82c497 fix(Map): Add support for Bridge. Made all tooltips left and right paddings. 2025-09-18 11:52:16 +03:00
guarzo
ab7e47b91f pr feedback 2025-09-18 06:18:23 +00:00
guarzo
cf1c103a46 fix: pr feedback 2025-09-17 15:50:26 +00:00
CI
71202a4a29 chore: [skip ci] 2025-09-14 15:00:45 +00:00
CI
a7e0ceac4c chore: release version v1.77.19 2025-09-14 15:00:45 +00:00
Aleksei Chichenkov
6bce701aab Merge pull request #515 from wanderer-industries/wh-db-fixed
fix(Map): Fixed for all Large wormholes jump mass from 300 to 375. Fi…
2025-09-14 18:00:20 +03:00
CI
f8b9e206a5 chore: [skip ci] 2025-09-13 21:53:27 +00:00
CI
4c1ec2004b chore: release version v1.77.18 2025-09-13 21:53:27 +00:00
Dmitry Popov
ebed74d239 Revert "fix: Updated ACL create/update APIs"
This reverts commit b6c680e802.
2025-09-13 23:52:02 +02:00
Dmitry Popov
24c32511d8 Merge branch 'main' into jumpgates 2025-09-13 11:30:40 +02:00
DanSylvest
06e7b6e3eb fix(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 2025-09-12 19:37:59 +03:00
CI
dec82e89c2 chore: [skip ci] 2025-09-11 19:14:13 +00:00
CI
f5ac5bc4ec chore: release version v1.77.17 2025-09-11 19:14:13 +00:00
Dmitry Popov
b6c680e802 fix: Updated ACL create/update APIs 2025-09-11 21:13:41 +02:00
CI
5fa57c13b4 chore: [skip ci] 2025-09-11 17:56:11 +00:00
CI
acc81fda44 chore: release version v1.77.16 2025-09-11 17:56:11 +00:00
Dmitry Popov
7ab5acf45f Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-11 19:55:37 +02:00
Dmitry Popov
0d4ffbcc22 fix: Fixed issue with ACL add members button for managers. Added WANDERER_RESTRICT_ACLS_CREATION env support. 2025-09-11 19:55:32 +02:00
CI
a9253ac2df chore: [skip ci] 2025-09-10 07:29:33 +00:00
CI
d00b4843a7 chore: release version v1.77.15 2025-09-10 07:29:33 +00:00
Aleksei Chichenkov
6068de2c71 Merge pull request #514 from wanderer-industries/unnecessary-rerenders
fix(Map): Fix problem with unnecessary rerenders and loads routes if …
2025-09-10 10:29:03 +03:00
DanSylvest
73da427c6b fix(Map): Fix problem with unnecessary rerenders and loads routes if move/positioning widgets. 2025-09-10 10:10:17 +03:00
CI
9b7ec0ddfe chore: [skip ci] 2025-09-08 22:07:20 +00:00
CI
c2f5f14c44 chore: release version v1.77.14 2025-09-08 22:07:20 +00:00
Dmitry Popov
0b7c3588d5 fix: Fixed issue with loading connection info 2025-09-09 00:06:49 +02:00
CI
a51fac5736 chore: [skip ci] 2025-09-07 22:27:12 +00:00
CI
726c3d0704 chore: release version v1.77.13 2025-09-07 22:27:12 +00:00
Dmitry Popov
8dd564dbd0 fix: Updated character tracking, added an extra check for offline characters to reduce errors 2025-09-08 00:26:40 +02:00
CI
e33c65cddc chore: [skip ci] 2025-09-07 19:28:25 +00:00
CI
f2fbd2ead0 chore: release version v1.77.12 2025-09-07 19:28:25 +00:00
Dmitry Popov
123a2e45eb Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-09-07 21:27:56 +02:00
Dmitry Popov
f8d2d9c680 fix: Decreased character tracking grace period 2025-09-07 21:27:53 +02:00
CI
9dcbef9a79 chore: [skip ci] 2025-09-07 19:16:08 +00:00
guarzo
873946a1a6 fix: removed wormhole only logic error 2025-09-05 01:43:47 +00:00
Dmitry Popov
854524a03c feat(Core): added support for jumpgates connection type 2025-03-23 21:51:26 +01:00
39 changed files with 943 additions and 813 deletions

View File

@@ -2,6 +2,98 @@
<!-- changelog -->
## [v1.78.1](https://github.com/wanderer-industries/wanderer/compare/v1.78.0...v1.78.1) (2025-09-24)
### Bug Fixes:
* pr feedback
* removed wormhole only logic error
## [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)

View File

@@ -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));
}

View File

@@ -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 (
<>

View File

@@ -56,7 +56,8 @@ export const KillsCounter = ({
className={className}
tooltipClassName="!px-0"
size={size}
interactive={true}
interactive
smallPaddings
>
{children}
</WdTooltipWrapper>

View File

@@ -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, {

View File

@@ -29,6 +29,13 @@
&.Gate {
stroke: #9aff40;
}
&.Bridge {
stroke: #9aff40;
stroke-dasharray: 10 5;
stroke-linecap: round;
}
}
.EdgePathFront {

View File

@@ -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]}

View File

@@ -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">

View File

@@ -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];

View File

@@ -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;
};

View File

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

View File

@@ -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);
}

View File

@@ -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={() => {

View File

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

View File

@@ -16,8 +16,21 @@ const SystemKillsContent = () => {
} = useMapRootState();
const [systemId] = selectedSystems || [];
const whCacheRef = useMemo(() => new Map<number, boolean>(), []);
const systemStaticInfo = getSystemStaticInfo(systemId)!;
const isWormholeSystem = useCallback(
(systemId: number): boolean => {
const cached = whCacheRef.get(systemId);
if (cached !== undefined) return cached;
const info = getSystemStaticInfo(systemId);
const isWH = info?.system_class != null ? isWormholeSpace(Number(info.system_class)) : false;
whCacheRef.set(systemId, isWH);
return isWH;
},
[whCacheRef],
);
const { kills, isLoading, error } = useSystemKills({
systemId,
@@ -30,15 +43,9 @@ const SystemKillsContent = () => {
const showLoading = isLoading && kills.length === 0;
const filteredKills = useMemo(() => {
if (!settingsKills.whOnly || !settingsKills.showAll) return kills;
return kills.filter(kill => {
if (!systemStaticInfo) {
console.warn(`System with id ${kill.solar_system_id} not found.`);
return false;
}
return isWormholeSpace(systemStaticInfo.system_class);
});
}, [kills, settingsKills.whOnly, systemStaticInfo, settingsKills.showAll]);
if (!settingsKills.whOnly) return kills;
return kills.filter(kill => isWormholeSystem(Number(kill.solar_system_id)));
}, [kills, settingsKills.whOnly, isWormholeSystem]);
if (!isSubscriptionActive) {
return (

View File

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

View File

@@ -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={() => {

View File

@@ -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={{

View File

@@ -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)}
/>

View File

@@ -1,6 +1,7 @@
export enum ConnectionType {
wormhole,
gate,
bridge,
}
export enum MassState {

View 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);
};

View File

@@ -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: [
%{

View File

@@ -168,6 +168,7 @@ defmodule WandererApp.Api.MapConnection do
# where 0 - Wormhole
# where 1 - Gate
# where 2 - Bridge
attribute :type, :integer do
default(0)

View File

@@ -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)}",

View File

@@ -13,10 +13,9 @@ defmodule WandererApp.Character.TrackerManager.Impl do
}
@check_start_queue_interval :timer.seconds(1)
@garbage_collection_interval :timer.minutes(15)
@garbage_collection_interval :timer.minutes(5)
@untrack_characters_interval :timer.minutes(5)
@inactive_character_timeout :timer.minutes(10)
@untrack_character_timeout :timer.minutes(10)
@inactive_character_timeout :timer.minutes(5)
@logger Application.compile_env(:wanderer_app, :logger)
@@ -116,13 +115,6 @@ 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}],
@@ -134,8 +126,6 @@ defmodule WandererApp.Character.TrackerManager.Impl do
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",
[],
@@ -239,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,

View File

@@ -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(),

View File

@@ -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)

View File

@@ -213,85 +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", [])
presence_character_ids
|> 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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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> --%>

View File

@@ -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

View File

@@ -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]}

View File

@@ -11,7 +11,7 @@ defmodule WandererAppWeb.PresenceGracePeriodManager do
require Logger
# 30 minutes
@grace_period_ms :timer.minutes(30)
@grace_period_ms :timer.minutes(10)
@check_remove_queue_interval :timer.seconds(30)
defstruct pending_removals: %{}, timers: %{}, to_remove: []

View File

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

View File

@@ -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**
----
----

View File

@@ -23286,7 +23286,7 @@
"solarSystemID": 31002568,
"statics": [
"U210",
"H296"
"Y790"
],
"systemName": "J005663",
"effectName": null

View File

@@ -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,