mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-04-08 03:37:57 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a549e70e1a | ||
|
|
74e8f45265 | ||
|
|
c61b8a9942 | ||
|
|
0891706489 | ||
|
|
7d720dcfb5 | ||
|
|
63b40b9c75 | ||
|
|
fc167fafaf | ||
|
|
b9197880f0 | ||
|
|
88f027facd | ||
|
|
d62ad709ab | ||
|
|
15aeb8eb85 | ||
|
|
6970db438d | ||
|
|
9ab7fcc46e | ||
|
|
931a8e629d | ||
|
|
a06df968a2 | ||
|
|
965df31da0 | ||
|
|
171591f07d | ||
|
|
9a63700dfb | ||
|
|
092fdb01e5 | ||
|
|
0ae9b54e3f | ||
|
|
d4928c0195 | ||
|
|
f00deb1556 | ||
|
|
11b0ba4018 | ||
|
|
9bf6893524 | ||
|
|
e45e6a39eb | ||
|
|
7f1691b2db | ||
|
|
d1006b329a | ||
|
|
e49471fb94 | ||
|
|
ad35d9e172 | ||
|
|
18d50329bc | ||
|
|
d8fb980a3b | ||
|
|
b8b3bc60ad | ||
|
|
80d5dd1eb1 | ||
|
|
1ab0e96cbb | ||
|
|
e3a13b9554 | ||
|
|
1f3387e4ff | ||
|
|
95d2fa232a | ||
|
|
eed1d8bc27 | ||
|
|
c451735559 | ||
|
|
aa586b7994 | ||
|
|
39317831f9 | ||
|
|
b71bc94d4f | ||
|
|
0e920a58e6 | ||
|
|
9385751332 | ||
|
|
ffaa48ff9e | ||
|
|
94665f4e68 | ||
|
|
e9fd0665c8 | ||
|
|
9a0271f711 | ||
|
|
0c68535656 | ||
|
|
9ed350befa | ||
|
|
c410f5f37d | ||
|
|
8559be00f0 | ||
|
|
1a24ee4c74 | ||
|
|
de86703737 | ||
|
|
c5af43dca1 | ||
|
|
549fa1d2cf |
173
CHANGELOG.md
173
CHANGELOG.md
@@ -2,6 +2,179 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.98.0](https://github.com/wanderer-industries/wanderer/compare/v1.97.5...v1.98.0) (2026-04-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* core: added character profile pages support
|
||||
|
||||
## [v1.97.5](https://github.com/wanderer-industries/wanderer/compare/v1.97.4...v1.97.5) (2026-03-26)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed character re-auth issues
|
||||
|
||||
## [v1.97.4](https://github.com/wanderer-industries/wanderer/compare/v1.97.3...v1.97.4) (2026-03-26)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed character re-auth issues
|
||||
|
||||
## [v1.97.3](https://github.com/wanderer-industries/wanderer/compare/v1.97.2...v1.97.3) (2026-03-25)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed character re-auth issues
|
||||
|
||||
## [v1.97.2](https://github.com/wanderer-industries/wanderer/compare/v1.97.1...v1.97.2) (2026-03-23)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed tracking issues & adding systems to map from routes
|
||||
|
||||
## [v1.97.1](https://github.com/wanderer-industries/wanderer/compare/v1.97.0...v1.97.1) (2026-03-21)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Add new Pochven medium wormholes (I078, L687, O546). Change lifetime to 12H from 16H for X450, U372, R081, F216.
|
||||
|
||||
## [v1.97.0](https://github.com/wanderer-industries/wanderer/compare/v1.96.6...v1.97.0) (2026-03-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* signatures: Sync mass status with connection, show it on unsplashed sigs
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Connection context menu and wormhole. Change UI for select wormhole mass state. Change UI for select ship-size for wormhole. Add ability to set mass for signatures
|
||||
|
||||
## [v1.96.6](https://github.com/wanderer-industries/wanderer/compare/v1.96.5...v1.96.6) (2026-03-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed tracking issues
|
||||
|
||||
## [v1.96.5](https://github.com/wanderer-industries/wanderer/compare/v1.96.4...v1.96.5) (2026-02-27)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed access token refresh issues
|
||||
|
||||
## [v1.96.4](https://github.com/wanderer-industries/wanderer/compare/v1.96.3...v1.96.4) (2026-02-17)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Change character token validity status. Now we will see red frame and icon for tracked characters which token was expired.
|
||||
|
||||
## [v1.96.3](https://github.com/wanderer-industries/wanderer/compare/v1.96.2...v1.96.3) (2026-02-15)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* tracking: Fixed character tracking issues
|
||||
|
||||
## [v1.96.2](https://github.com/wanderer-industries/wanderer/compare/v1.96.1...v1.96.2) (2026-02-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Added icons for RoutesBy
|
||||
|
||||
## [v1.96.1](https://github.com/wanderer-industries/wanderer/compare/v1.96.0...v1.96.1) (2026-02-12)
|
||||
|
||||
|
||||
|
||||
|
||||
## [v1.96.0](https://github.com/wanderer-industries/wanderer/compare/v1.95.0...v1.96.0) (2026-02-12)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* signatures: Fixed creator visibility issues. Added 4.5 hour color for unsplashed
|
||||
|
||||
## [v1.95.0](https://github.com/wanderer-industries/wanderer/compare/v1.94.0...v1.95.0) (2026-02-11)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* subscriptions: Added top map donators support
|
||||
|
||||
* Added lost files
|
||||
|
||||
* Added paywall for RoutesBy widget
|
||||
|
||||
* removed unnecessary env variable for routes
|
||||
|
||||
* Add systems with Security Status cleaning. Add trade hubs. Add ability to store data for this widget
|
||||
|
||||
* Add Routes By widget. Allow to find nearest blue loot and red loot stations. Added ability to set waypoint to station.
|
||||
|
||||
* auto add system on sig addition
|
||||
|
||||
* map: Reviewed changes
|
||||
|
||||
* map: Logic for multiple owner updates
|
||||
|
||||
* map: wip New Dialog for Structure Owners
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* signatures: Fixed back linked sigs data sync and leading to system override issues
|
||||
|
||||
* signatures: Moved C1/C2/C3 and C4/C5 to the bottom of the available list
|
||||
|
||||
* use cache for sse
|
||||
|
||||
* adding system when linked signature is provided
|
||||
|
||||
* saving updates to unknown sigs
|
||||
|
||||
* wh position and sig type change
|
||||
|
||||
* api updates and linked sig addition
|
||||
|
||||
* api fixes and format
|
||||
|
||||
* Wrong file added to commits
|
||||
|
||||
## [v1.94.0](https://github.com/wanderer-industries/wanderer/compare/v1.93.0...v1.94.0) (2026-02-08)
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@layer tailwind-base, primereact, tailwind-utilities;
|
||||
|
||||
@import 'quill/dist/quill.snow.css';
|
||||
@import 'primereact/resources/themes/arya-blue/theme.css' layer(primereact);
|
||||
/*@import 'primereact/resources/themes/bootstrap4-dark-blue/theme.css' layer(primereact);*/
|
||||
|
||||
@@ -1025,3 +1026,77 @@ body > div:first-of-type {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 10px rgba(236, 72, 153, 0.3);
|
||||
}
|
||||
|
||||
/* Quill editor dark theme overrides */
|
||||
.ql-toolbar.ql-snow {
|
||||
border-color: #44403c;
|
||||
background-color: #1c1917;
|
||||
}
|
||||
|
||||
.ql-container.ql-snow {
|
||||
border-color: #44403c;
|
||||
background-color: #0c0a09;
|
||||
color: #e7e5e4;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.ql-editor.ql-blank::before {
|
||||
color: #78716c;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.ql-snow .ql-stroke {
|
||||
stroke: #a8a29e;
|
||||
}
|
||||
|
||||
.ql-snow .ql-fill,
|
||||
.ql-snow .ql-stroke.ql-fill {
|
||||
fill: #a8a29e;
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker {
|
||||
color: #a8a29e;
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-options {
|
||||
background-color: #1c1917;
|
||||
border-color: #44403c;
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-label:hover,
|
||||
.ql-snow .ql-picker-label.ql-active,
|
||||
.ql-snow .ql-picker-item:hover,
|
||||
.ql-snow .ql-picker-item.ql-selected {
|
||||
color: #e7e5e4;
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-label:hover .ql-stroke,
|
||||
.ql-snow .ql-picker-label.ql-active .ql-stroke,
|
||||
.ql-snow button:hover .ql-stroke,
|
||||
.ql-snow button.ql-active .ql-stroke {
|
||||
stroke: #e7e5e4;
|
||||
}
|
||||
|
||||
.ql-snow .ql-picker-label:hover .ql-fill,
|
||||
.ql-snow .ql-picker-label.ql-active .ql-fill,
|
||||
.ql-snow button:hover .ql-fill,
|
||||
.ql-snow button.ql-active .ql-fill {
|
||||
fill: #e7e5e4;
|
||||
}
|
||||
|
||||
.ql-snow a {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.ql-tooltip {
|
||||
background-color: #1c1917 !important;
|
||||
border-color: #44403c !important;
|
||||
color: #e7e5e4 !important;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3) !important;
|
||||
}
|
||||
|
||||
.ql-tooltip input[type="text"] {
|
||||
background-color: #0c0a09;
|
||||
border-color: #44403c;
|
||||
color: #e7e5e4;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { emitMapEvent } from '@/hooks/Mapper/events';
|
||||
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { CharacterTypeRaw } from '@/hooks/Mapper/types';
|
||||
import { Commands, OutCommand } from '@/hooks/Mapper/types/mapHandlers.ts';
|
||||
import { useAutoAnimate } from '@formkit/auto-animate/react';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { useCallback } from 'react';
|
||||
import classes from './Characters.module.scss';
|
||||
import {
|
||||
TooltipPosition,
|
||||
WdEveEntityPortrait,
|
||||
WdEveEntityPortraitSize,
|
||||
WdTooltipWrapper,
|
||||
} from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdCharStateWrapper } from '@/hooks/Mapper/components/characters/components/WdCharStateWrapper.tsx';
|
||||
|
||||
interface CharactersProps {
|
||||
data: CharacterTypeRaw[];
|
||||
}
|
||||
@@ -17,7 +22,7 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
|
||||
const {
|
||||
outCommand,
|
||||
data: { mainCharacterEveId, followingCharacterEveId },
|
||||
data: { mainCharacterEveId, followingCharacterEveId, expiredCharacters },
|
||||
} = useMapRootState();
|
||||
|
||||
const handleSelect = useCallback(async (character: CharacterTypeRaw) => {
|
||||
@@ -35,61 +40,48 @@ export const Characters = ({ data }: CharactersProps) => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const items = data.map(character => (
|
||||
<li
|
||||
key={character.eve_id}
|
||||
className="flex flex-col items-center justify-center"
|
||||
onClick={() => handleSelect(character)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-hidden relative',
|
||||
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
|
||||
'transition-colors duration-250 hover:bg-stone-300/90',
|
||||
{
|
||||
['border-stone-800/90']: !character.online,
|
||||
['border-lime-600/70']: character.online,
|
||||
},
|
||||
)}
|
||||
title={character.name}
|
||||
>
|
||||
{mainCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[2px] left-[22px] w-[9px] h-[9px]',
|
||||
'text-yellow-500 text-[9px] rounded-[1px] z-10',
|
||||
'pi',
|
||||
PrimeIcons.STAR_FILL,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
const items = data.map(character => {
|
||||
const isExpired = expiredCharacters.includes(character.eve_id);
|
||||
|
||||
{followingCharacterEveId === character.eve_id && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[23px] left-[22px] w-[10px] h-[10px]',
|
||||
'text-sky-300 text-[10px] rounded-[1px] z-10',
|
||||
'pi pi-angle-double-right',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isDocked(character.location) && <div className={classes.Docked} />}
|
||||
<div
|
||||
className={clsx(
|
||||
'flex w-full h-full bg-transparent cursor-pointer',
|
||||
'bg-center bg-no-repeat bg-[length:100%]',
|
||||
'transition-opacity',
|
||||
'shadow-[inset_0_1px_6px_1px_#000000]',
|
||||
{
|
||||
['opacity-60']: !character.online,
|
||||
['opacity-100']: character.online,
|
||||
},
|
||||
)}
|
||||
style={{ backgroundImage: `url(https://images.evetech.net/characters/${character.eve_id}/portrait)` }}
|
||||
></div>
|
||||
</div>
|
||||
</li>
|
||||
));
|
||||
return (
|
||||
<li
|
||||
key={character.eve_id}
|
||||
className="flex flex-col items-center justify-center"
|
||||
onClick={() => handleSelect(character)}
|
||||
>
|
||||
<WdTooltipWrapper
|
||||
position={TooltipPosition.bottom}
|
||||
content={isExpired ? `Token is expired for ${character.name}` : character.name}
|
||||
>
|
||||
<WdCharStateWrapper
|
||||
eve_id={character.eve_id}
|
||||
location={character.location}
|
||||
isExpired={isExpired}
|
||||
isMain={mainCharacterEveId === character.eve_id}
|
||||
isFollowing={followingCharacterEveId === character.eve_id}
|
||||
isOnline={character.online}
|
||||
>
|
||||
<WdEveEntityPortrait
|
||||
eveId={character.eve_id}
|
||||
size={WdEveEntityPortraitSize.w33}
|
||||
className={clsx(
|
||||
'flex w-full h-full bg-transparent cursor-pointer',
|
||||
'bg-center bg-no-repeat bg-[length:100%]',
|
||||
'transition-opacity',
|
||||
'shadow-[inset_0_1px_6px_1px_#000000]',
|
||||
{
|
||||
['opacity-60']: !isExpired && !character.online,
|
||||
['opacity-100']: !isExpired && character.online,
|
||||
['opacity-50']: isExpired,
|
||||
},
|
||||
'!border-0',
|
||||
)}
|
||||
/>
|
||||
</WdCharStateWrapper>
|
||||
</WdTooltipWrapper>
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<ul className="flex gap-1 characters" id="characters" ref={parent}>
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
import { isDocked } from '@/hooks/Mapper/helpers/isDocked.ts';
|
||||
import classes from './WdCharStateWrapper.module.scss';
|
||||
import { WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
import { LocationRaw } from '@/hooks/Mapper/types';
|
||||
|
||||
type WdCharStateWrapperProps = {
|
||||
eve_id: string;
|
||||
isExpired?: boolean;
|
||||
isMain?: boolean;
|
||||
isFollowing?: boolean;
|
||||
location: LocationRaw | null;
|
||||
isOnline: boolean;
|
||||
} & WithChildren;
|
||||
|
||||
export const WdCharStateWrapper = ({
|
||||
location,
|
||||
isOnline,
|
||||
isMain,
|
||||
isFollowing,
|
||||
isExpired,
|
||||
children,
|
||||
}: WdCharStateWrapperProps) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'overflow-hidden relative',
|
||||
'flex w-[35px] h-[35px] rounded-[4px] border-[1px] border-solid bg-transparent cursor-pointer',
|
||||
'transition-colors duration-250 hover:bg-stone-300/90',
|
||||
{
|
||||
['border-stone-800/90']: !isExpired && !isOnline,
|
||||
['border-lime-600/70']: !isExpired && isOnline,
|
||||
['border-red-600/70']: isExpired,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{isMain && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[2px] left-[22px] w-[9px] h-[9px]',
|
||||
'text-yellow-500 text-[9px] rounded-[1px] z-10',
|
||||
'pi',
|
||||
PrimeIcons.STAR_FILL,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isFollowing && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[23px] left-[22px] w-[10px] h-[10px]',
|
||||
'text-sky-300 text-[10px] rounded-[1px] z-10',
|
||||
'pi pi-angle-double-right',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isDocked(location) && <div className={classes.Docked} />}
|
||||
{isExpired && (
|
||||
<span
|
||||
className={clsx(
|
||||
'absolute top-[4px] left-[4px] w-[10px] h-[10px]',
|
||||
'text-red-400 text-[10px] rounded-[1px] z-10',
|
||||
'pi pi-exclamation-triangle',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './WdCharStateWrapper.tsx';
|
||||
@@ -49,9 +49,9 @@ export const useContextMenuSystemInfoHandlers = () => {
|
||||
}
|
||||
|
||||
outCommand({
|
||||
type: OutCommand.addSystem,
|
||||
type: OutCommand.manualAddSystem,
|
||||
data: {
|
||||
system_id: solarSystemId,
|
||||
solar_system_id: parseInt(solarSystemId),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import {
|
||||
MASS_STATE_NAMES,
|
||||
MASS_STATE_NAMES_ORDER,
|
||||
SHIP_SIZES_NAMES,
|
||||
SHIP_SIZES_NAMES_ORDER,
|
||||
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';
|
||||
@@ -14,6 +6,8 @@ import { MenuItem } from 'primereact/menuitem';
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import { Edge } from 'reactflow';
|
||||
import { LifetimeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/LifetimeActionsWrapper.tsx';
|
||||
import { MassStatusActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/MassStatusActionsWrapper.tsx';
|
||||
import { ShipSizeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/ShipSizeActionsWrapper.tsx';
|
||||
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';
|
||||
@@ -86,16 +80,28 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
return <LifetimeActionsWrapper lifetime={edge.data?.time_status} onChangeLifetime={onChangeTimeState} />;
|
||||
},
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
className: clsx(classes.FastActions, '!h-[54px]'),
|
||||
template: () => {
|
||||
return (
|
||||
<MassStatusActionsWrapper
|
||||
massStatus={edge.data?.mass_status}
|
||||
onChangeMassStatus={onChangeMassState}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
className: clsx(classes.FastActions, '!h-[64px]'),
|
||||
template: () => {
|
||||
return (
|
||||
<ShipSizeActionsWrapper shipSize={edge.data?.ship_size_type} onChangeShipSize={onChangeShipSizeStatus} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
@@ -105,41 +111,6 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
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
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
})),
|
||||
},
|
||||
...(bothNullsec
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import {
|
||||
WdMassStatusSelector,
|
||||
WdMassStatusSelectorProps,
|
||||
} from '@/hooks/Mapper/components/ui-kit/WdMassStatusSelector.tsx';
|
||||
|
||||
export const MassStatusActionsWrapper = (props: WdMassStatusSelectorProps) => {
|
||||
return (
|
||||
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
|
||||
<div className="text-[12px] text-stone-500 font-semibold">Mass status:</div>
|
||||
|
||||
<WdMassStatusSelector {...props} />
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdShipSizeSelector, WdShipSizeSelectorProps } from '@/hooks/Mapper/components/ui-kit/WdShipSizeSelector.tsx';
|
||||
|
||||
export const ShipSizeActionsWrapper = (props: WdShipSizeSelectorProps) => {
|
||||
return (
|
||||
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
|
||||
<div className="text-[12px] text-stone-500 font-semibold">Ship size:</div>
|
||||
|
||||
<WdShipSizeSelector {...props} />
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
};
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
STATUS_CLASSES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { UnsplashedSignatureColumn } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { Tag } from 'primereact/tag';
|
||||
@@ -177,17 +177,19 @@ export const SolarSystemNodeDefault = memo((props: NodeProps<MapSolarSystemType>
|
||||
<>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
<UnsplashedSignatureColumn
|
||||
signatures={nodeVars.unsplashedLeft}
|
||||
wormholesData={nodeVars.wormholesData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
<UnsplashedSignatureColumn
|
||||
signatures={nodeVars.unsplashedRight}
|
||||
wormholesData={nodeVars.wormholesData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
STATUS_CLASSES,
|
||||
} from '@/hooks/Mapper/components/map/constants';
|
||||
import { WormholeClassComp } from '@/hooks/Mapper/components/map/components/WormholeClassComp';
|
||||
import { UnsplashedSignature } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { UnsplashedSignatureColumn } from '@/hooks/Mapper/components/map/components/UnsplashedSignature';
|
||||
import { TooltipPosition, WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { TooltipSize } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper/utils.ts';
|
||||
import { LocalCounter } from '@/hooks/Mapper/components/map/components/LocalCounter';
|
||||
@@ -157,17 +157,19 @@ export const SolarSystemNodeTheme = memo((props: NodeProps<MapSolarSystemType>)
|
||||
<>
|
||||
{nodeVars.unsplashedLeft.length > 0 && (
|
||||
<div className={classes.Unsplashed}>
|
||||
{nodeVars.unsplashedLeft.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
<UnsplashedSignatureColumn
|
||||
signatures={nodeVars.unsplashedLeft}
|
||||
wormholesData={nodeVars.wormholesData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{nodeVars.unsplashedRight.length > 0 && (
|
||||
<div className={clsx(classes.Unsplashed, classes['Unsplashed--right'])}>
|
||||
{nodeVars.unsplashedRight.map(sig => (
|
||||
<UnsplashedSignature key={sig.eve_id} signature={sig} />
|
||||
))}
|
||||
<UnsplashedSignatureColumn
|
||||
signatures={nodeVars.unsplashedRight}
|
||||
wormholesData={nodeVars.wormholesData}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
@use '@/hooks/Mapper/components/map/styles/eve-common-variables';
|
||||
|
||||
.Signature {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
display: block;
|
||||
|
||||
& > .Box {
|
||||
width: 13px;
|
||||
height: 4px;
|
||||
border-radius: 4px;
|
||||
color: var(--text-color);
|
||||
font-size: 8px;
|
||||
text-align: center;
|
||||
font-weight: bolder;
|
||||
display: block;
|
||||
}
|
||||
|
||||
& > .Eol {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
|
||||
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo.ts';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import clsx from 'clsx';
|
||||
import { useMemo } from 'react';
|
||||
import classes from './UnsplashedSignature.module.scss';
|
||||
|
||||
interface UnsplashedSignatureProps {
|
||||
signature: SystemSignature;
|
||||
}
|
||||
export const UnsplashedSignature = ({ signature }: UnsplashedSignatureProps) => {
|
||||
const {
|
||||
data: { wormholesData },
|
||||
} = useMapRootState();
|
||||
|
||||
const whData = useMemo(() => wormholesData[signature.type], [signature.type, wormholesData]);
|
||||
const whClass = useMemo(() => (whData ? WORMHOLES_ADDITIONAL_INFO[whData.dest] : null), [whData]);
|
||||
|
||||
const customInfo = useMemo(() => {
|
||||
return parseSignatureCustomInfo(signature.custom_info);
|
||||
}, [signature]);
|
||||
|
||||
const k162TypeOption = useMemo(() => {
|
||||
if (!customInfo?.k162Type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return K162_TYPES_MAP[customInfo.k162Type];
|
||||
}, [customInfo]);
|
||||
|
||||
const isEOL = useMemo(() => {
|
||||
return customInfo?.time_status === TimeStatus._1h;
|
||||
}, [customInfo]);
|
||||
|
||||
const is4H = useMemo(() => {
|
||||
return customInfo?.time_status === TimeStatus._4h;
|
||||
}, [customInfo]);
|
||||
|
||||
const whClassStyle = useMemo(() => {
|
||||
if (signature.type === 'K162' && k162TypeOption) {
|
||||
const k162Data = wormholesData[k162TypeOption.whClassName];
|
||||
const k162Class = k162Data ? WORMHOLES_ADDITIONAL_INFO[k162Data.dest] : null;
|
||||
return k162Class ? WORMHOLE_CLASS_STYLES[k162Class.wormholeClassID] : '';
|
||||
}
|
||||
return whClass ? WORMHOLE_CLASS_STYLES[whClass.wormholeClassID] : '';
|
||||
}, [signature, whClass, k162TypeOption, wormholesData]);
|
||||
|
||||
return (
|
||||
<WdTooltipWrapper
|
||||
className={clsx(classes.Signature)}
|
||||
// @ts-ignore
|
||||
content={
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">{signature.eve_id}</b>}>
|
||||
{renderInfoColumn(signature)}
|
||||
</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">
|
||||
<rect y="1" width="13" height="4" rx="2" className={whClassStyle} fill="currentColor" />
|
||||
{isEOL && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#a153ac" />}
|
||||
{is4H && <rect x="4" width="5" height="6" rx="1" className={clsx(classes.Eol)} fill="#d8b4fe" />}
|
||||
</svg>
|
||||
</div>
|
||||
</WdTooltipWrapper>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { InfoDrawer } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { renderInfoColumn } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/renders';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
|
||||
import { resolveSignatureFillVar } from '@/hooks/Mapper/components/map/helpers/wormholeClassFillVars';
|
||||
import { MassState, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes';
|
||||
|
||||
interface PillData {
|
||||
signature: SystemSignature;
|
||||
fill: string;
|
||||
isEOL: boolean;
|
||||
is4H: boolean;
|
||||
isVerge: boolean;
|
||||
isHalf: boolean;
|
||||
}
|
||||
|
||||
const PILL_W = 13;
|
||||
const PILL_H = 8;
|
||||
const GAP = 2;
|
||||
const COLS = 4;
|
||||
|
||||
interface UnsplashedSignatureColumnProps {
|
||||
signatures: SystemSignature[];
|
||||
wormholesData: Record<string, WormholeDataRaw>;
|
||||
}
|
||||
|
||||
export const UnsplashedSignatureColumn = ({ signatures, wormholesData }: UnsplashedSignatureColumnProps) => {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
const [hoveredIndex, setHoveredIndex] = useState(-1);
|
||||
|
||||
const pills: PillData[] = useMemo(() => {
|
||||
return signatures.map(sig => {
|
||||
const customInfo = parseSignatureCustomInfo(sig.custom_info);
|
||||
return {
|
||||
signature: sig,
|
||||
fill: resolveSignatureFillVar(sig, wormholesData),
|
||||
isEOL: customInfo.time_status === TimeStatus._1h,
|
||||
is4H: customInfo.time_status === TimeStatus._4h,
|
||||
isVerge: customInfo.mass_status === MassState.verge,
|
||||
isHalf: customInfo.mass_status === MassState.half,
|
||||
};
|
||||
});
|
||||
}, [signatures, wormholesData]);
|
||||
|
||||
const count = pills.length;
|
||||
if (count === 0) return null;
|
||||
|
||||
const cols = Math.min(count, COLS);
|
||||
const rows = Math.ceil(count / COLS);
|
||||
const svgWidth = cols * (PILL_W + GAP) - GAP;
|
||||
const svgHeight = rows * (PILL_H + GAP) - GAP;
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(e: React.MouseEvent<SVGSVGElement>) => {
|
||||
const svg = svgRef.current;
|
||||
if (!svg) return;
|
||||
const rect = svg.getBoundingClientRect();
|
||||
const localX = e.clientX - rect.left;
|
||||
const localY = e.clientY - rect.top;
|
||||
|
||||
const col = Math.floor(localX / (PILL_W + GAP));
|
||||
const row = Math.floor(localY / (PILL_H + GAP));
|
||||
|
||||
const withinPill =
|
||||
localX - col * (PILL_W + GAP) <= PILL_W && localY - row * (PILL_H + GAP) <= PILL_H;
|
||||
|
||||
const idx = withinPill ? row * COLS + col : -1;
|
||||
setHoveredIndex(idx < count ? idx : -1);
|
||||
},
|
||||
[count],
|
||||
);
|
||||
|
||||
const onMouseLeave = useCallback(() => setHoveredIndex(-1), []);
|
||||
|
||||
// Compute tooltip position relative to viewport
|
||||
let tooltipX = 0;
|
||||
let tooltipY = 0;
|
||||
if (hoveredIndex >= 0 && svgRef.current) {
|
||||
const svgRect = svgRef.current.getBoundingClientRect();
|
||||
const col = hoveredIndex % COLS;
|
||||
const row = Math.floor(hoveredIndex / COLS);
|
||||
tooltipX = svgRect.left + col * (PILL_W + GAP) + PILL_W / 2;
|
||||
tooltipY = svgRect.top + row * (PILL_H + GAP) - 4;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width={svgWidth}
|
||||
height={svgHeight}
|
||||
viewBox={`0 0 ${svgWidth} ${svgHeight}`}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ display: 'block', position: 'relative', top: 3 }}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
{pills.map((pill, i) => {
|
||||
const col = i % COLS;
|
||||
const row = Math.floor(i / COLS);
|
||||
const x = col * (PILL_W + GAP);
|
||||
const y = row * (PILL_H + GAP);
|
||||
return (
|
||||
<g key={pill.signature.eve_id} transform={`translate(${x},${y})`}>
|
||||
<rect y="1" width="13" height="4" rx="2" fill={pill.fill} />
|
||||
{pill.isEOL && <rect x="4" width="5" height="6" rx="1" fill="#a153ac" />}
|
||||
{pill.is4H && <rect x="4" width="5" height="6" rx="1" fill="#d8b4fe" />}
|
||||
{pill.isVerge && <rect x="0" width="5" height="6" rx="1" fill="#af0000" />}
|
||||
{pill.isHalf && <rect x="0" width="5" height="6" rx="1" fill="#ffd700" />}
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
</svg>
|
||||
|
||||
{hoveredIndex >= 0 &&
|
||||
pills[hoveredIndex] &&
|
||||
createPortal(
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
left: tooltipX,
|
||||
top: tooltipY,
|
||||
transform: 'translate(-50%, -100%)',
|
||||
zIndex: 9999,
|
||||
pointerEvents: 'none',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
background: 'var(--surface-card, #1e1e2e)',
|
||||
border: '1px solid var(--surface-border, #333)',
|
||||
borderRadius: 6,
|
||||
padding: '4px 6px',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.4)',
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-1">
|
||||
<InfoDrawer title={<b className="text-slate-50">{pills[hoveredIndex].signature.eve_id}</b>}>
|
||||
{renderInfoColumn(pills[hoveredIndex].signature)}
|
||||
</InfoDrawer>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1 +1 @@
|
||||
export * from './UnsplashedSignature.tsx';
|
||||
export * from './UnsplashedSignatureColumn.tsx';
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './getShapeClass';
|
||||
export * from './getBackgroundClass';
|
||||
export * from './prepareUnsplashedChunks';
|
||||
export * from './checkPermissions';
|
||||
export * from './wormholeClassFillVars';
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { WORMHOLE_CLASS_STYLES, WORMHOLES_ADDITIONAL_INFO } from '@/hooks/Mapper/components/map/constants';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants';
|
||||
import { parseSignatureCustomInfo } from '@/hooks/Mapper/helpers/parseSignatureCustomInfo';
|
||||
import { WormholeDataRaw } from '@/hooks/Mapper/types/wormholes';
|
||||
import { SystemSignature } from '@/hooks/Mapper/types/signatures';
|
||||
|
||||
// Maps WORMHOLE_CLASS_STYLES CSS class names → CSS variable references for inline SVG fill.
|
||||
// The CSS classes set `color: var(--eve-wh-type-color-*)`, but SVG `fill` needs an explicit value.
|
||||
const CLASS_NAME_TO_CSS_VAR: Record<string, string> = {
|
||||
'eve-wh-type-color-c1': 'var(--eve-wh-type-color-c1)',
|
||||
'eve-wh-type-color-c2': 'var(--eve-wh-type-color-c2)',
|
||||
'eve-wh-type-color-c3': 'var(--eve-wh-type-color-c3)',
|
||||
'eve-wh-type-color-c4': 'var(--eve-wh-type-color-c4)',
|
||||
'eve-wh-type-color-c5': 'var(--eve-wh-type-color-c5)',
|
||||
'eve-wh-type-color-c6': 'var(--eve-wh-type-color-c6)',
|
||||
'eve-wh-type-color-high': 'var(--eve-wh-type-color-high)',
|
||||
'eve-wh-type-color-low': 'var(--eve-wh-type-color-low)',
|
||||
'eve-wh-type-color-null': 'var(--eve-wh-type-color-null)',
|
||||
'eve-wh-type-color-thera': 'var(--eve-wh-type-color-thera)',
|
||||
'eve-wh-type-color-c13': 'var(--eve-wh-type-color-c13)',
|
||||
'eve-wh-type-color-drifter': 'var(--eve-wh-type-color-drifter)',
|
||||
'eve-wh-type-color-zarzakh': 'var(--eve-wh-type-color-zarzakh)',
|
||||
// eve-kind-color-abyss and eve-kind-color-penalty both resolve to --eve-wh-type-color-c6
|
||||
'eve-kind-color-abyss': 'var(--eve-wh-type-color-c6)',
|
||||
'eve-kind-color-penalty': 'var(--eve-wh-type-color-c6)',
|
||||
};
|
||||
|
||||
const DEFAULT_FILL = '#833ca4';
|
||||
|
||||
export function resolveSignatureFillVar(
|
||||
signature: SystemSignature,
|
||||
wormholesData: Record<string, WormholeDataRaw>,
|
||||
): string {
|
||||
const customInfo = parseSignatureCustomInfo(signature.custom_info);
|
||||
|
||||
// K162 override: use the k162Type to look up the real destination class
|
||||
if (signature.type === 'K162' && customInfo.k162Type) {
|
||||
const k162Option = K162_TYPES_MAP[customInfo.k162Type];
|
||||
if (k162Option) {
|
||||
const k162Data = wormholesData[k162Option.whClassName];
|
||||
const k162Class = k162Data ? WORMHOLES_ADDITIONAL_INFO[k162Data.dest] : null;
|
||||
if (k162Class) {
|
||||
const className = WORMHOLE_CLASS_STYLES[k162Class.wormholeClassID];
|
||||
if (className && CLASS_NAME_TO_CSS_VAR[className]) {
|
||||
return CLASS_NAME_TO_CSS_VAR[className];
|
||||
}
|
||||
}
|
||||
}
|
||||
return DEFAULT_FILL;
|
||||
}
|
||||
|
||||
// Normal type lookup
|
||||
const whData = wormholesData[signature.type];
|
||||
if (!whData) return DEFAULT_FILL;
|
||||
|
||||
const whClass = WORMHOLES_ADDITIONAL_INFO[whData.dest];
|
||||
if (!whClass) return DEFAULT_FILL;
|
||||
|
||||
const className = WORMHOLE_CLASS_STYLES[whClass.wormholeClassID];
|
||||
if (!className) return DEFAULT_FILL;
|
||||
|
||||
return CLASS_NAME_TO_CSS_VAR[className] || DEFAULT_FILL;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import { Regions, REGIONS_MAP, SPACE_TO_CLASS } from '@/hooks/Mapper/constants';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace';
|
||||
import { getSystemClassStyles } from '@/hooks/Mapper/components/map/helpers';
|
||||
import { sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import { CharacterTypeRaw, OutCommand, PingType, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { CharacterTypeRaw, OutCommand, PingType, SystemSignature, WormholeDataRaw } from '@/hooks/Mapper/types';
|
||||
import { useUnsplashedSignatures } from './useUnsplashedSignatures';
|
||||
import { useSystemName } from './useSystemName';
|
||||
import { LabelInfo, useLabelsInfo } from './useLabelsInfo';
|
||||
@@ -46,6 +46,7 @@ export interface SolarSystemNodeVars {
|
||||
userCharacters: string[];
|
||||
unsplashedLeft: Array<SystemSignature>;
|
||||
unsplashedRight: Array<SystemSignature>;
|
||||
wormholesData: Record<string, WormholeDataRaw>;
|
||||
isThickConnections: boolean;
|
||||
isRally: boolean;
|
||||
classTitle: string | null;
|
||||
@@ -210,6 +211,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
charactersInSystem,
|
||||
unsplashedLeft,
|
||||
unsplashedRight,
|
||||
wormholesData,
|
||||
isThickConnections,
|
||||
classTitle: class_title,
|
||||
temporaryName: computedTemporaryName,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SystemViewStandalone, TooltipPosition, WHClassView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { MassState, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
|
||||
import { renderK162Type } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureK162TypeSelect';
|
||||
@@ -21,17 +21,27 @@ export const renderInfoColumn = (row: SystemSignature) => {
|
||||
{row.temporary_name && <span className={clsx('text-[12px]')}>{row.temporary_name}</span>}
|
||||
|
||||
{customInfo.time_status === TimeStatus._1h && (
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as EOL">
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.bottom} content="Signature marked as EOL">
|
||||
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{customInfo.isCrit && (
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.top} content="Signature marked as Crit">
|
||||
<WdTooltipWrapper offset={5} position={TooltipPosition.bottom} content="Signature marked as Crit">
|
||||
<div className="pi pi-clock text-fuchsia-400 text-[11px] mr-[2px]"></div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{customInfo.mass_status === MassState.verge && (
|
||||
<WdTooltipWrapper
|
||||
offset={5}
|
||||
position={TooltipPosition.bottom}
|
||||
content="Signature marked as Verge of collapse"
|
||||
>
|
||||
<div className="pi pi-exclamation-triangle text-red-400 text-[11px] mr-[2px]"></div>
|
||||
</WdTooltipWrapper>
|
||||
)}
|
||||
|
||||
{row.type && (
|
||||
<WHClassView
|
||||
className="text-[11px]"
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { MassState, OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
@@ -15,6 +15,7 @@ type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & {
|
||||
linked_system: string;
|
||||
k162Type: string;
|
||||
time_status: TimeStatus;
|
||||
mass_status: MassState;
|
||||
};
|
||||
|
||||
export interface MapSettingsProps {
|
||||
@@ -59,6 +60,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
custom_info: JSON.stringify({
|
||||
k162Type: values.k162Type,
|
||||
time_status: values.time_status,
|
||||
mass_status: values.mass_status,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -139,16 +141,19 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
|
||||
let k162Type = null;
|
||||
let time_status = TimeStatus._24h;
|
||||
let mass_status = MassState.normal;
|
||||
if (custom_info) {
|
||||
const customInfo = JSON.parse(custom_info);
|
||||
k162Type = customInfo.k162Type;
|
||||
time_status = customInfo.time_status;
|
||||
mass_status = customInfo.mass_status ?? MassState.normal;
|
||||
}
|
||||
|
||||
signatureForm.reset({
|
||||
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
|
||||
k162Type: k162Type,
|
||||
time_status: time_status,
|
||||
mass_status: mass_status,
|
||||
...rest,
|
||||
});
|
||||
}, [signatureForm, signatureData]);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootConten
|
||||
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
|
||||
import { SignatureLifetimeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLifetimeSelect.tsx';
|
||||
import { SignatureTempName } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureTempName.tsx';
|
||||
import { SignatureMassStatusSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureMassStatusSelect.tsx';
|
||||
|
||||
export const SignatureGroupContentWormholes = () => {
|
||||
const { watch } = useFormContext<SystemSignature>();
|
||||
@@ -34,6 +35,11 @@ export const SignatureGroupContentWormholes = () => {
|
||||
<SignatureLifetimeSelect name="time_status" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>Mass status:</span>
|
||||
<SignatureMassStatusSelect name="mass_status" />
|
||||
</div>
|
||||
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>Temp. Name:</span>
|
||||
<SignatureTempName />
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { MassState, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { WdMassStatusSelector } from '@/hooks/Mapper/components/ui-kit/WdMassStatusSelector.tsx';
|
||||
|
||||
export interface SignatureMassStatusSelectProps {
|
||||
name: string;
|
||||
defaultValue?: MassState;
|
||||
}
|
||||
|
||||
export const SignatureMassStatusSelect = ({
|
||||
name,
|
||||
defaultValue = MassState.normal,
|
||||
}: SignatureMassStatusSelectProps) => {
|
||||
const { control } = useFormContext<SystemSignature>();
|
||||
|
||||
return (
|
||||
<div className="my-1">
|
||||
<Controller
|
||||
// @ts-ignore
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field }) => {
|
||||
// @ts-ignore
|
||||
return <WdMassStatusSelector massStatus={field.value} onChangeMassStatus={e => field.onChange(e)} />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,3 +2,4 @@ export * from './SignatureGroupSelect';
|
||||
export * from './SignatureGroupContent';
|
||||
export * from './SignatureK162TypeSelect';
|
||||
export * from './SignatureLifetimeSelect';
|
||||
export * from './SignatureMassStatusSelect';
|
||||
|
||||
@@ -4,12 +4,16 @@ import { DataTable } from 'primereact/datatable';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { TrackingCharacter } from '@/hooks/Mapper/types';
|
||||
import { useTracking } from '@/hooks/Mapper/components/mapRootContent/components/TrackingDialog/TrackingProvider.tsx';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
|
||||
export const TrackingCharactersList = () => {
|
||||
const [selected, setSelected] = useState<TrackingCharacter[]>([]);
|
||||
const { trackingCharacters, main, following, updateTracking } = useTracking();
|
||||
const refVars = useRef({ trackingCharacters });
|
||||
refVars.current = { trackingCharacters };
|
||||
const {
|
||||
data: { expiredCharacters },
|
||||
} = useMapRootState();
|
||||
|
||||
useEffect(() => {
|
||||
setSelected(trackingCharacters.filter(x => x.tracked));
|
||||
@@ -66,7 +70,9 @@ export const TrackingCharactersList = () => {
|
||||
bodyClassName="text-ellipsis overflow-hidden whitespace-nowrap"
|
||||
headerClassName="[&_div]:ml-2"
|
||||
body={row => {
|
||||
return <CharacterCard showCorporationLogo showTicker isOwn {...row.character} />;
|
||||
const isExpired = expiredCharacters.includes(row.character.eve_id);
|
||||
|
||||
return <CharacterCard showCorporationLogo showTicker isOwn isExpired={isExpired} {...row.character} />;
|
||||
}}
|
||||
/>
|
||||
</DataTable>
|
||||
|
||||
@@ -38,7 +38,7 @@ const TrackingDialogComp = ({ visible, onHide }: TrackingDialogProps) => {
|
||||
resizable={false}
|
||||
visible={visible}
|
||||
onHide={onHide}
|
||||
className="w-[640px] h-[400px] text-text-color min-h-0"
|
||||
className="w-[640px] h-[600px] text-text-color min-h-0"
|
||||
>
|
||||
<TabView
|
||||
className="vertical-tabs-container h-full [&_.p-tabview-panels]:!pr-0"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext, useCallback, useContext, useRef, useState, useEffect } from 'react';
|
||||
import { createContext, useCallback, useContext, useRef, useState } from 'react';
|
||||
import { Commands, OutCommand, TrackingCharacter } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { IncomingEvent, WithChildren } from '@/hooks/Mapper/types/common.ts';
|
||||
|
||||
@@ -17,6 +17,7 @@ import { useCallback } from 'react';
|
||||
import classes from './CharacterCard.module.scss';
|
||||
import { ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
|
||||
import { WdCharStateWrapper } from '../../characters/components';
|
||||
|
||||
export type CharacterCardProps = {
|
||||
compact?: boolean;
|
||||
@@ -29,6 +30,9 @@ export type CharacterCardProps = {
|
||||
showAllyLogo?: boolean;
|
||||
showAllyLogoPlaceholder?: boolean;
|
||||
simpleMode?: boolean;
|
||||
isExpired?: boolean;
|
||||
isMain?: boolean;
|
||||
isFollowing?: boolean;
|
||||
} & WithIsOwnCharacter &
|
||||
WithClassName;
|
||||
|
||||
@@ -55,6 +59,10 @@ export const CharacterCard = ({
|
||||
showTicker,
|
||||
useSystemsCache,
|
||||
className,
|
||||
isExpired,
|
||||
isMain,
|
||||
isFollowing,
|
||||
|
||||
...char
|
||||
}: CharacterCardInnerProps) => {
|
||||
const handleSelect = useCallback(() => {
|
||||
@@ -204,7 +212,22 @@ export const CharacterCard = ({
|
||||
<div className={clsx('w-full text-xs box-border')} onClick={handleSelect}>
|
||||
<div className="w-full flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<WdEveEntityPortrait eveId={char.eve_id} size={WdEveEntityPortraitSize.w33} />
|
||||
<WdCharStateWrapper
|
||||
eve_id={char.eve_id}
|
||||
location={char.location}
|
||||
isExpired={isExpired}
|
||||
isOnline={char.online}
|
||||
isMain={isMain}
|
||||
isFollowing={isFollowing}
|
||||
>
|
||||
<WdEveEntityPortrait
|
||||
eveId={char.eve_id}
|
||||
size={WdEveEntityPortraitSize.w33}
|
||||
className={clsx({
|
||||
['border-red-600/50']: isExpired,
|
||||
})}
|
||||
/>
|
||||
</WdCharStateWrapper>
|
||||
|
||||
{showCorporationLogo && (
|
||||
<WdTooltipWrapper position={TooltipPosition.top} content={char.corporation_name}>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
|
||||
import { MassState } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
|
||||
|
||||
const MASS_STATUS = [
|
||||
{
|
||||
id: MassState.verge,
|
||||
label: 'Verge',
|
||||
className: 'bg-red-400 hover:!bg-red-400',
|
||||
inactiveClassName: 'bg-red-400/30',
|
||||
description: 'Mass status: Verge of collapse',
|
||||
},
|
||||
{
|
||||
id: MassState.half,
|
||||
label: 'Half',
|
||||
className: 'bg-orange-300 hover:!bg-orange-300',
|
||||
inactiveClassName: 'bg-orange-300/30',
|
||||
description: 'Mass status: Half',
|
||||
},
|
||||
{
|
||||
id: MassState.normal,
|
||||
label: 'Normal',
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
description: 'Mass status: Normal',
|
||||
},
|
||||
];
|
||||
|
||||
export interface WdMassStatusSelectorProps {
|
||||
massStatus?: MassState;
|
||||
onChangeMassStatus(massStatus: MassState): void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WdMassStatusSelector = ({
|
||||
massStatus = MassState.normal,
|
||||
onChangeMassStatus,
|
||||
className,
|
||||
}: WdMassStatusSelectorProps) => {
|
||||
return (
|
||||
<form>
|
||||
<div className={clsx('grid grid-cols-[auto_auto_auto] gap-1', className)}>
|
||||
{MASS_STATUS.map(x => (
|
||||
<WdButton
|
||||
key={x.id}
|
||||
outlined={false}
|
||||
value={x.label}
|
||||
tooltip={x.description}
|
||||
tooltipOptions={BUILT_IN_TOOLTIP_OPTIONS}
|
||||
size="small"
|
||||
className={clsx(
|
||||
`py-[1px] justify-center min-w-auto w-auto border-0 text-[12px] font-bold leading-[20px]`,
|
||||
{ [x.inactiveClassName]: massStatus !== x.id },
|
||||
x.className,
|
||||
)}
|
||||
onClick={() => onChangeMassStatus(x.id)}
|
||||
>
|
||||
{x.label}
|
||||
</WdButton>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
|
||||
import { ShipSizeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
|
||||
import {
|
||||
SHIP_SIZES_DESCRIPTION,
|
||||
SHIP_SIZES_NAMES,
|
||||
SHIP_SIZES_NAMES_ORDER,
|
||||
SHIP_SIZES_NAMES_SHORT,
|
||||
SHIP_SIZES_SIZE,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
|
||||
const SHIP_SIZE_STYLES: Record<ShipSizeStatus, { className: string; inactiveClassName: string }> = {
|
||||
[ShipSizeStatus.small]: {
|
||||
className: 'bg-indigo-400 hover:!bg-indigo-400',
|
||||
inactiveClassName: 'bg-indigo-400/30',
|
||||
},
|
||||
[ShipSizeStatus.medium]: {
|
||||
className: 'bg-cyan-500 hover:!bg-cyan-500',
|
||||
inactiveClassName: 'bg-cyan-500/30',
|
||||
},
|
||||
[ShipSizeStatus.large]: {
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
},
|
||||
[ShipSizeStatus.freight]: {
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
},
|
||||
[ShipSizeStatus.capital]: {
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
},
|
||||
};
|
||||
|
||||
export interface WdShipSizeSelectorProps {
|
||||
shipSize?: ShipSizeStatus;
|
||||
onChangeShipSize(shipSize: ShipSizeStatus): void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WdShipSizeSelector = ({
|
||||
shipSize = ShipSizeStatus.large,
|
||||
onChangeShipSize,
|
||||
className,
|
||||
}: WdShipSizeSelectorProps) => {
|
||||
return (
|
||||
<form>
|
||||
<div className={clsx('grid grid-cols-[1fr_1fr_1fr_1fr_1fr] gap-1', className)}>
|
||||
{SHIP_SIZES_NAMES_ORDER.map(size => {
|
||||
const style = SHIP_SIZE_STYLES[size];
|
||||
const tooltip = `${SHIP_SIZES_NAMES[size]} • ${SHIP_SIZES_SIZE[size]} t. ${SHIP_SIZES_DESCRIPTION[size]}`;
|
||||
|
||||
return (
|
||||
<WdButton
|
||||
key={size}
|
||||
outlined={false}
|
||||
value={SHIP_SIZES_NAMES_SHORT[size]}
|
||||
tooltip={tooltip}
|
||||
tooltipOptions={BUILT_IN_TOOLTIP_OPTIONS}
|
||||
size="small"
|
||||
className={clsx(
|
||||
`py-[1px] justify-center min-w-auto w-auto border-0 text-[11px] font-bold leading-[20px]`,
|
||||
{ [style.inactiveClassName]: shipSize !== size },
|
||||
style.className,
|
||||
)}
|
||||
onClick={() => onChangeShipSize(size)}
|
||||
>
|
||||
<span className="text-[11px] font-bold">{SHIP_SIZES_NAMES_SHORT[size]}</span>
|
||||
</WdButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -56,6 +56,7 @@ export type MapRootData = MapUnionTypes & {
|
||||
trackingCharactersData: TrackingCharacter[];
|
||||
loadingPublicRoutes: boolean;
|
||||
map_slug: string | null;
|
||||
expiredCharacters: string[];
|
||||
};
|
||||
|
||||
const INITIAL_DATA: MapRootData = {
|
||||
@@ -102,6 +103,7 @@ const INITIAL_DATA: MapRootData = {
|
||||
pings: [],
|
||||
loadingPublicRoutes: false,
|
||||
map_slug: null,
|
||||
expiredCharacters: [],
|
||||
};
|
||||
|
||||
export enum InterfaceStoredSettingsProps {
|
||||
|
||||
@@ -29,6 +29,7 @@ export const useMapInit = () => {
|
||||
following_character_eve_id,
|
||||
user_hubs,
|
||||
map_slug,
|
||||
expired_characters,
|
||||
} = props;
|
||||
|
||||
const updateData: Partial<MapRootData> = {};
|
||||
@@ -108,6 +109,10 @@ export const useMapInit = () => {
|
||||
updateData.map_slug = map_slug;
|
||||
}
|
||||
|
||||
if ('expired_characters' in props) {
|
||||
updateData.expiredCharacters = expired_characters;
|
||||
}
|
||||
|
||||
update(updateData);
|
||||
},
|
||||
[update, addSystemStatic],
|
||||
|
||||
@@ -108,6 +108,7 @@ export type CommandInit = {
|
||||
main_character_eve_id?: string | null;
|
||||
following_character_eve_id?: string | null;
|
||||
map_slug?: string;
|
||||
expired_characters: string[];
|
||||
};
|
||||
|
||||
export type CommandAddSystems = SolarSystemRawType[];
|
||||
|
||||
@@ -31,6 +31,7 @@ export type SignatureCustomInfo = {
|
||||
k162Type?: string;
|
||||
time_status?: number;
|
||||
isCrit?: boolean;
|
||||
mass_status?: number;
|
||||
};
|
||||
|
||||
export type SystemSignature = {
|
||||
|
||||
@@ -9,6 +9,7 @@ import DownloadJson from './downloadJson';
|
||||
import NewVersionUpdate from './newVersionUpdate';
|
||||
import MapAction from './maps/mapAction';
|
||||
import ShowCharactersAddAlert from './showCharactersAddAlert';
|
||||
import WysiwygEditor from './wysiwygEditor';
|
||||
|
||||
export default {
|
||||
DownloadJson,
|
||||
@@ -22,4 +23,5 @@ export default {
|
||||
CopyToClipboard,
|
||||
NewVersionUpdate,
|
||||
ShowCharactersAddAlert,
|
||||
WysiwygEditor,
|
||||
};
|
||||
|
||||
46
assets/js/hooks/wysiwygEditor.ts
Normal file
46
assets/js/hooks/wysiwygEditor.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import Quill from "quill";
|
||||
import TurndownService from "turndown";
|
||||
|
||||
const WysiwygEditor = {
|
||||
mounted() {
|
||||
const view = this as any;
|
||||
const editorContainer = view.el.querySelector(".ql-editor-container");
|
||||
if (!editorContainer) return;
|
||||
|
||||
const toolbarOptions = [
|
||||
["bold", "italic", "underline", "strike"],
|
||||
["blockquote", "link"],
|
||||
[{ list: "ordered" }, { list: "bullet" }],
|
||||
[{ header: [1, 2, 3, false] }],
|
||||
["clean"],
|
||||
];
|
||||
|
||||
const quill = new Quill(editorContainer, {
|
||||
theme: "snow",
|
||||
modules: { toolbar: toolbarOptions },
|
||||
});
|
||||
|
||||
const initialContent = editorContainer.getAttribute("data-initial-content");
|
||||
if (initialContent) {
|
||||
quill.clipboard.dangerouslyPasteHTML(initialContent);
|
||||
}
|
||||
|
||||
quill.on("text-change", () => {
|
||||
view.pushEvent("content-text-change", { content: quill.getText() });
|
||||
});
|
||||
|
||||
view.handleEvent("request_editor_content", () => {
|
||||
const html = quill.root.innerHTML;
|
||||
|
||||
if (quill.getText().trim() === "") {
|
||||
view.pushEvent("editor_content_markdown", { markdown: "" });
|
||||
} else {
|
||||
const turndownService = new TurndownService();
|
||||
const markdown = turndownService.turndown(html);
|
||||
view.pushEvent("editor_content_markdown", { markdown: markdown });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default WysiwygEditor;
|
||||
@@ -43,6 +43,8 @@
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"tailwindcss": "^3.3.6",
|
||||
"quill": "^2.0.3",
|
||||
"turndown": "^7.2.0",
|
||||
"topbar": "^3.0.0",
|
||||
"use-local-storage-state": "^19.3.1"
|
||||
},
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 35 KiB |
@@ -922,6 +922,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz#775374306116d51c0c500b8c4face0f9a04752d8"
|
||||
integrity sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==
|
||||
|
||||
"@mixmark-io/domino@^2.2.0":
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3"
|
||||
integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==
|
||||
|
||||
"@nodelib/fs.scandir@2.1.5":
|
||||
version "2.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5"
|
||||
@@ -3000,6 +3005,11 @@ esutils@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
eventemitter3@^5.0.1:
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.4.tgz#a86d66170433712dde814707ac52b5271ceb1feb"
|
||||
integrity sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==
|
||||
|
||||
execa@^5.0.0:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd"
|
||||
@@ -3041,7 +3051,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
|
||||
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
|
||||
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
|
||||
|
||||
fast-diff@^1.1.2:
|
||||
fast-diff@^1.1.2, fast-diff@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
|
||||
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
|
||||
@@ -4321,11 +4331,21 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash-es@^4.17.21:
|
||||
version "4.18.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.18.1.tgz#b962eeb80d9d983a900bf342961fb7418ca10b1d"
|
||||
integrity sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==
|
||||
|
||||
lodash.castarray@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.castarray/-/lodash.castarray-4.4.0.tgz#c02513515e309daddd4c24c60cfddcf5976d9115"
|
||||
integrity sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==
|
||||
|
||||
lodash.clonedeep@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
|
||||
integrity sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==
|
||||
|
||||
lodash.debounce@^4.0.8:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
|
||||
@@ -5146,6 +5166,11 @@ package-json-from-dist@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505"
|
||||
integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
|
||||
|
||||
parchment@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/parchment/-/parchment-3.0.0.tgz#2e3a4ada454e1206ae76ea7afcb50e9fb517e7d6"
|
||||
integrity sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==
|
||||
|
||||
parent-module@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
|
||||
@@ -5451,6 +5476,25 @@ queue-microtask@^1.2.2:
|
||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||
|
||||
quill-delta@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/quill-delta/-/quill-delta-5.1.0.tgz#1c4bc08f7c8e5cc4bdc88a15a1a70c1cc72d2b48"
|
||||
integrity sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==
|
||||
dependencies:
|
||||
fast-diff "^1.3.0"
|
||||
lodash.clonedeep "^4.5.0"
|
||||
lodash.isequal "^4.5.0"
|
||||
|
||||
quill@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/quill/-/quill-2.0.3.tgz#752765a31d5a535cdc5717dc49d4e50099365eb1"
|
||||
integrity sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==
|
||||
dependencies:
|
||||
eventemitter3 "^5.0.1"
|
||||
lodash-es "^4.17.21"
|
||||
parchment "^3.0.0"
|
||||
quill-delta "^5.1.0"
|
||||
|
||||
react-dom@18.3.1, react-dom@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
|
||||
@@ -5987,16 +6031,7 @@ string-length@^4.0.1:
|
||||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@@ -6081,14 +6116,7 @@ stringify-entities@^4.0.0:
|
||||
character-entities-html4 "^2.0.0"
|
||||
character-entities-legacy "^3.0.0"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@@ -6296,6 +6324,13 @@ ts-jest@^29.1.2:
|
||||
type-fest "^4.41.0"
|
||||
yargs-parser "^21.1.1"
|
||||
|
||||
turndown@^7.2.0:
|
||||
version "7.2.4"
|
||||
resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.2.4.tgz#42d98202aefa8c188c997b586bc6da78bdf27ea2"
|
||||
integrity sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==
|
||||
dependencies:
|
||||
"@mixmark-io/domino" "^2.2.0"
|
||||
|
||||
type-check@^0.4.0, type-check@~0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1"
|
||||
@@ -6618,16 +6653,7 @@ wordwrap@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
|
||||
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
dependencies:
|
||||
ansi-styles "^4.0.0"
|
||||
string-width "^4.1.0"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
|
||||
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
|
||||
|
||||
@@ -41,6 +41,7 @@ defmodule WandererApp.Api.Character do
|
||||
)
|
||||
|
||||
define(:admin_all, action: :admin_all)
|
||||
define(:update_description, action: :update_description)
|
||||
end
|
||||
|
||||
actions do
|
||||
@@ -141,6 +142,11 @@ defmodule WandererApp.Api.Character do
|
||||
|
||||
accept([:eve_wallet_balance])
|
||||
end
|
||||
|
||||
update :update_description do
|
||||
accept([:description])
|
||||
require_atomic? false
|
||||
end
|
||||
end
|
||||
|
||||
cloak do
|
||||
@@ -211,6 +217,11 @@ defmodule WandererApp.Api.Character do
|
||||
attribute :eve_wallet_balance, :float
|
||||
attribute :tracking_pool, :string
|
||||
|
||||
attribute :description, :string do
|
||||
allow_nil? true
|
||||
constraints max_length: 10_000
|
||||
end
|
||||
|
||||
create_timestamp(:inserted_at)
|
||||
update_timestamp(:updated_at)
|
||||
end
|
||||
|
||||
@@ -22,6 +22,7 @@ defmodule WandererApp.Api.MapTransaction do
|
||||
define(:by_user, action: :by_user)
|
||||
define(:create, action: :create)
|
||||
define(:top_donators, action: :top_donators)
|
||||
define(:server_top_donators, action: :server_top_donators)
|
||||
end
|
||||
|
||||
actions do
|
||||
@@ -77,6 +78,31 @@ defmodule WandererApp.Api.MapTransaction do
|
||||
|> then(&{:ok, &1})
|
||||
end
|
||||
end
|
||||
|
||||
action :server_top_donators, {:array, :struct} do
|
||||
argument(:after, :utc_datetime, allow_nil?: true)
|
||||
|
||||
run fn input, _context ->
|
||||
base =
|
||||
from(t in __MODULE__,
|
||||
where: t.type == :in and not is_nil(t.user_id),
|
||||
group_by: [t.user_id],
|
||||
select: %{user_id: t.user_id, total_amount: sum(t.amount)},
|
||||
order_by: [desc: sum(t.amount)],
|
||||
limit: 10
|
||||
)
|
||||
|
||||
query =
|
||||
case input.arguments[:after] do
|
||||
nil -> base
|
||||
after_date -> base |> where([t], t.inserted_at >= ^after_date)
|
||||
end
|
||||
|
||||
query
|
||||
|> WandererApp.Repo.all()
|
||||
|> then(&{:ok, &1})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
||||
@@ -265,6 +265,10 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
|
||||
_ ->
|
||||
Logger.debug(fn ->
|
||||
"[Tracker] update_online skipped for character #{character_id} - no valid access token"
|
||||
end)
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
@@ -601,6 +605,10 @@ defmodule WandererApp.Character.Tracker do
|
||||
end
|
||||
|
||||
_ ->
|
||||
Logger.debug(fn ->
|
||||
"[Tracker] update_location skipped for character #{character_id} - no valid access token"
|
||||
end)
|
||||
|
||||
{:error, :skipped}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -115,9 +115,11 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end)}
|
||||
end
|
||||
|
||||
# Filter characters to only include those with actual tracking permission
|
||||
# This prevents showing characters in the tracking dialog that will fail when toggled
|
||||
defp filter_characters_with_tracking_permission(characters, %{id: map_id, owner_id: owner_id}) do
|
||||
@doc """
|
||||
Filters a list of characters to only include those with actual tracking permission on a map.
|
||||
This prevents showing characters in the tracking dialog that will fail when toggled.
|
||||
"""
|
||||
def filter_characters_with_tracking_permission(characters, %{id: map_id, owner_id: owner_id}) do
|
||||
# Load ACLs with members properly (same approach as get_map_characters)
|
||||
acls = load_map_acls_with_members(map_id)
|
||||
|
||||
@@ -215,8 +217,11 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
end
|
||||
|
||||
# Check if a character has permission to be tracked on a map
|
||||
defp check_character_tracking_permission(character, map_id) do
|
||||
@doc """
|
||||
Checks if a character has permission to be tracked on a map.
|
||||
Returns {:ok, :allowed} or {:error, reason}.
|
||||
"""
|
||||
def check_character_tracking_permission(character, map_id) do
|
||||
with {:ok, %{acls: acls, owner_id: owner_id}} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
|
||||
@@ -754,6 +754,9 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
new_expires_at: token.expires_at
|
||||
)
|
||||
|
||||
# Clear any previous invalid_grant failure counter on successful refresh
|
||||
WandererApp.Cache.delete("character:#{character_id}:invalid_grant_count")
|
||||
|
||||
{:ok, _character} =
|
||||
character
|
||||
|> WandererApp.Api.Character.update(%{
|
||||
@@ -786,12 +789,12 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Invalid grant error during token refresh",
|
||||
character_id: character_id,
|
||||
error_message: error_message,
|
||||
time_since_expiry_seconds: time_since_expiry,
|
||||
original_expires_at: expires_at
|
||||
)
|
||||
# Track consecutive invalid_grant failures before permanently invalidating tokens.
|
||||
# EVE SSO can return invalid_grant for transient server issues, so we require
|
||||
# 3 consecutive failures before wiping tokens.
|
||||
fail_key = "character:#{character_id}:invalid_grant_count"
|
||||
count = WandererApp.Cache.lookup!(fail_key, 0) + 1
|
||||
WandererApp.Cache.put(fail_key, count, ttl: :timer.hours(2))
|
||||
|
||||
# Emit telemetry for token refresh failures
|
||||
:telemetry.execute([:wanderer_app, :token, :refresh_failed], %{count: 1}, %{
|
||||
@@ -800,8 +803,29 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
time_since_expiry: time_since_expiry
|
||||
})
|
||||
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :invalid_grant}
|
||||
if count >= 3 do
|
||||
Logger.warning(
|
||||
"TOKEN_REFRESH_FAILED: Invalid grant error (#{count}/3, invalidating tokens)",
|
||||
character_id: character_id,
|
||||
error_message: error_message,
|
||||
time_since_expiry_seconds: time_since_expiry,
|
||||
original_expires_at: expires_at
|
||||
)
|
||||
|
||||
WandererApp.Cache.delete(fail_key)
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :invalid_grant}
|
||||
else
|
||||
Logger.warning(
|
||||
"TOKEN_REFRESH_FAILED: Invalid grant error (#{count}/3, deferring invalidation)",
|
||||
character_id: character_id,
|
||||
error_message: error_message,
|
||||
time_since_expiry_seconds: time_since_expiry,
|
||||
original_expires_at: expires_at
|
||||
)
|
||||
|
||||
{:error, :token_refresh_failed}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_refresh_token_result(
|
||||
@@ -833,37 +857,92 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
defp handle_refresh_token_result(
|
||||
{:error, %OAuth2.Error{} = error},
|
||||
character,
|
||||
_character,
|
||||
character_id,
|
||||
expires_at,
|
||||
scopes
|
||||
_scopes
|
||||
) do
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
{:error, :invalid_grant}
|
||||
time_since_expiry =
|
||||
DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
|
||||
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Transient OAuth2 error during token refresh",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
time_since_expiry_seconds: time_since_expiry
|
||||
)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :token, :refresh_failed], %{count: 1}, %{
|
||||
character_id: character_id,
|
||||
error_type: "oauth2_error",
|
||||
time_since_expiry: time_since_expiry
|
||||
})
|
||||
|
||||
{:error, :token_refresh_failed}
|
||||
end
|
||||
|
||||
defp handle_refresh_token_result(error, character, character_id, expires_at, scopes) do
|
||||
Logger.warning("Failed to refresh token for #{character_id}: #{inspect(error)}")
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :failed}
|
||||
defp handle_refresh_token_result(error, _character, character_id, expires_at, _scopes) do
|
||||
time_since_expiry =
|
||||
DateTime.diff(DateTime.utc_now(), DateTime.from_unix!(expires_at), :second)
|
||||
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Unexpected error during token refresh",
|
||||
character_id: character_id,
|
||||
error: inspect(error),
|
||||
time_since_expiry_seconds: time_since_expiry
|
||||
)
|
||||
|
||||
:telemetry.execute([:wanderer_app, :token, :refresh_failed], %{count: 1}, %{
|
||||
character_id: character_id,
|
||||
error_type: "unexpected_error",
|
||||
time_since_expiry: time_since_expiry
|
||||
})
|
||||
|
||||
{:error, :token_refresh_failed}
|
||||
end
|
||||
|
||||
defp invalidate_character_tokens(character, character_id, expires_at, scopes) do
|
||||
attrs = %{access_token: nil, refresh_token: nil, expires_at: expires_at, scopes: scopes}
|
||||
|
||||
with {:ok, _} <- WandererApp.Api.Character.update(character, attrs) do
|
||||
WandererApp.Character.update_character(character_id, attrs)
|
||||
:ok
|
||||
# Skip invalidation if the character was recently re-authorized via SSO.
|
||||
# This protects fresh tokens from being wiped by transient invalid_grant
|
||||
# errors that can occur shortly after re-auth.
|
||||
if WandererApp.Cache.lookup!("character:#{character_id}:reauth_grace", false) do
|
||||
Logger.info(
|
||||
"[ApiClient] Skipping token invalidation for #{character_id} - within re-auth grace period"
|
||||
)
|
||||
else
|
||||
error ->
|
||||
Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}")
|
||||
# Re-load from DB to avoid race with concurrent re-auth
|
||||
case WandererApp.Api.Character.by_id(character_id) do
|
||||
{:ok, current_character} ->
|
||||
# Only invalidate if tokens haven't been refreshed since we started
|
||||
if current_character.access_token == character.access_token do
|
||||
attrs = %{
|
||||
access_token: nil,
|
||||
refresh_token: nil,
|
||||
expires_at: expires_at,
|
||||
scopes: scopes
|
||||
}
|
||||
|
||||
with {:ok, _} <- WandererApp.Api.Character.update(current_character, attrs) do
|
||||
WandererApp.Character.update_character(character_id, attrs)
|
||||
else
|
||||
error ->
|
||||
Logger.error("Failed to clear tokens for #{character_id}: #{inspect(error)}")
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:character_token_invalid
|
||||
)
|
||||
else
|
||||
Logger.info(
|
||||
"[ApiClient] Skipping token invalidation for #{character_id} - tokens were refreshed concurrently"
|
||||
)
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
Logger.error("Failed to load character #{character_id} for token invalidation")
|
||||
end
|
||||
end
|
||||
|
||||
Phoenix.PubSub.broadcast(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}",
|
||||
:character_token_invalid
|
||||
)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@@ -515,15 +515,15 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
})
|
||||
end
|
||||
|
||||
# Update connection time_status from signature custom_info
|
||||
signature_time_status =
|
||||
# Update connection time_status and mass_status from signature custom_info
|
||||
{signature_time_status, signature_mass_status} =
|
||||
if not is_nil(signature.custom_info) do
|
||||
case Jason.decode(signature.custom_info) do
|
||||
{:ok, map} -> Map.get(map, "time_status")
|
||||
{:error, _} -> nil
|
||||
{:ok, decoded} -> {Map.get(decoded, "time_status"), Map.get(decoded, "mass_status")}
|
||||
{:error, _} -> {nil, nil}
|
||||
end
|
||||
else
|
||||
nil
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
# Update connection ship_size_type from signature wormhole type
|
||||
@@ -531,14 +531,14 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
|
||||
# Back-link detection: if current signature yields no ship_size_type (e.g., K162),
|
||||
# look for a forward signature in the target system that links back to our source
|
||||
{signature_time_status, signature_ship_size_type} =
|
||||
{signature_time_status, signature_ship_size_type, signature_mass_status} =
|
||||
if is_nil(signature_ship_size_type) do
|
||||
case Server.SignaturesImpl.find_forward_signature(
|
||||
target_system.id,
|
||||
source_system.solar_system_id
|
||||
) do
|
||||
nil ->
|
||||
{signature_time_status, signature_ship_size_type}
|
||||
{signature_time_status, signature_ship_size_type, signature_mass_status}
|
||||
|
||||
forward_sig ->
|
||||
Logger.info(
|
||||
@@ -548,20 +548,33 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
|
||||
forward_ship_size = EVEUtil.get_wh_size(forward_sig.type)
|
||||
|
||||
forward_time_status =
|
||||
if is_nil(signature_time_status) and not is_nil(forward_sig.custom_info) do
|
||||
{forward_time_status, forward_mass_status} =
|
||||
if not is_nil(forward_sig.custom_info) do
|
||||
case Jason.decode(forward_sig.custom_info) do
|
||||
{:ok, map} -> Map.get(map, "time_status")
|
||||
{:error, _} -> nil
|
||||
{:ok, decoded} ->
|
||||
fwd_time =
|
||||
if is_nil(signature_time_status),
|
||||
do: Map.get(decoded, "time_status"),
|
||||
else: signature_time_status
|
||||
|
||||
fwd_mass =
|
||||
if is_nil(signature_mass_status),
|
||||
do: Map.get(decoded, "mass_status"),
|
||||
else: signature_mass_status
|
||||
|
||||
{fwd_time, fwd_mass}
|
||||
|
||||
{:error, _} ->
|
||||
{signature_time_status, signature_mass_status}
|
||||
end
|
||||
else
|
||||
signature_time_status
|
||||
{signature_time_status, signature_mass_status}
|
||||
end
|
||||
|
||||
{forward_time_status, forward_ship_size}
|
||||
{forward_time_status, forward_ship_size, forward_mass_status}
|
||||
end
|
||||
else
|
||||
{signature_time_status, signature_ship_size_type}
|
||||
{signature_time_status, signature_ship_size_type, signature_mass_status}
|
||||
end
|
||||
|
||||
if not is_nil(signature_time_status) do
|
||||
@@ -579,6 +592,14 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
ship_size_type: signature_ship_size_type
|
||||
})
|
||||
end
|
||||
|
||||
if not is_nil(signature_mass_status) do
|
||||
Server.update_connection_mass_status(map_id, %{
|
||||
solar_system_source_id: source_system.solar_system_id,
|
||||
solar_system_target_id: solar_system_target,
|
||||
mass_status: signature_mass_status
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
# Broadcast update
|
||||
|
||||
@@ -159,7 +159,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
if is_same_user_as_owner do
|
||||
# All characters from the map owner's account have full access
|
||||
:ok
|
||||
{:ok, character_id}
|
||||
else
|
||||
[character_permissions] =
|
||||
WandererApp.Permissions.check_characters_access([character], acls)
|
||||
@@ -179,12 +179,12 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:remove_character, character_id, :no_track_permission}
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
{:ok, character_id}
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
{:ok, character_id}
|
||||
end
|
||||
end,
|
||||
timeout: :timer.seconds(60),
|
||||
@@ -193,7 +193,26 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
)
|
||||
|> Enum.reduce([], fn
|
||||
{:ok, {:remove_character, character_id, reason}}, acc ->
|
||||
[{character_id, reason} | acc]
|
||||
# Track consecutive permission failures - only remove after 3 consecutive hourly failures
|
||||
fail_key = "map_#{map_id}:char_#{character_id}:perm_fail_count"
|
||||
count = WandererApp.Cache.lookup!(fail_key, 0) + 1
|
||||
WandererApp.Cache.put(fail_key, count, ttl: :timer.hours(4))
|
||||
|
||||
if count >= 3 do
|
||||
WandererApp.Cache.delete(fail_key)
|
||||
[{character_id, reason} | acc]
|
||||
else
|
||||
Logger.info(
|
||||
"[CharacterCleanup] Character #{character_id} permission fail #{count}/3 on map #{map_id}, deferring removal"
|
||||
)
|
||||
|
||||
acc
|
||||
end
|
||||
|
||||
{:ok, {:ok, character_id}}, acc ->
|
||||
# Character passed permission check - clear any previous failure counter
|
||||
WandererApp.Cache.delete("map_#{map_id}:char_#{character_id}:perm_fail_count")
|
||||
acc
|
||||
|
||||
{:ok, _result}, acc ->
|
||||
acc
|
||||
@@ -314,10 +333,14 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
settings
|
||||
|> Enum.each(fn s ->
|
||||
Logger.info(fn ->
|
||||
"[CharacterCleanup] Map #{map_id} - destroying settings and removing character #{s.character_id}"
|
||||
"[CharacterCleanup] Map #{map_id} - untracking settings and removing character #{s.character_id}"
|
||||
end)
|
||||
|
||||
WandererApp.MapCharacterSettingsRepo.destroy!(s)
|
||||
WandererApp.MapCharacterSettingsRepo.untrack!(%{
|
||||
map_id: s.map_id,
|
||||
character_id: s.character_id
|
||||
})
|
||||
|
||||
remove_character(map_id, s.character_id)
|
||||
end)
|
||||
|
||||
@@ -780,10 +803,14 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
old_alliance_id = Map.get(cached_values, alliance_key)
|
||||
|
||||
if character.alliance_id != old_alliance_id do
|
||||
{
|
||||
[{:character_alliance, %{alliance_id: character.alliance_id}} | updates],
|
||||
Map.put(cache_updates, alliance_key, character.alliance_id)
|
||||
}
|
||||
cache_updates = Map.put(cache_updates, alliance_key, character.alliance_id)
|
||||
|
||||
if is_nil(old_alliance_id) do
|
||||
# Initial cache population, not a real change - just update cache
|
||||
{updates, cache_updates}
|
||||
else
|
||||
{[{:character_alliance, %{alliance_id: character.alliance_id}} | updates], cache_updates}
|
||||
end
|
||||
else
|
||||
{updates, cache_updates}
|
||||
end
|
||||
@@ -802,10 +829,15 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
old_corporation_id = Map.get(cached_values, corporation_key)
|
||||
|
||||
if character.corporation_id != old_corporation_id do
|
||||
{
|
||||
[{:character_corporation, %{corporation_id: character.corporation_id}} | updates],
|
||||
Map.put(cache_updates, corporation_key, character.corporation_id)
|
||||
}
|
||||
cache_updates = Map.put(cache_updates, corporation_key, character.corporation_id)
|
||||
|
||||
if is_nil(old_corporation_id) do
|
||||
# Initial cache population, not a real change - just update cache
|
||||
{updates, cache_updates}
|
||||
else
|
||||
{[{:character_corporation, %{corporation_id: character.corporation_id}} | updates],
|
||||
cache_updates}
|
||||
end
|
||||
else
|
||||
{updates, cache_updates}
|
||||
end
|
||||
@@ -952,12 +984,63 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:ok, character} =
|
||||
WandererApp.Character.get_character(character_id)
|
||||
|
||||
add_character(map_id, character, true)
|
||||
case WandererApp.Api.MapCharacterSettings.read_by_map_and_character(%{
|
||||
map_id: map_id,
|
||||
character_id: character_id
|
||||
}) do
|
||||
{:ok, %{tracked: false}} ->
|
||||
# Was previously untracked (by system cleanup or user).
|
||||
# If character now has valid tokens, check permissions and auto-restore tracking.
|
||||
# This handles the case where re-auth gives fresh tokens but DB still says tracked: false.
|
||||
if not is_nil(character.access_token) do
|
||||
case WandererApp.Character.TrackingUtils.check_character_tracking_permission(
|
||||
character,
|
||||
map_id
|
||||
) do
|
||||
{:ok, :allowed} ->
|
||||
Logger.info(
|
||||
"[CharactersImpl] Auto-restoring tracking for character #{character_id} on map #{map_id} - " <>
|
||||
"character has valid tokens and permissions after reconnect"
|
||||
)
|
||||
|
||||
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: true
|
||||
})
|
||||
add_character(map_id, character, true)
|
||||
|
||||
WandererApp.MapCharacterSettingsRepo.track(%{
|
||||
map_id: map_id,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: true
|
||||
})
|
||||
|
||||
_ ->
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Skipping re-track for character #{character_id} on map #{map_id} - " <>
|
||||
"character lacks permissions"
|
||||
end)
|
||||
|
||||
add_character(map_id, character, false)
|
||||
end
|
||||
else
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Skipping re-track for character #{character_id} on map #{map_id} - " <>
|
||||
"character has no valid access token"
|
||||
end)
|
||||
|
||||
add_character(map_id, character, false)
|
||||
end
|
||||
|
||||
_ ->
|
||||
# New character or already tracked - enable tracking
|
||||
add_character(map_id, character, true)
|
||||
|
||||
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: true
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
# Broadcasts permission update to trigger LiveView refresh for the character's user.
|
||||
|
||||
@@ -276,7 +276,13 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
map_id,
|
||||
connection_update
|
||||
),
|
||||
do: update_connection(map_id, :update_mass_status, [:mass_status], connection_update)
|
||||
do:
|
||||
update_connection(map_id, :update_mass_status, [:mass_status], connection_update, fn
|
||||
%{mass_status: old_mass_status}, %{mass_status: mass_status} = updated_connection ->
|
||||
if mass_status != old_mass_status do
|
||||
maybe_update_linked_signature_mass_status(map_id, updated_connection)
|
||||
end
|
||||
end)
|
||||
|
||||
def update_connection_ship_size_type(
|
||||
map_id,
|
||||
@@ -528,6 +534,71 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
end
|
||||
|
||||
defp maybe_update_linked_signature_mass_status(
|
||||
map_id,
|
||||
%{
|
||||
mass_status: mass_status,
|
||||
solar_system_source: solar_system_source,
|
||||
solar_system_target: solar_system_target
|
||||
} = _updated_connection
|
||||
) do
|
||||
with source_system when not is_nil(source_system) <-
|
||||
WandererApp.Map.find_system_by_location(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_source}
|
||||
),
|
||||
target_system when not is_nil(target_system) <-
|
||||
WandererApp.Map.find_system_by_location(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_target}
|
||||
),
|
||||
source_linked_signatures <-
|
||||
find_linked_signatures(source_system, target_system),
|
||||
target_linked_signatures <- find_linked_signatures(target_system, source_system) do
|
||||
update_signatures_mass_status(
|
||||
map_id,
|
||||
source_system.solar_system_id,
|
||||
source_linked_signatures,
|
||||
mass_status
|
||||
)
|
||||
|
||||
update_signatures_mass_status(
|
||||
map_id,
|
||||
target_system.solar_system_id,
|
||||
target_linked_signatures,
|
||||
mass_status
|
||||
)
|
||||
else
|
||||
error ->
|
||||
Logger.warning("Failed to update_linked_signature_mass_status: #{inspect(error)}")
|
||||
end
|
||||
end
|
||||
|
||||
defp update_signatures_mass_status(_map_id, _solar_system_id, [], _mass_status), do: :ok
|
||||
|
||||
defp update_signatures_mass_status(map_id, solar_system_id, signatures, mass_status) do
|
||||
signatures
|
||||
|> Enum.each(fn %{custom_info: custom_info_json} = sig ->
|
||||
update_params =
|
||||
if not is_nil(custom_info_json) do
|
||||
updated_custom_info =
|
||||
custom_info_json
|
||||
|> Jason.decode!()
|
||||
|> Map.merge(%{"mass_status" => mass_status})
|
||||
|> Jason.encode!()
|
||||
|
||||
%{custom_info: updated_custom_info}
|
||||
else
|
||||
updated_custom_info = Jason.encode!(%{"mass_status" => mass_status})
|
||||
%{custom_info: updated_custom_info}
|
||||
end
|
||||
|
||||
SignaturesImpl.apply_update_signature(map_id, sig, update_params)
|
||||
end)
|
||||
|
||||
Impl.broadcast!(map_id, :signatures_updated, solar_system_id)
|
||||
end
|
||||
|
||||
def maybe_add_connection(
|
||||
map_id,
|
||||
location,
|
||||
|
||||
@@ -222,6 +222,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
{:ok, updated} ->
|
||||
maybe_update_connection_time_status(map_id, existing, updated)
|
||||
maybe_update_connection_mass_status(map_id, existing, updated)
|
||||
maybe_sync_custom_mass_status_to_connection(map_id, existing, updated)
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
@@ -275,6 +276,29 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
|
||||
defp maybe_update_connection_mass_status(_map_id, _old_sig, _updated_sig), do: :ok
|
||||
|
||||
defp maybe_sync_custom_mass_status_to_connection(
|
||||
map_id,
|
||||
%{custom_info: old_custom_info} = _old_sig,
|
||||
%{custom_info: new_custom_info, system_id: system_id, linked_system_id: linked_system_id} =
|
||||
_updated_sig
|
||||
)
|
||||
when not is_nil(linked_system_id) do
|
||||
old_mass_status = get_mass_status(old_custom_info)
|
||||
new_mass_status = get_mass_status(new_custom_info)
|
||||
|
||||
if old_mass_status != new_mass_status and not is_nil(new_mass_status) do
|
||||
{:ok, source_system} = MapSystem.by_id(system_id)
|
||||
|
||||
ConnectionsImpl.update_connection_mass_status(map_id, %{
|
||||
solar_system_source_id: source_system.solar_system_id,
|
||||
solar_system_target_id: linked_system_id,
|
||||
mass_status: new_mass_status
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_sync_custom_mass_status_to_connection(_map_id, _old_sig, _updated_sig), do: :ok
|
||||
|
||||
@doc """
|
||||
Finds the "forward" signature in a target system that links back to the source system.
|
||||
Used for back-link detection: when a K162 is linked from System B → System A,
|
||||
@@ -367,4 +391,12 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
|> Jason.decode!()
|
||||
|> Map.get("time_status")
|
||||
end
|
||||
|
||||
defp get_mass_status(nil), do: nil
|
||||
|
||||
defp get_mass_status(custom_info_json) do
|
||||
custom_info_json
|
||||
|> Jason.decode!()
|
||||
|> Map.get("mass_status")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -789,8 +789,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
defp do_add_system(
|
||||
map_id,
|
||||
%{
|
||||
solar_system_id: solar_system_id,
|
||||
coordinates: coordinates
|
||||
solar_system_id: solar_system_id
|
||||
} = system_info,
|
||||
user_id,
|
||||
character_id
|
||||
@@ -803,19 +802,14 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|
||||
rtree_name = "rtree_#{map_id}"
|
||||
|
||||
%{"x" => x, "y" => y} =
|
||||
coordinates
|
||||
system_info
|
||||
|> Map.get(:coordinates)
|
||||
|> case do
|
||||
%{"x" => x, "y" => y} ->
|
||||
%{"x" => x, "y" => y}
|
||||
|
||||
_ ->
|
||||
%{x: x, y: y} =
|
||||
WandererApp.Map.PositionCalculator.get_new_system_position(
|
||||
nil,
|
||||
rtree_name,
|
||||
map_opts
|
||||
)
|
||||
|
||||
{:ok, %{x: x, y: y}} = calc_new_system_position(map_id, nil, rtree_name, map_opts)
|
||||
%{"x" => x, "y" => y}
|
||||
end
|
||||
|
||||
|
||||
@@ -46,14 +46,18 @@ defmodule WandererApp.Ueberauth.Strategy.Eve do
|
||||
|> with_param(:hl, conn)
|
||||
|> with_state_param(conn)
|
||||
|
||||
opts = oauth_client_options_from_conn(conn, with_wallet, is_admin?)
|
||||
|
||||
WandererApp.Cache.put(
|
||||
"eve_auth_#{params[:state]}",
|
||||
[with_wallet: with_wallet, is_admin?: is_admin?],
|
||||
[
|
||||
with_wallet: with_wallet,
|
||||
is_admin?: is_admin?,
|
||||
tracking_pool: Keyword.get(opts, :tracking_pool)
|
||||
],
|
||||
ttl: :timer.minutes(30)
|
||||
)
|
||||
|
||||
opts = oauth_client_options_from_conn(conn, with_wallet, is_admin?)
|
||||
|
||||
redirect!(conn, WandererApp.Ueberauth.Strategy.Eve.OAuth.authorize_url!(params, opts))
|
||||
|
||||
false ->
|
||||
|
||||
@@ -68,6 +68,13 @@ defmodule WandererAppWeb do
|
||||
end
|
||||
end
|
||||
|
||||
def blog_live_view do
|
||||
live_view(
|
||||
layout: {WandererAppWeb.Layouts, :blog},
|
||||
container: {:div, class: ""}
|
||||
)
|
||||
end
|
||||
|
||||
def live_component do
|
||||
quote do
|
||||
use Phoenix.LiveComponent
|
||||
|
||||
@@ -206,6 +206,7 @@ defmodule WandererAppWeb.Layouts do
|
||||
>
|
||||
<li><a href="/changelog">Changelog</a></li>
|
||||
<li><a href="/news">News</a></li>
|
||||
<li :if={@map_subscriptions_enabled}><a href="/sponsors">Sponsors</a></li>
|
||||
<li><a href="/license">License</a></li>
|
||||
<li><a href="/contacts">Contact Us</a></li>
|
||||
</ul>
|
||||
@@ -236,6 +237,13 @@ defmodule WandererAppWeb.Layouts do
|
||||
icon="hero-signal-solid"
|
||||
tip="Characters Tracking"
|
||||
/>
|
||||
<.nav_link
|
||||
:if={@map_subscriptions_enabled}
|
||||
href="/sponsors"
|
||||
active={@active_tab == :sponsors}
|
||||
icon="hero-heart-solid"
|
||||
tip="Our Sponsors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -10,6 +10,12 @@ defmodule WandererAppWeb.AuthController do
|
||||
def callback(%{assigns: %{ueberauth_auth: auth, current_user: user} = _assigns} = conn, _params) do
|
||||
active_tracking_pool = WandererApp.Character.TrackingConfigUtils.get_active_pool!()
|
||||
|
||||
Logger.info(
|
||||
"[AuthController] SSO callback SUCCESS for eve_id=#{auth.info.email}, " <>
|
||||
"has_token=#{not is_nil(auth.credentials.token)}, " <>
|
||||
"has_refresh=#{not is_nil(auth.credentials.refresh_token)}"
|
||||
)
|
||||
|
||||
character_data = %{
|
||||
eve_id: "#{auth.info.email}",
|
||||
name: auth.info.name,
|
||||
@@ -40,8 +46,25 @@ defmodule WandererAppWeb.AuthController do
|
||||
character
|
||||
|> WandererApp.Api.Character.update(character_update)
|
||||
|
||||
Logger.info(
|
||||
"[AuthController] Character #{character.id} tokens updated in DB, " <>
|
||||
"access_token_present=#{not is_nil(character.access_token)}"
|
||||
)
|
||||
|
||||
WandererApp.Character.update_character(character.id, character_update)
|
||||
|
||||
# Clear the invalid_grant counter so stale failures don't cause
|
||||
# premature token invalidation after a successful re-auth
|
||||
WandererApp.Cache.delete("character:#{character.id}:invalid_grant_count")
|
||||
|
||||
# Set a grace period to protect fresh tokens from being wiped by
|
||||
# in-flight or immediately-subsequent invalid_grant errors
|
||||
WandererApp.Cache.put(
|
||||
"character:#{character.id}:reauth_grace",
|
||||
true,
|
||||
ttl: :timer.minutes(5)
|
||||
)
|
||||
|
||||
# Update corporation/alliance data from ESI to ensure access control is current
|
||||
update_character_affiliation(character)
|
||||
|
||||
@@ -96,7 +119,16 @@ defmodule WandererAppWeb.AuthController do
|
||||
end
|
||||
|
||||
def callback(conn, _params) do
|
||||
# This runs when Ueberauth auth FAILED — tokens are NOT updated
|
||||
ueberauth_failure = conn.assigns[:ueberauth_failure]
|
||||
|
||||
Logger.warning(
|
||||
"[AuthController] SSO callback FAILED - no ueberauth_auth in assigns. " <>
|
||||
"Failure: #{inspect(ueberauth_failure)}"
|
||||
)
|
||||
|
||||
conn
|
||||
|> put_flash(:error, "Authorization failed. Please try again.")
|
||||
|> redirect(to: "/characters")
|
||||
end
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ defmodule WandererAppWeb.RouteBuilderController do
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("[RouteBuilderController] find_closest failed: #{inspect(reason)}")
|
||||
|
||||
conn
|
||||
|> put_status(:bad_gateway)
|
||||
|> json(%{error: "route_builder_failed"})
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
defmodule WandererAppWeb.CharacterProfileLive do
|
||||
use WandererAppWeb, :live_view
|
||||
|
||||
require Logger
|
||||
|
||||
@impl true
|
||||
def mount(%{"eve_id" => eve_id_str}, _session, socket) do
|
||||
case Integer.parse(eve_id_str) do
|
||||
{eve_id, ""} ->
|
||||
case load_character(eve_id) do
|
||||
{:ok, character} ->
|
||||
is_owner = owner?(socket.assigns.current_user, eve_id_str)
|
||||
description_html = render_description(character.description)
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
page_title: character.name,
|
||||
profile: build_profile(character),
|
||||
is_owner: is_owner,
|
||||
editing: false,
|
||||
description_html: description_html,
|
||||
description_raw: character.description || ""
|
||||
)}
|
||||
|
||||
{:error, _} ->
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "Character not found")
|
||||
|> redirect(to: "/")}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:ok,
|
||||
socket
|
||||
|> put_flash(:error, "Invalid character ID")
|
||||
|> redirect(to: "/")}
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event("toggle_edit", _params, socket) do
|
||||
if socket.assigns.is_owner do
|
||||
{:noreply, assign(socket, editing: !socket.assigns.editing)}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("save_description", _params, socket) do
|
||||
if socket.assigns.is_owner do
|
||||
{:noreply, push_event(socket, "request_editor_content", %{})}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_event("cancel_edit", _params, socket) do
|
||||
{:noreply, assign(socket, editing: false)}
|
||||
end
|
||||
|
||||
def handle_event("content-text-change", _params, socket) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_event("editor_content_markdown", %{"markdown" => markdown}, socket) do
|
||||
if socket.assigns.is_owner do
|
||||
markdown = String.slice(markdown, 0, 10_000)
|
||||
eve_id_str = socket.assigns.profile.eve_id
|
||||
|
||||
case WandererApp.Api.Character.by_eve_id(eve_id_str) do
|
||||
{:ok, character} ->
|
||||
case WandererApp.Api.Character.update_description(character, %{description: markdown}) do
|
||||
{:ok, _updated} ->
|
||||
Cachex.del(:api_cache, "character_profile_#{eve_id_str}")
|
||||
description_html = render_description(markdown)
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(
|
||||
editing: false,
|
||||
description_html: description_html,
|
||||
description_raw: markdown
|
||||
)
|
||||
|> put_flash(:info, "Description updated")}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Failed to update description: #{inspect(reason)}")
|
||||
{:noreply, put_flash(socket, :error, "Failed to save description")}
|
||||
end
|
||||
|
||||
{:error, _} ->
|
||||
{:noreply, put_flash(socket, :error, "Character not found")}
|
||||
end
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
defp owner?(current_user, eve_id_str) do
|
||||
case current_user do
|
||||
%{characters: characters} when is_list(characters) ->
|
||||
Enum.any?(characters, fn c -> to_string(c.eve_id) == eve_id_str end)
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp load_character(eve_id) do
|
||||
WandererApp.Api.Character.by_eve_id(eve_id)
|
||||
end
|
||||
|
||||
defp build_profile(character) do
|
||||
%{
|
||||
eve_id: character.eve_id,
|
||||
name: character.name,
|
||||
corporation_id: character.corporation_id,
|
||||
corporation_name: character.corporation_name,
|
||||
corporation_ticker: character.corporation_ticker,
|
||||
alliance_id: character.alliance_id,
|
||||
alliance_name: character.alliance_name,
|
||||
alliance_ticker: character.alliance_ticker,
|
||||
online: character.online
|
||||
}
|
||||
end
|
||||
|
||||
defp render_description(nil), do: ""
|
||||
defp render_description(""), do: ""
|
||||
|
||||
defp render_description(markdown) do
|
||||
case Earmark.as_html(markdown) do
|
||||
{:ok, html, _} ->
|
||||
HtmlSanitizeEx.markdown_html(html)
|
||||
|
||||
{:error, _, _} ->
|
||||
""
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,133 @@
|
||||
<main class="w-full h-full p-4 pl-20 pb-20 overflow-auto">
|
||||
<article class="ccp-font w-full max-w-2xl mx-auto">
|
||||
<div class="bg-neutral-900/60 text-stone-200 [text-shadow:0_0px_8px_rgba(0,0,0,0.7)] px-6 py-5 mt-8 rounded-lg">
|
||||
<div class="flex flex-row items-center gap-4">
|
||||
<img
|
||||
src={"https://images.evetech.net/characters/#{@profile.eve_id}/portrait?size=256"}
|
||||
class="w-20 h-20 rounded-lg flex-shrink-0"
|
||||
alt={@profile.name}
|
||||
/>
|
||||
<div class="flex flex-col gap-1 min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="text-xl font-bold text-white m-0 truncate">{@profile.name}</h2>
|
||||
<span
|
||||
:if={@profile.online}
|
||||
class="w-2 h-2 rounded-full bg-green-500 flex-shrink-0"
|
||||
title="Online"
|
||||
/>
|
||||
<span
|
||||
:if={!@profile.online}
|
||||
class="w-2 h-2 rounded-full bg-gray-500 flex-shrink-0"
|
||||
title="Offline"
|
||||
/>
|
||||
</div>
|
||||
<div :if={@profile.corporation_name} class="flex items-center gap-2 text-sm">
|
||||
<img
|
||||
src={"https://images.evetech.net/corporations/#{@profile.corporation_id}/logo?size=64"}
|
||||
class="w-5 h-5 rounded"
|
||||
alt={@profile.corporation_name}
|
||||
/>
|
||||
<span class="text-stone-300 truncate">
|
||||
<span :if={@profile.corporation_ticker} class="text-gray-500">
|
||||
[{@profile.corporation_ticker}]
|
||||
</span>
|
||||
{@profile.corporation_name}
|
||||
</span>
|
||||
</div>
|
||||
<div :if={@profile.alliance_name} class="flex items-center gap-2 text-sm">
|
||||
<img
|
||||
src={"https://images.evetech.net/alliances/#{@profile.alliance_id}/logo?size=64"}
|
||||
class="w-5 h-5 rounded"
|
||||
alt={@profile.alliance_name}
|
||||
/>
|
||||
<span class="text-stone-300 truncate">
|
||||
<span :if={@profile.alliance_ticker} class="text-gray-500">
|
||||
[{@profile.alliance_ticker}]
|
||||
</span>
|
||||
{@profile.alliance_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 ml-auto flex-shrink-0">
|
||||
<a
|
||||
href={"https://zkillboard.com/character/#{@profile.eve_id}/"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<.button
|
||||
type="button"
|
||||
class="p-button p-component p-button-primary w-full"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
<span class="p-button-label">zKill</span>
|
||||
</.button>
|
||||
</a>
|
||||
<a
|
||||
href={"https://evewho.com/character/#{@profile.eve_id}"}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<.button
|
||||
type="button"
|
||||
class="p-button p-component p-button-primary w-full"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
<span class="p-button-label">EVE Who</span>
|
||||
</.button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- About / Description section --%>
|
||||
<div class="mt-4 border-t border-stone-700/50 pt-4">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<h3 class="text-sm font-semibold text-stone-400 m-0 uppercase tracking-wide">About</h3>
|
||||
<button
|
||||
:if={@is_owner && !@editing}
|
||||
phx-click="toggle_edit"
|
||||
class="h-8 w-8 hover:text-white"
|
||||
type="button"
|
||||
>
|
||||
<.icon name="hero-pencil-square-solid" class="w-6 h-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<%!-- View mode --%>
|
||||
<div :if={!@editing}>
|
||||
<%= if @description_html != "" do %>
|
||||
<div class="prose prose-invert prose-sm max-w-none text-stone-300">
|
||||
{raw(@description_html)}
|
||||
</div>
|
||||
<% else %>
|
||||
<p class="text-stone-500 italic text-sm m-0">No description yet.</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%!-- Edit mode --%>
|
||||
<div :if={@editing && @is_owner}>
|
||||
<div id="wysiwyg-editor" phx-hook="WysiwygEditor" phx-update="ignore">
|
||||
<div class="ql-editor-container" data-initial-content={@description_html}></div>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-3">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="save_description"
|
||||
class="p-button p-component p-button-primary w-full"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
<span class="p-button-label">Save</span>
|
||||
</.button>
|
||||
<.button
|
||||
type="button"
|
||||
phx-click="cancel_edit"
|
||||
class="p-button p-component p-button-primary w-full"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
<span class="p-button-label">Cancel</span>
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</main>
|
||||
@@ -22,6 +22,11 @@ defmodule WandererAppWeb.CharactersLive do
|
||||
"character:#{character_id}:corporation"
|
||||
)
|
||||
|
||||
Phoenix.PubSub.subscribe(
|
||||
WandererApp.PubSub,
|
||||
"character:#{character_id}"
|
||||
)
|
||||
|
||||
:ok = WandererApp.Character.TrackerManager.start_tracking(character_id)
|
||||
end)
|
||||
|
||||
@@ -83,12 +88,11 @@ defmodule WandererAppWeb.CharactersLive do
|
||||
{:ok, _} = WandererApp.MapCharacterSettingsRepo.untrack(settings)
|
||||
end)
|
||||
|
||||
{:ok, updated_character} =
|
||||
socket.assigns.characters
|
||||
|> Enum.find(&(&1.id == character_id))
|
||||
|> WandererApp.Api.Character.mark_as_deleted()
|
||||
# Load character from DB instead of using plain map from assigns
|
||||
{:ok, character} = WandererApp.Api.Character.by_id(character_id)
|
||||
{:ok, _updated_character} = WandererApp.Api.Character.mark_as_deleted(character)
|
||||
|
||||
WandererApp.Character.update_character(character_id, updated_character)
|
||||
WandererApp.Character.update_character(character_id, %{deleted: true, user_id: nil})
|
||||
|
||||
{:ok, characters} =
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: socket.assigns.user_id})
|
||||
@@ -148,6 +152,18 @@ defmodule WandererAppWeb.CharactersLive do
|
||||
{:noreply, socket |> assign(characters: characters |> Enum.map(&map_ui_character/1))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
event,
|
||||
socket
|
||||
)
|
||||
when event in [:character_token_invalid, :token_updated] do
|
||||
{:ok, characters} =
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: socket.assigns.user_id})
|
||||
|
||||
{:noreply, socket |> assign(characters: characters |> Enum.map(&map_ui_character/1))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(
|
||||
_event,
|
||||
|
||||
@@ -197,6 +197,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions justify-end">
|
||||
<.link
|
||||
navigate={~p"/characters/#{character.eve_id}"}
|
||||
class="tooltip tooltip-bottom"
|
||||
data-tip="View Profile"
|
||||
>
|
||||
<.icon name="hero-user-solid" class="w-4 h-4 hover:text-white" />
|
||||
</.link>
|
||||
<.link
|
||||
patch={~p"/characters/authorize"}
|
||||
class="tooltip tooltip-bottom"
|
||||
@@ -236,7 +243,9 @@
|
||||
</figure>
|
||||
</:col>
|
||||
<:col :let={character} label="Name">
|
||||
{character.name}
|
||||
<.link navigate={~p"/characters/#{character.eve_id}"} class="hover:text-white underline">
|
||||
{character.name}
|
||||
</.link>
|
||||
</:col>
|
||||
<:col :let={character} label="Corporation">
|
||||
{character
|
||||
|
||||
@@ -54,7 +54,7 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
selected_map_slug: map_slug
|
||||
)
|
||||
|> assign_async(:characters, fn ->
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
load_trackable_characters(selected_map, current_user.id)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -100,37 +100,55 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
selected_map = socket.assigns.selected_map
|
||||
%{result: characters} = socket.assigns.characters
|
||||
|
||||
case characters |> Enum.find(&(&1.id == character_id)) do
|
||||
%{tracked: current_tracked, eve_id: eve_id} ->
|
||||
# Use TrackingUtils.update_tracking to properly set/unset the tracking_start_time
|
||||
# cache key, which is required for the character to appear in get_tracked_character_ids
|
||||
case TrackingUtils.update_tracking(
|
||||
selected_map.id,
|
||||
eve_id,
|
||||
current_user.id,
|
||||
not current_tracked,
|
||||
self(),
|
||||
false
|
||||
) do
|
||||
{:ok, _tracking_data, _event} ->
|
||||
:ok
|
||||
result =
|
||||
case characters |> Enum.find(&(&1.id == character_id)) do
|
||||
%{tracked: current_tracked, eve_id: eve_id} ->
|
||||
# Use TrackingUtils.update_tracking to properly set/unset the tracking_start_time
|
||||
# cache key, which is required for the character to appear in get_tracked_character_ids
|
||||
case TrackingUtils.update_tracking(
|
||||
selected_map.id,
|
||||
eve_id,
|
||||
current_user.id,
|
||||
not current_tracked,
|
||||
self(),
|
||||
false
|
||||
) do
|
||||
{:ok, _tracking_data, _event} ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error(
|
||||
"Failed to toggle tracking for character #{character_id} on map #{selected_map.id}: #{inspect(reason)}"
|
||||
)
|
||||
end
|
||||
{:error, reason} ->
|
||||
Logger.error(
|
||||
"Failed to toggle tracking for character #{character_id} on map #{selected_map.id}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
nil ->
|
||||
Logger.warning(
|
||||
"Character #{character_id} not found in available characters for map #{selected_map.id}"
|
||||
)
|
||||
end
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
nil ->
|
||||
Logger.warning(
|
||||
"Character #{character_id} not found in available characters for map #{selected_map.id}"
|
||||
)
|
||||
|
||||
{:error, "Character not found"}
|
||||
end
|
||||
|
||||
socket =
|
||||
case result do
|
||||
{:error, _reason} ->
|
||||
put_flash(
|
||||
socket,
|
||||
:error,
|
||||
"Failed to toggle tracking. Character may not have sufficient permissions on this map."
|
||||
)
|
||||
|
||||
_ ->
|
||||
socket
|
||||
end
|
||||
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_async(:characters, fn ->
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
load_trackable_characters(selected_map, current_user.id)
|
||||
end)}
|
||||
end
|
||||
|
||||
@@ -154,10 +172,21 @@ defmodule WandererAppWeb.CharactersTrackingLive do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign_async(:characters, fn ->
|
||||
WandererApp.Maps.load_characters(selected_map, current_user.id)
|
||||
load_trackable_characters(selected_map, current_user.id)
|
||||
end)}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(_event, socket), do: {:noreply, socket}
|
||||
|
||||
defp load_trackable_characters(map, user_id) do
|
||||
case WandererApp.Maps.load_characters(map, user_id) do
|
||||
{:ok, %{characters: characters}} ->
|
||||
filtered = TrackingUtils.filter_characters_with_tracking_permission(characters, map)
|
||||
{:ok, %{characters: filtered}}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -385,14 +385,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_tracking_event(:invalid_token_message, socket, _map_id) do
|
||||
socket
|
||||
|> put_flash(
|
||||
:error,
|
||||
"One of your characters has expired token. Please refresh it on characters page."
|
||||
)
|
||||
end
|
||||
|
||||
defp handle_tracking_event(:map_character_limit, socket, _map_id) do
|
||||
socket
|
||||
|> put_flash(
|
||||
|
||||
@@ -553,23 +553,14 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
{:ok, map_character_ids} <-
|
||||
WandererApp.Cache.lookup("map_#{map_id}:presence_character_ids", []) do
|
||||
events =
|
||||
case tracked_characters |> Enum.any?(&(&1.access_token == nil)) do
|
||||
case track_character && not has_tracked_characters? do
|
||||
true ->
|
||||
[:invalid_token_message]
|
||||
[:empty_tracked_characters]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
|
||||
events =
|
||||
case track_character && not has_tracked_characters? do
|
||||
true ->
|
||||
events ++ [:empty_tracked_characters]
|
||||
|
||||
_ ->
|
||||
events
|
||||
end
|
||||
|
||||
character_limit_reached? = map_character_ids |> Enum.count() >= characters_limit
|
||||
|
||||
events =
|
||||
@@ -623,6 +614,9 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
nil
|
||||
end
|
||||
|
||||
expired_characters =
|
||||
tracked_characters |> Enum.filter(&(&1.access_token == nil)) |> Enum.map(& &1.eve_id)
|
||||
|
||||
initial_data =
|
||||
%{
|
||||
kills: kills_data,
|
||||
@@ -630,6 +624,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|
||||
map_character_ids
|
||||
|> WandererApp.Character.get_character_eve_ids!(),
|
||||
user_characters: tracked_characters |> Enum.map(& &1.eve_id),
|
||||
expired_characters: expired_characters,
|
||||
system_static_infos: nil,
|
||||
wormholes: nil,
|
||||
effects: nil,
|
||||
|
||||
@@ -176,12 +176,14 @@ defmodule WandererAppWeb.MapRoutesEventHandler do
|
||||
routes_type = Map.get(event, "type", "blueLoot")
|
||||
security_type = Map.get(event, "securityType", "both")
|
||||
is_subscription_active? = Map.get(socket.assigns, :is_subscription_active?, false)
|
||||
|
||||
routes_limit =
|
||||
if is_subscription_active? == true do
|
||||
@paid_routes_limit
|
||||
else
|
||||
Map.get(@alpha_routes_limit_by_type, routes_type, @default_alpha_routes_limit)
|
||||
end
|
||||
|
||||
routes_settings =
|
||||
routes_settings
|
||||
|> get_routes_settings()
|
||||
|
||||
@@ -296,22 +296,23 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
})
|
||||
end
|
||||
|
||||
signature_time_status =
|
||||
{signature_time_status, signature_mass_status} =
|
||||
if not is_nil(signature.custom_info) do
|
||||
signature.custom_info |> Jason.decode!() |> Map.get("time_status")
|
||||
decoded = signature.custom_info |> Jason.decode!()
|
||||
{Map.get(decoded, "time_status"), Map.get(decoded, "mass_status")}
|
||||
else
|
||||
nil
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
signature_ship_size_type = EVEUtil.get_wh_size(signature.type)
|
||||
|
||||
# Back-link detection: if current signature yields no ship_size_type (e.g., K162),
|
||||
# look for a forward signature in the target system that links back to our source
|
||||
{signature_time_status, signature_ship_size_type} =
|
||||
{signature_time_status, signature_ship_size_type, signature_mass_status} =
|
||||
if is_nil(signature_ship_size_type) do
|
||||
case SignaturesImpl.find_forward_signature(target_system.id, solar_system_source) do
|
||||
nil ->
|
||||
{signature_time_status, signature_ship_size_type}
|
||||
{signature_time_status, signature_ship_size_type, signature_mass_status}
|
||||
|
||||
forward_sig ->
|
||||
Logger.info(
|
||||
@@ -321,17 +322,29 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
|
||||
forward_ship_size = EVEUtil.get_wh_size(forward_sig.type)
|
||||
|
||||
forward_time_status =
|
||||
if is_nil(signature_time_status) and not is_nil(forward_sig.custom_info) do
|
||||
forward_sig.custom_info |> Jason.decode!() |> Map.get("time_status")
|
||||
{forward_time_status, forward_mass_status} =
|
||||
if not is_nil(forward_sig.custom_info) do
|
||||
decoded = forward_sig.custom_info |> Jason.decode!()
|
||||
|
||||
fwd_time =
|
||||
if is_nil(signature_time_status),
|
||||
do: Map.get(decoded, "time_status"),
|
||||
else: signature_time_status
|
||||
|
||||
fwd_mass =
|
||||
if is_nil(signature_mass_status),
|
||||
do: Map.get(decoded, "mass_status"),
|
||||
else: signature_mass_status
|
||||
|
||||
{fwd_time, fwd_mass}
|
||||
else
|
||||
signature_time_status
|
||||
{signature_time_status, signature_mass_status}
|
||||
end
|
||||
|
||||
{forward_time_status, forward_ship_size}
|
||||
{forward_time_status, forward_ship_size, forward_mass_status}
|
||||
end
|
||||
else
|
||||
{signature_time_status, signature_ship_size_type}
|
||||
{signature_time_status, signature_ship_size_type, signature_mass_status}
|
||||
end
|
||||
|
||||
if not is_nil(signature_time_status) do
|
||||
@@ -351,6 +364,15 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
ship_size_type: signature_ship_size_type
|
||||
})
|
||||
end
|
||||
|
||||
if not is_nil(signature_mass_status) do
|
||||
map_id
|
||||
|> WandererApp.Map.Server.update_connection_mass_status(%{
|
||||
solar_system_source_id: solar_system_source,
|
||||
solar_system_target_id: solar_system_target,
|
||||
mass_status: signature_mass_status
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :signatures_updated, solar_system_source)
|
||||
|
||||
@@ -128,6 +128,33 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"manual_add_system",
|
||||
%{"solar_system_id" => solar_system_id} = _event,
|
||||
%{
|
||||
assigns: %{
|
||||
current_user: %{id: current_user_id},
|
||||
has_tracked_characters?: true,
|
||||
map_id: map_id,
|
||||
main_character_id: main_character_id,
|
||||
user_permissions: %{add_system: true}
|
||||
}
|
||||
} =
|
||||
socket
|
||||
)
|
||||
when not is_nil(main_character_id) do
|
||||
WandererApp.Map.Server.add_system(
|
||||
map_id,
|
||||
%{
|
||||
solar_system_id: solar_system_id
|
||||
},
|
||||
current_user_id,
|
||||
main_character_id
|
||||
)
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"manual_paste_systems_and_connections",
|
||||
%{
|
||||
|
||||
@@ -55,16 +55,6 @@ defmodule WandererAppWeb.MapLive do
|
||||
{:noreply, socket |> push_navigate(to: ~p"/#{map_slug}")}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:character_token_invalid, socket),
|
||||
do:
|
||||
{:noreply,
|
||||
socket
|
||||
|> put_flash(
|
||||
:error,
|
||||
"One of your characters has expired token. Please refresh it on characters page."
|
||||
)}
|
||||
|
||||
def handle_info(:no_main_character_set, socket),
|
||||
do:
|
||||
{:noreply,
|
||||
|
||||
@@ -6,6 +6,7 @@ defmodule WandererAppWeb.Nav do
|
||||
|
||||
alias WandererAppWeb.{
|
||||
AccessListsLive,
|
||||
CharacterProfileLive,
|
||||
MapLive,
|
||||
MapsLive,
|
||||
CharactersLive,
|
||||
@@ -64,6 +65,9 @@ defmodule WandererAppWeb.Nav do
|
||||
{CharactersLive, _} ->
|
||||
:characters
|
||||
|
||||
{CharacterProfileLive, _} ->
|
||||
:characters
|
||||
|
||||
{CharactersTrackingLive, _} ->
|
||||
:characters_tracking
|
||||
|
||||
|
||||
93
lib/wanderer_app_web/live/sponsors/sponsors_live.ex
Normal file
93
lib/wanderer_app_web/live/sponsors/sponsors_live.ex
Normal file
@@ -0,0 +1,93 @@
|
||||
defmodule WandererAppWeb.SponsorsLive do
|
||||
use WandererAppWeb, :live_view
|
||||
|
||||
alias BetterNumber, as: Number
|
||||
|
||||
require Logger
|
||||
|
||||
@cache_key "server_top_donators"
|
||||
@cache_ttl :timer.minutes(15)
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
if not WandererApp.Env.map_subscriptions_enabled?() do
|
||||
{:ok, socket |> redirect(to: "/")}
|
||||
else
|
||||
top_donators = load_top_donators()
|
||||
{corporation_id, corporation_info} = load_corporation_info()
|
||||
|
||||
{:ok,
|
||||
assign(socket,
|
||||
page_title: "Sponsors",
|
||||
top_donators: top_donators,
|
||||
corporation_id: corporation_id,
|
||||
corporation_info: corporation_info
|
||||
)}
|
||||
end
|
||||
end
|
||||
|
||||
def format_isk(amount) do
|
||||
Number.to_human(amount, units: ["", "K", "M", "B", "T", "P"])
|
||||
end
|
||||
|
||||
defp load_top_donators do
|
||||
case Cachex.get(:api_cache, @cache_key) do
|
||||
{:ok, nil} ->
|
||||
donators = fetch_and_enrich()
|
||||
Cachex.put(:api_cache, @cache_key, donators, ttl: @cache_ttl)
|
||||
donators
|
||||
|
||||
{:ok, cached} ->
|
||||
cached
|
||||
|
||||
_ ->
|
||||
fetch_and_enrich()
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_and_enrich do
|
||||
after_date = DateTime.utc_now() |> DateTime.add(-30, :day)
|
||||
|
||||
case WandererApp.Api.MapTransaction.server_top_donators(%{after: after_date}) do
|
||||
{:ok, donators} ->
|
||||
enrich_with_characters(donators)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Failed to load server top donators: #{inspect(reason)}")
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp enrich_with_characters(donators) do
|
||||
donators
|
||||
|> Enum.map(fn %{user_id: user_id, total_amount: total_amount} ->
|
||||
case WandererApp.Api.Character.active_by_user(%{user_id: user_id}) do
|
||||
{:ok, [character | _]} ->
|
||||
%{
|
||||
character_name: character.name,
|
||||
eve_id: character.eve_id,
|
||||
corporation_name: character.corporation_name,
|
||||
corporation_ticker: character.corporation_ticker,
|
||||
total_amount: total_amount
|
||||
}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp load_corporation_info do
|
||||
corp_eve_id = WandererApp.Env.corp_eve_id()
|
||||
|
||||
if corp_eve_id == -1 do
|
||||
{nil, nil}
|
||||
else
|
||||
case WandererApp.Esi.get_corporation_info(corp_eve_id) do
|
||||
{:ok, info} -> {corp_eve_id, info}
|
||||
_ -> {nil, nil}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
119
lib/wanderer_app_web/live/sponsors/sponsors_live.html.heex
Normal file
119
lib/wanderer_app_web/live/sponsors/sponsors_live.html.heex
Normal file
@@ -0,0 +1,119 @@
|
||||
<main class="w-full h-full p-4 pl-20 pb-20 overflow-auto">
|
||||
<article class="ccp-font w-full max-w-2xl mx-auto">
|
||||
<h1 class="font-bold text-lg ccp-font text-white mb-1">
|
||||
Our Sponsors
|
||||
</h1>
|
||||
<p class="text-stone-400 text-sm mb-4">
|
||||
Top 10 ISK donators across all maps in the last 30 days.
|
||||
</p>
|
||||
|
||||
<div class="bg-neutral-900/60 text-stone-200 px-6 py-5 rounded-lg">
|
||||
<div :if={@top_donators == []} class="text-center text-gray-400 py-8">
|
||||
No donations found for this period.
|
||||
</div>
|
||||
|
||||
<div :if={@top_donators != []} class="space-y-2">
|
||||
<div
|
||||
:for={{donator, index} <- Enum.with_index(@top_donators)}
|
||||
class="flex flex-row items-center gap-4 p-3 rounded-lg bg-base-200/50"
|
||||
>
|
||||
<span class="text-lg font-bold text-gray-500 w-6 text-right flex-shrink-0">
|
||||
{index + 1}
|
||||
</span>
|
||||
<img
|
||||
src={"https://images.evetech.net/characters/#{donator.eve_id}/portrait?size=64"}
|
||||
class="w-10 h-10 rounded-lg flex-shrink-0"
|
||||
alt={donator.character_name}
|
||||
/>
|
||||
<div class="flex flex-col gap-0.5 min-w-0">
|
||||
<.link
|
||||
navigate={~p"/characters/#{donator.eve_id}"}
|
||||
class="text-sm font-bold text-white truncate hover:text-blue-400 transition-colors"
|
||||
>
|
||||
{donator.character_name}
|
||||
</.link>
|
||||
<div :if={donator.corporation_name} class="flex items-center gap-1.5 text-xs">
|
||||
<span class="text-stone-300 truncate">
|
||||
<span class="text-gray-500">[{donator.corporation_ticker}]</span>
|
||||
{donator.corporation_name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto text-right font-mono text-sm text-green-400 flex-shrink-0">
|
||||
ISK {WandererAppWeb.SponsorsLive.format_isk(donator.total_amount)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mt-6">
|
||||
<.button
|
||||
type="button"
|
||||
phx-click={show_modal("donate-modal")}
|
||||
class="p-button p-component p-button-primary w-full"
|
||||
style="min-width: 0;"
|
||||
>
|
||||
<span class="p-button-label"> Become a Sponsor</span>
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<.modal id="donate-modal" title="How to become a sponsor?" class="!w-[700px]">
|
||||
<div :if={is_nil(@corporation_info)} class="w-full max-h-[80vh] overflow-y-auto mx-auto">
|
||||
It's not available yet :(
|
||||
</div>
|
||||
<div :if={@corporation_info} class="w-full max-h-[80vh] overflow-y-auto mx-auto">
|
||||
<div class="mx-auto p-4 rounded-lg shadow-md">
|
||||
<div
|
||||
:if={@corporation_info}
|
||||
class="w-full flex flex-row items-center justify-between gap-2 p-4 bg-stone-950 bg-opacity-70 rounded-lg"
|
||||
>
|
||||
Wanderer EVE Corporation:
|
||||
<div class="flex flex-row items-center justify-between gap-2 p-4 bg-stone-950 bg-opacity-70 rounded-lg">
|
||||
<div class="avatar">
|
||||
<div class="rounded-md w-12 h-12">
|
||||
<img
|
||||
src={"https://images.evetech.net/corporations/#{@corporation_id}/logo?size=32"}
|
||||
alt={@corporation_info["name"]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span> {@corporation_info["name"]}</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="mt-2 text-2xl font-semibold mb-4 text-white-800">
|
||||
How to Donate ISK to Wanderer in Eve Online
|
||||
</h2>
|
||||
<ol class="list-decimal list-inside mb-4">
|
||||
<li class="mb-2">
|
||||
<strong>Open corporations overview:</strong>
|
||||
Click on the 'Social' and then on 'Corporation' in the Neocom menu to access corporations search.
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong>Search for a Corporation:</strong>
|
||||
Type in the search bar the name: <b>{@corporation_info["name"]}</b>.
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong>Choose 'Give Money':</strong>
|
||||
Select the 'Give Money' in the context menu to initiate the transfer.
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong>Specify the Amount:</strong>
|
||||
Input the amount of ISK you wish to transfer to the corporate account.
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong>Add a Reason (Optional):</strong>
|
||||
Include a short note or reason for the transfer if desired.
|
||||
</li>
|
||||
<li class="mb-2">
|
||||
<strong>Confirm the Transfer:</strong>
|
||||
Double-check the recipient's name and the amount, then click 'OK' to complete the transaction.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
The ISK will be transferred instantly to the Wanderer's account. Ensure you enter the correct recipient name to avoid any errors. Fly safe and enjoy your time in Eve Online!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</.modal>
|
||||
</main>
|
||||
@@ -7,7 +7,7 @@ defmodule WandererAppWeb.Router do
|
||||
|
||||
import WandererAppWeb.UserAuth,
|
||||
warn: false,
|
||||
only: [redirect_if_user_is_authenticated: 2]
|
||||
only: [redirect_if_user_is_authenticated: 2, require_authenticated_user: 2]
|
||||
|
||||
import WandererAppWeb.BasicAuth,
|
||||
warn: false,
|
||||
@@ -164,6 +164,10 @@ defmodule WandererAppWeb.Router do
|
||||
plug :put_layout, html: {WandererAppWeb.Layouts, :blog}
|
||||
end
|
||||
|
||||
pipeline :require_auth do
|
||||
plug :require_authenticated_user
|
||||
end
|
||||
|
||||
pipeline :api do
|
||||
plug WandererAppWeb.Plugs.ContentNegotiation, accepts: ["json"]
|
||||
plug :accepts, ["json"]
|
||||
@@ -417,6 +421,8 @@ defmodule WandererAppWeb.Router do
|
||||
get "/", BlogController, :license
|
||||
end
|
||||
|
||||
|
||||
|
||||
scope "/swaggerui" do
|
||||
pipe_through [:browser, :api_spec]
|
||||
|
||||
@@ -549,6 +555,8 @@ defmodule WandererAppWeb.Router do
|
||||
live "/tracking", CharactersTrackingLive, :index
|
||||
live "/characters", CharactersLive, :index
|
||||
live "/characters/authorize", CharactersLive, :authorize
|
||||
live "/characters/:eve_id", CharacterProfileLive, :show
|
||||
live "/sponsors", SponsorsLive, :index
|
||||
live "/maps/new", MapsLive, :create
|
||||
live "/maps/:slug/edit", MapsLive, :edit
|
||||
live "/maps/:slug/settings", MapsLive, :settings
|
||||
|
||||
4
mix.exs
4
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.94.0"
|
||||
@version "1.98.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
@@ -134,6 +134,8 @@ defmodule WandererApp.MixProject do
|
||||
{:live_view_events, "~> 0.1.0"},
|
||||
{:ash_pagify, "~> 1.4.1"},
|
||||
{:timex, "~> 3.0"},
|
||||
{:earmark, "~> 1.4"},
|
||||
{:html_sanitize_ex, "~> 1.4"},
|
||||
# Test coverage and quality
|
||||
{:excoveralls, "~> 0.18", only: :test}
|
||||
]
|
||||
|
||||
2
mix.lock
2
mix.lock
@@ -57,6 +57,7 @@
|
||||
"hackney": {:hex, :hackney, "1.25.0", "390e9b83f31e5b325b9f43b76e1a785cbdb69b5b6cd4e079aa67835ded046867", [:rebar3], [{:certifi, "~> 2.15.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.4", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "7209bfd75fd1f42467211ff8f59ea74d6f2a9e81cbcee95a56711ee79fd6b1d4"},
|
||||
"heroicons": {:hex, :heroicons, "0.5.5", "c2bcb05a90f010df246a5a2a2b54cac15483b5de137b2ef0bead77fcdf06e21a", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:phoenix_live_view, ">= 0.18.2", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}], "hexpm", "2f4bf929440fecd5191ba9f40e5009b0f75dc993d765c0e4d068fcb7026d6da1"},
|
||||
"hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"},
|
||||
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.5.0", "ea13a4a92ba0fa17bc6199f1bb7b755a8595ec3b5f763330ea8570d8b5f648e4", [:mix], [{:mochiweb, "~> 2.15 or ~> 3.1", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm", "4eaa2205ae56fab95d0f25065d709b05f0cba730f3fcec184dfde594acdd4578"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"igniter": {:hex, :igniter, "0.7.0", "6848714fa5afa14258c82924a57af9364745316241a409435cf39cbe11e3ae80", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "1e7254780dbf4b44c9eccd6d86d47aa961efc298d7f520c24acb0258c8e90ba9"},
|
||||
"inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
|
||||
@@ -78,6 +79,7 @@
|
||||
"mint": {:hex, :mint, "1.7.1", "113fdb2b2f3b59e47c7955971854641c61f378549d73e829e1768de90fc1abf1", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "fceba0a4d0f24301ddee3024ae116df1c3f4bb7a563a731f45fdfeb9d39a231b"},
|
||||
"mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"},
|
||||
"mix_audit": {:hex, :mix_audit, "2.1.3", "c70983d5cab5dca923f9a6efe559abfb4ec3f8e87762f02bab00fa4106d17eda", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.9", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "8c3987100b23099aea2f2df0af4d296701efd031affb08d0746b2be9e35988ec"},
|
||||
"mochiweb": {:hex, :mochiweb, "3.3.0", "2898ad0bfeee234e4cbae623c7052abc3ff0d73d499ba6e6ffef445b13ffd07a", [:rebar3], [], "hexpm", "aa85b777fb23e9972ebc424e40b5d35106f19bc998873e026dedd876df8ee50c"},
|
||||
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
|
||||
"nebulex": {:hex, :nebulex, "2.6.2", "0874989db4e382362884662d2ee9f31b4c4862595f4ec300bd279068729dd2d0", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.1", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "002a1774d5a187eb631ae4006db13df4bb6b325fe2a3c14cb14a1f3e989042b4"},
|
||||
"nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"},
|
||||
|
||||
@@ -3,7 +3,7 @@ title: "Event: Wanderer 2026 Roadmap Reveal",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/2026/01-01-roadmap/cover.webp",
|
||||
tags: ~w(event roadmap 2026 announcement community),
|
||||
description: "JWanderer's 2026 roadmap are ready to reveal! Discover what exciting features and improvements are coming in 2026."
|
||||
description: "Wanderer's 2026 roadmap are ready to reveal! Discover what exciting features and improvements are coming in 2026."
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
%{
|
||||
title: "Event: Weekly Giveaway Challenge",
|
||||
author: "Wanderer Team",
|
||||
cover_image_uri: "/images/news/2026/01-05-weekly-giveaway/cover.webp",
|
||||
tags: ~w(event giveaway challenge),
|
||||
description: "Join our Weekly Giveaway Challenge! Be the fastest to claim your reward!"
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
|
||||

|
||||
|
||||
### Event Details
|
||||
|
||||
In 2026, we're going to giveaway partnership SKIN codes for our community, every week!
|
||||
|
||||
- **Event Name:** Weekly Giveaway Challenge
|
||||
- **Event Link:** [Join Weekly Giveaway Challenge](https://eventcortex.com/events/invite/Cjo87svZFq6J8cc1cubH4B7AR_VfPmQ4)
|
||||
|
||||
---
|
||||
|
||||
### Tips for Participants
|
||||
|
||||
- **Be Ready:** Know the reveal time and be online a few minutes early.
|
||||
|
||||
---
|
||||
|
||||
Good luck, and may the fastest capsuleer win!
|
||||
|
||||
---
|
||||
|
||||
Fly safe,
|
||||
**Wanderer Team**
|
||||
|
||||
---
|
||||
@@ -291,7 +291,7 @@
|
||||
"src": ["c2", "c3", "c4", "c5", "c6"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"lifetime": "12",
|
||||
"total_mass": 1000000000,
|
||||
"name": "F216",
|
||||
"respawn": ["wandering", "reverse"]
|
||||
@@ -698,7 +698,7 @@
|
||||
"src": ["pochven"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"lifetime": "12",
|
||||
"total_mass": 1000000000,
|
||||
"name": "R081",
|
||||
"respawn": ["wandering"]
|
||||
@@ -830,7 +830,7 @@
|
||||
"src": ["ns"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 375000000,
|
||||
"lifetime": "16",
|
||||
"lifetime": "12",
|
||||
"total_mass": 1000000000,
|
||||
"name": "U372",
|
||||
"respawn": ["wandering", "reverse"]
|
||||
@@ -929,7 +929,7 @@
|
||||
"src": ["pochven"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 62000000,
|
||||
"lifetime": "16",
|
||||
"lifetime": "12",
|
||||
"total_mass": 1000000000,
|
||||
"name": "X450",
|
||||
"respawn": ["wandering"]
|
||||
@@ -1065,5 +1065,38 @@
|
||||
"total_mass": "",
|
||||
"name": "K162",
|
||||
"respawn": []
|
||||
},
|
||||
{
|
||||
"mass_regen": 0,
|
||||
"dest": "pochven",
|
||||
"src": ["pochven"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 62000000,
|
||||
"lifetime": "4.5",
|
||||
"total_mass": 100000000,
|
||||
"name": "I078",
|
||||
"respawn": ["wandering"]
|
||||
},
|
||||
{
|
||||
"mass_regen": 0,
|
||||
"dest": "pochven",
|
||||
"src": ["pochven"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 62000000,
|
||||
"lifetime": "4.5",
|
||||
"total_mass": 100000000,
|
||||
"name": "L687",
|
||||
"respawn": ["wandering"]
|
||||
},
|
||||
{
|
||||
"mass_regen": 0,
|
||||
"dest": "pochven",
|
||||
"src": ["pochven"],
|
||||
"static": false,
|
||||
"max_mass_per_jump": 62000000,
|
||||
"lifetime": "4.5",
|
||||
"total_mass": 100000000,
|
||||
"name": "O546",
|
||||
"respawn": ["wandering"]
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddCharacterDescription do
|
||||
@moduledoc """
|
||||
Updates resources based on their most recent snapshots.
|
||||
|
||||
This file was autogenerated with `mix ash_postgres.generate_migrations`
|
||||
"""
|
||||
|
||||
use Ecto.Migration
|
||||
|
||||
def up do
|
||||
alter table(:maps_v1) do
|
||||
modify :scopes, {:array, :text}, default: '{wormholes}'
|
||||
end
|
||||
|
||||
alter table(:character_v1) do
|
||||
add :description, :text
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:character_v1) do
|
||||
remove :description
|
||||
end
|
||||
|
||||
alter table(:maps_v1) do
|
||||
modify :scopes, {:array, :text}, default: nil
|
||||
end
|
||||
end
|
||||
end
|
||||
135
priv/repo/seeds_sponsors.exs
Normal file
135
priv/repo/seeds_sponsors.exs
Normal file
@@ -0,0 +1,135 @@
|
||||
# Seed script for example sponsor donations.
|
||||
#
|
||||
# Creates sample users, characters, a map, and donation transactions
|
||||
# so the /sponsors page and /characters/:eve_id profile pages have data.
|
||||
#
|
||||
# Run with:
|
||||
# mix run priv/repo/seeds_sponsors.exs
|
||||
#
|
||||
require Logger
|
||||
Logger.info("Seeding sponsor donation data...")
|
||||
|
||||
alias WandererApp.Repo
|
||||
|
||||
# Well-known EVE character data (real public info, no secrets)
|
||||
characters_data = [
|
||||
%{
|
||||
eve_id: "96734492",
|
||||
name: "Oz Hasaki",
|
||||
corporation_id: 98_553_333,
|
||||
corporation_name: "Wormhole Wanderers",
|
||||
corporation_ticker: "W.W",
|
||||
alliance_id: nil,
|
||||
alliance_name: nil,
|
||||
alliance_ticker: nil
|
||||
},
|
||||
%{
|
||||
eve_id: "2119543215",
|
||||
name: "Katya Itzimansen",
|
||||
corporation_id: 98_681_432,
|
||||
corporation_name: "Anoikis Explorers",
|
||||
corporation_ticker: "A.EXP",
|
||||
alliance_id: 99_011_258,
|
||||
alliance_name: "Anoikis Coalition",
|
||||
alliance_ticker: "ANOK"
|
||||
},
|
||||
%{
|
||||
eve_id: "93568202",
|
||||
name: "Dmitriy Lancel",
|
||||
corporation_id: 98_712_045,
|
||||
corporation_name: "Signal Cartel",
|
||||
corporation_ticker: "1420.",
|
||||
alliance_id: 99_005_338,
|
||||
alliance_name: "EvE-Scout Enclave",
|
||||
alliance_ticker: "SCOUT"
|
||||
},
|
||||
%{
|
||||
eve_id: "94801715",
|
||||
name: "Heron Explorer",
|
||||
corporation_id: 98_553_333,
|
||||
corporation_name: "Wormhole Wanderers",
|
||||
corporation_ticker: "W.W",
|
||||
alliance_id: nil,
|
||||
alliance_name: nil,
|
||||
alliance_ticker: nil
|
||||
}
|
||||
]
|
||||
|
||||
# Donation amounts (ISK) for each character — descending order
|
||||
donation_amounts = [
|
||||
2_500_000_000.0,
|
||||
1_200_000_000.0,
|
||||
800_000_000.0,
|
||||
350_000_000.0
|
||||
]
|
||||
|
||||
# ---------- Create users, characters, a map, and transactions ----------
|
||||
|
||||
Repo.transaction(fn ->
|
||||
# 1. Create a dummy map for the transactions (needs name + slug)
|
||||
{:ok, map} =
|
||||
WandererApp.Api.Map.new(%{
|
||||
name: "Seed Sponsors Map",
|
||||
slug: "seed-sponsors-map"
|
||||
})
|
||||
|
||||
Logger.info(" Created map: #{map.id}")
|
||||
|
||||
Enum.zip(characters_data, donation_amounts)
|
||||
|> Enum.each(fn {char_data, amount} ->
|
||||
# 2. Create a user
|
||||
{:ok, user} =
|
||||
WandererApp.Api.User
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
name: char_data.name,
|
||||
hash: "seed_sponsor_#{char_data.eve_id}"
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
Logger.info(" Created user: #{user.id} (#{user.name})")
|
||||
|
||||
# 3. Create a character linked to that user
|
||||
{:ok, character} =
|
||||
WandererApp.Api.Character
|
||||
|> Ash.Changeset.for_create(:create, %{
|
||||
eve_id: char_data.eve_id,
|
||||
name: char_data.name
|
||||
})
|
||||
|> Ash.create()
|
||||
|
||||
# Assign user
|
||||
{:ok, character} = WandererApp.Api.Character.assign_user(character, %{user_id: user.id})
|
||||
|
||||
# Update corporation info
|
||||
{:ok, character} =
|
||||
WandererApp.Api.Character.update_corporation(character, %{
|
||||
corporation_id: char_data.corporation_id,
|
||||
corporation_name: char_data.corporation_name,
|
||||
corporation_ticker: char_data.corporation_ticker
|
||||
})
|
||||
|
||||
# Update alliance info (if present)
|
||||
if char_data.alliance_id do
|
||||
WandererApp.Api.Character.update_alliance(character, %{
|
||||
alliance_id: char_data.alliance_id,
|
||||
alliance_name: char_data.alliance_name,
|
||||
alliance_ticker: char_data.alliance_ticker
|
||||
})
|
||||
end
|
||||
|
||||
Logger.info(" Created character: #{character.eve_id} (#{character.name})")
|
||||
|
||||
# 4. Create a donation transaction (type: :in)
|
||||
{:ok, _txn} =
|
||||
WandererApp.Api.MapTransaction.create(%{
|
||||
map_id: map.id,
|
||||
user_id: user.id,
|
||||
type: :in,
|
||||
amount: amount
|
||||
})
|
||||
|
||||
Logger.info(" Created donation: #{amount} ISK from #{char_data.name}")
|
||||
end)
|
||||
end)
|
||||
|
||||
Logger.info("Sponsor seed data complete!")
|
||||
413
priv/resource_snapshots/repo/character_v1/20260406213852.json
Normal file
413
priv/resource_snapshots/repo/character_v1/20260406213852.json
Normal file
@@ -0,0 +1,413 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "eve_id",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "online",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "deleted",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "scopes",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "character_owner_hash",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "token_type",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "expires_at",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "ship_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "ship_item_id",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "corporation_id",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "corporation_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "corporation_ticker",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "alliance_id",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "alliance_name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "alliance_ticker",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "tracking_pool",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "character_v1_user_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "user_v1"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "user_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "encrypted_eve_wallet_balance",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "encrypted_location",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "encrypted_ship",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "encrypted_solar_system_id",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "encrypted_structure_id",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "encrypted_station_id",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "encrypted_access_token",
|
||||
"type": "binary"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "encrypted_refresh_token",
|
||||
"type": "binary"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "B75AEDD6CAB8418E22082401EEB78FDA9E2B9B70883EC7EA0E05EE26695D138E",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "character_v1_unique_eve_id_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "eve_id"
|
||||
}
|
||||
],
|
||||
"name": "unique_eve_id",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "character_v1"
|
||||
}
|
||||
277
priv/resource_snapshots/repo/maps_v1/20260406213852.json
Normal file
277
priv/resource_snapshots/repo/maps_v1/20260406213852.json
Normal file
@@ -0,0 +1,277 @@
|
||||
{
|
||||
"attributes": [
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"gen_random_uuid()\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "name",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "slug",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "description",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "personal_note",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "public_api_key",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "[]",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "hubs",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "\"wormholes\"",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "scope",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "deleted",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "only_tracked_characters",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "options",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "webhooks_enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "false",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "sse_enabled",
|
||||
"type": "boolean"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "'{wormholes}'",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "scopes",
|
||||
"type": [
|
||||
"array",
|
||||
"text"
|
||||
]
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "inserted_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "fragment(\"(now() AT TIME ZONE 'utc')\")",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "updated_at",
|
||||
"type": "utc_datetime_usec"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "maps_v1_owner_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "character_v1"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "owner_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "17E507A2F8B57193D92DF2E707C6623C68A07B5058227A12DEF1522777BE7B83",
|
||||
"identities": [
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "maps_v1_unique_public_api_key_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "public_api_key"
|
||||
}
|
||||
],
|
||||
"name": "unique_public_api_key",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
},
|
||||
{
|
||||
"all_tenants?": false,
|
||||
"base_filter": null,
|
||||
"index_name": "maps_v1_unique_slug_index",
|
||||
"keys": [
|
||||
{
|
||||
"type": "atom",
|
||||
"value": "slug"
|
||||
}
|
||||
],
|
||||
"name": "unique_slug",
|
||||
"nils_distinct?": true,
|
||||
"where": null
|
||||
}
|
||||
],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "maps_v1"
|
||||
}
|
||||
BIN
priv/static/images/30747_64.png
Normal file
BIN
priv/static/images/30747_64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.4 KiB |
BIN
priv/static/images/89219_64.png
Normal file
BIN
priv/static/images/89219_64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
priv/static/images/concord-so.png
Normal file
BIN
priv/static/images/concord-so.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
BIN
priv/static/images/map.png
Normal file
BIN
priv/static/images/map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
BIN
priv/static/images/market.png
Normal file
BIN
priv/static/images/market.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -155,7 +155,7 @@ defmodule WandererAppWeb.OpenAPISpecAnalyzer do
|
||||
# Categorize schemas based on naming patterns
|
||||
request_schemas = Enum.filter(schema_names, &String.contains?(&1, "Request"))
|
||||
response_schemas = Enum.filter(schema_names, &String.contains?(&1, "Response"))
|
||||
shared_schemas = schema_names -- request_schemas -- response_schemas
|
||||
shared_schemas = schema_names -- (request_schemas -- response_schemas)
|
||||
|
||||
%{
|
||||
total: length(schema_names),
|
||||
|
||||
Reference in New Issue
Block a user