mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-04-15 23:28:01 +00:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39b1d09753 | ||
|
|
3d675aa10c | ||
|
|
cb66b73337 | ||
|
|
22ae774127 | ||
|
|
ab41b38f12 | ||
|
|
f1a933894f | ||
|
|
85d9095cb2 | ||
|
|
0032b56d09 | ||
|
|
48a6869565 | ||
|
|
3f9eff0d33 | ||
|
|
8e5ed22bc0 |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -2,6 +2,28 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.99.0](https://github.com/wanderer-industries/wanderer/compare/v1.98.1...v1.99.0) (2026-04-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* core: Add support for editing passages mass
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Add update mass button for ship
|
||||
|
||||
## [v1.98.1](https://github.com/wanderer-industries/wanderer/compare/v1.98.0...v1.98.1) (2026-04-10)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* signatures: Back linked signatures not cleared old connection statuses
|
||||
|
||||
## [v1.98.0](https://github.com/wanderer-industries/wanderer/compare/v1.97.5...v1.98.0) (2026-04-06)
|
||||
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ export const LocalCharactersItemTemplate = ({ showShipName, ...options }: LocalC
|
||||
<div
|
||||
className={clsx(
|
||||
classes.CharacterRow,
|
||||
'box-border flex items-center w-full whitespace-nowrap overflow-hidden text-ellipsis min-w-[0px]',
|
||||
'box-border flex items-center w-full whitespace-nowrap overflow-hidden text-ellipsis min-w-[0px] ',
|
||||
'px-1',
|
||||
{
|
||||
'surface-hover': options.odd,
|
||||
|
||||
@@ -17,29 +17,35 @@ import classes from './Connections.module.scss';
|
||||
import { InfoDrawer, SystemView, TimeAgo } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
|
||||
import { PassageCard } from './PassageCard';
|
||||
import { PassageMassDialog } from './PassageMassDialog';
|
||||
|
||||
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
|
||||
|
||||
const itemTemplate = (item: PassageWithSourceTarget, options: VirtualScrollerTemplateOptions) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterRow, 'w-full box-border', {
|
||||
'surface-hover': options.odd,
|
||||
['border-b border-gray-600 border-opacity-20']: !options.last,
|
||||
['bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10']: false,
|
||||
})}
|
||||
style={{ height: options.props.itemSize + 'px' }}
|
||||
>
|
||||
<PassageCard {...item} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const getPassageMass = (passage: Passage) => passage.mass ?? parseInt(passage.ship.ship_type_info.mass);
|
||||
|
||||
export interface ConnectionPassagesContentProps {
|
||||
passages: PassageWithSourceTarget[];
|
||||
onEditPassage: (passage: PassageWithSourceTarget) => void;
|
||||
}
|
||||
|
||||
export const ConnectionPassages = ({ passages = [] }: ConnectionPassagesContentProps) => {
|
||||
export const ConnectionPassages = ({ passages = [], onEditPassage }: ConnectionPassagesContentProps) => {
|
||||
const itemTemplate = useCallback(
|
||||
(item: PassageWithSourceTarget, options: VirtualScrollerTemplateOptions) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterRow, 'w-full box-border', {
|
||||
'surface-hover': options.odd,
|
||||
['border-b border-gray-600 border-opacity-20']: !options.last,
|
||||
['bg-green-500 hover:bg-green-700 transition duration-300 bg-opacity-10 hover:bg-opacity-10']: false,
|
||||
})}
|
||||
style={{ height: options.props.itemSize + 'px' }}
|
||||
>
|
||||
<PassageCard {...item} onEdit={() => onEditPassage(item)} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[onEditPassage],
|
||||
);
|
||||
|
||||
if (passages.length === 0) {
|
||||
return <div className="flex justify-center items-center text-stone-400 select-none">Nobody passed here</div>;
|
||||
}
|
||||
@@ -83,6 +89,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
|
||||
const [passages, setPassages] = useState<Passage[]>([]);
|
||||
const [info, setInfo] = useState<ConnectionInfoOutput | null>(null);
|
||||
const [editingPassage, setEditingPassage] = useState<PassageWithSourceTarget | null>(null);
|
||||
|
||||
const loadInfo = useCallback(
|
||||
async (connection: SolarSystemConnection) => {
|
||||
@@ -109,7 +116,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
},
|
||||
});
|
||||
|
||||
setPassages(result.passages.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at)));
|
||||
setPassages([...result.passages].sort((a, b) => sortByDate(b.inserted_at, a.inserted_at)));
|
||||
},
|
||||
[outCommand],
|
||||
);
|
||||
@@ -119,7 +126,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
return [];
|
||||
}
|
||||
|
||||
return passages
|
||||
return [...passages]
|
||||
.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at))
|
||||
.map<PassageWithSourceTarget>(x => ({
|
||||
...x,
|
||||
@@ -130,16 +137,48 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedConnection) {
|
||||
setEditingPassage(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setEditingPassage(null);
|
||||
loadInfo(selectedConnection);
|
||||
loadPassages(selectedConnection);
|
||||
}, [selectedConnection]);
|
||||
}, [loadInfo, loadPassages, selectedConnection]);
|
||||
|
||||
const approximateMass = useMemo(() => {
|
||||
return passages.reduce((acc, x) => acc + parseInt(x.ship.ship_type_info.mass), 0);
|
||||
return passages.reduce((acc, x) => acc + getPassageMass(x), 0);
|
||||
}, [passages]);
|
||||
|
||||
const handleEditPassage = useCallback((passage: PassageWithSourceTarget) => {
|
||||
setEditingPassage(passage);
|
||||
}, []);
|
||||
|
||||
const handleHidePassageDialog = useCallback(() => {
|
||||
setEditingPassage(null);
|
||||
}, []);
|
||||
|
||||
const handleSavePassageMass = useCallback(
|
||||
async (mass: number) => {
|
||||
if (!editingPassage) {
|
||||
return;
|
||||
}
|
||||
|
||||
await outCommand({
|
||||
type: OutCommand.updatePassageMass,
|
||||
data: {
|
||||
id: editingPassage.id,
|
||||
mass,
|
||||
},
|
||||
});
|
||||
|
||||
setPassages(prev => prev.map(passage => (passage.id === editingPassage.id ? { ...passage, mass } : passage)));
|
||||
setEditingPassage(prev => (prev ? { ...prev, mass } : prev));
|
||||
handleHidePassageDialog();
|
||||
},
|
||||
[editingPassage, handleHidePassageDialog, outCommand],
|
||||
);
|
||||
|
||||
if (!cnInfo) {
|
||||
return null;
|
||||
}
|
||||
@@ -201,8 +240,15 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
{/* separator */}
|
||||
<div className="w-full h-px bg-neutral-800 px-0.5"></div>
|
||||
|
||||
<ConnectionPassages passages={preparedPassages} />
|
||||
<ConnectionPassages passages={preparedPassages} onEditPassage={handleEditPassage} />
|
||||
</div>
|
||||
|
||||
<PassageMassDialog
|
||||
passage={editingPassage}
|
||||
visible={editingPassage != null}
|
||||
onHide={handleHidePassageDialog}
|
||||
onSave={handleSavePassageMass}
|
||||
/>
|
||||
</Sidebar>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import clsx from 'clsx';
|
||||
import classes from './PassageCard.module.scss';
|
||||
import { PassageWithSourceTarget } from '@/hooks/Mapper/types';
|
||||
import { SystemView, TimeAgo, TooltipPosition, WdImgButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { SystemView, TimeAgo, TooltipPosition, WdImgButton, WdTransition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { MouseEvent, useCallback, useMemo, useState } from 'react';
|
||||
import { ZKB_ICON } from '@/hooks/Mapper/icons';
|
||||
import { charEveWhoLink, charZKBLink } from '@/hooks/Mapper/helpers/linkHelpers.ts';
|
||||
import { getShipName } from './getShipName.ts';
|
||||
|
||||
type PassageCardType = {
|
||||
// compact?: boolean;
|
||||
showShipName?: boolean;
|
||||
// showSystem?: boolean;
|
||||
// useSystemsCache?: boolean;
|
||||
onEdit?: () => void;
|
||||
} & PassageWithSourceTarget;
|
||||
|
||||
const SHIP_NAME_RX = /u'|'/g;
|
||||
export const getShipName = (name: string) => {
|
||||
return name
|
||||
.replace(SHIP_NAME_RX, '')
|
||||
.replace(/\\u([\dA-Fa-f]{4})/g, (_, grp) => {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
})
|
||||
.replace(/\\x([\dA-Fa-f]{2})/g, (_, grp) => {
|
||||
return String.fromCharCode(parseInt(grp, 16));
|
||||
});
|
||||
};
|
||||
|
||||
export const PassageCard = ({ inserted_at, character: char, ship, source, target, from }: PassageCardType) => {
|
||||
export const PassageCard = ({
|
||||
inserted_at,
|
||||
character: char,
|
||||
ship,
|
||||
source,
|
||||
target,
|
||||
from,
|
||||
mass,
|
||||
onEdit,
|
||||
}: PassageCardType) => {
|
||||
const isOwn = false;
|
||||
const [hovered, setHovered] = useState(false);
|
||||
|
||||
const insertedAt = useMemo(() => {
|
||||
const date = new Date(inserted_at);
|
||||
@@ -37,9 +37,23 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
|
||||
|
||||
const handleOpenZKB = useCallback(() => window.open(charZKBLink(char.eve_id), '_blank'), [char]);
|
||||
const handleOpenEveWho = useCallback(() => window.open(charEveWhoLink(char.eve_id), '_blank'), [char]);
|
||||
const handleEdit = useCallback(
|
||||
(event: MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
onEdit?.();
|
||||
},
|
||||
[onEdit],
|
||||
);
|
||||
|
||||
const handleMouseEnter = useCallback(() => setHovered(true), []);
|
||||
const handleMouseLeave = useCallback(() => setHovered(false), []);
|
||||
|
||||
return (
|
||||
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}>
|
||||
<div
|
||||
className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className="flex flex-col justify-between px-2 py-1 gap-1">
|
||||
{/*here icon and other*/}
|
||||
<div className={clsx(classes.CharRow, classes.FourColumns)}>
|
||||
@@ -140,15 +154,30 @@ export const PassageCard = ({ inserted_at, character: char, ship, source, target
|
||||
</WdTooltipWrapper>
|
||||
</span>
|
||||
|
||||
<div className="text-stone-400">{kgToTons(parseInt(ship.ship_type_info.mass))}</div>
|
||||
<div className="text-stone-400">{kgToTons(mass ?? parseInt(ship.ship_type_info.mass))}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/*ship icon*/}
|
||||
<span
|
||||
<div
|
||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||
style={{ backgroundImage: `url(https://images.evetech.net/types/${ship.ship_type_id}/icon)` }}
|
||||
/>
|
||||
>
|
||||
<WdTransition active={hovered} timeout={50}>
|
||||
<div>
|
||||
{hovered && (
|
||||
<div
|
||||
className={clsx(
|
||||
'transition-all transform ease-in duration-200 cursor-pointer',
|
||||
'pi text-stone-500 text-[17px] w-[33px] h-[32px] !flex items-center justify-center border rounded-[2px]',
|
||||
'pi-cog text-stone-200/80 hover:!text-orange-400 border-stone-500/70 hover:!border-orange-400 bg-stone-800/70',
|
||||
)}
|
||||
onClick={handleEdit}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</WdTransition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -44,17 +44,6 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
|
||||
switch (group) {
|
||||
case SignatureGroup.Wormhole:
|
||||
if (values.linked_system) {
|
||||
await outCommand({
|
||||
type: OutCommand.linkSignatureToSystem,
|
||||
data: {
|
||||
signature_eve_id: signatureData.eve_id,
|
||||
solar_system_source: systemId,
|
||||
solar_system_target: values.linked_system,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
out = {
|
||||
...out,
|
||||
custom_info: JSON.stringify({
|
||||
@@ -125,6 +114,20 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
},
|
||||
});
|
||||
|
||||
// Link after updateSignatures so the K162's linked_system_id is still nil
|
||||
// during signature update, preventing maybe_update_connection_* from
|
||||
// resetting connection properties set by the forward signature.
|
||||
if (group === SignatureGroup.Wormhole && values.linked_system) {
|
||||
await outCommand({
|
||||
type: OutCommand.linkSignatureToSystem,
|
||||
data: {
|
||||
signature_eve_id: signatureData.eve_id,
|
||||
solar_system_source: systemId,
|
||||
solar_system_target: values.linked_system,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
signatureForm.reset();
|
||||
onHide();
|
||||
},
|
||||
|
||||
@@ -6,8 +6,10 @@ export type PassageLimitedCharacterType = Pick<
|
||||
>;
|
||||
|
||||
export type Passage = {
|
||||
id: string;
|
||||
from: boolean;
|
||||
inserted_at: string; // Date
|
||||
mass: number | null;
|
||||
ship: ShipTypeRaw;
|
||||
character: PassageLimitedCharacterType;
|
||||
};
|
||||
|
||||
@@ -272,6 +272,7 @@ export enum OutCommand {
|
||||
addSystem = 'add_system',
|
||||
openUserSettings = 'open_user_settings',
|
||||
getPassages = 'get_passages',
|
||||
updatePassageMass = 'update_passage_mass',
|
||||
linkSignatureToSystem = 'link_signature_to_system',
|
||||
getCorporationNames = 'get_corporation_names',
|
||||
getCorporationTicker = 'get_corporation_ticker',
|
||||
|
||||
@@ -17,12 +17,15 @@ defmodule WandererApp.Api.MapChainPassages do
|
||||
define(:read, action: :read)
|
||||
define(:by_map_id, action: :by_map_id)
|
||||
define(:by_connection, action: :by_connection)
|
||||
define(:update_mass, action: :update_mass)
|
||||
define(:by_id, get_by: [:id], action: :read)
|
||||
end
|
||||
|
||||
actions do
|
||||
default_accept [
|
||||
:ship_type_id,
|
||||
:ship_name,
|
||||
:mass,
|
||||
:solar_system_source_id,
|
||||
:solar_system_target_id
|
||||
]
|
||||
@@ -30,6 +33,12 @@ defmodule WandererApp.Api.MapChainPassages do
|
||||
defaults [:create, :read, :destroy]
|
||||
|
||||
update :update do
|
||||
accept [:mass]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
update :update_mass do
|
||||
accept [:mass]
|
||||
require_atomic? false
|
||||
end
|
||||
|
||||
@@ -37,6 +46,7 @@ defmodule WandererApp.Api.MapChainPassages do
|
||||
accept [
|
||||
:ship_type_id,
|
||||
:ship_name,
|
||||
:mass,
|
||||
:solar_system_source_id,
|
||||
:solar_system_target_id,
|
||||
:map_id,
|
||||
@@ -85,8 +95,10 @@ defmodule WandererApp.Api.MapChainPassages do
|
||||
|> WandererApp.Repo.all()
|
||||
|> Enum.map(fn [passage, character] ->
|
||||
%{
|
||||
id: passage.id,
|
||||
ship_type_id: passage.ship_type_id,
|
||||
ship_name: passage.ship_name,
|
||||
mass: passage.mass,
|
||||
inserted_at: passage.inserted_at,
|
||||
character: character
|
||||
}
|
||||
@@ -108,6 +120,7 @@ defmodule WandererApp.Api.MapChainPassages do
|
||||
|
||||
attribute :ship_type_id, :integer
|
||||
attribute :ship_name, :string
|
||||
attribute :mass, :integer
|
||||
attribute :solar_system_source_id, :integer
|
||||
attribute :solar_system_target_id, :integer
|
||||
|
||||
|
||||
@@ -552,15 +552,9 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
if not is_nil(forward_sig.custom_info) do
|
||||
case Jason.decode(forward_sig.custom_info) do
|
||||
{: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
|
||||
# Always prefer forward sig values over K162 defaults
|
||||
fwd_time = Map.get(decoded, "time_status") || signature_time_status
|
||||
fwd_mass = Map.get(decoded, "mass_status") || signature_mass_status
|
||||
|
||||
{fwd_time, fwd_mass}
|
||||
|
||||
@@ -600,6 +594,29 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
mass_status: signature_mass_status
|
||||
})
|
||||
end
|
||||
|
||||
# Update K162's custom_info to match the resolved connection values
|
||||
if not is_nil(signature_time_status) or not is_nil(signature_mass_status) do
|
||||
updated_custom_info =
|
||||
(signature.custom_info || "{}")
|
||||
|> Jason.decode!()
|
||||
|> then(fn decoded ->
|
||||
decoded
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_time_status),
|
||||
do: Map.put(d, "time_status", signature_time_status),
|
||||
else: d
|
||||
end)
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_mass_status),
|
||||
do: Map.put(d, "mass_status", signature_mass_status),
|
||||
else: d
|
||||
end)
|
||||
end)
|
||||
|> Jason.encode!()
|
||||
|
||||
MapSystemSignature.update(signature, %{custom_info: updated_custom_info})
|
||||
end
|
||||
end
|
||||
|
||||
# Broadcast update
|
||||
|
||||
@@ -265,6 +265,42 @@ defmodule WandererAppWeb.MapConnectionsEventHandler do
|
||||
{:reply, passages, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
"update_passage_mass",
|
||||
%{"id" => passage_id, "mass" => mass} = _event,
|
||||
%{
|
||||
assigns: %{
|
||||
has_tracked_characters?: true,
|
||||
user_permissions: %{update_system: true}
|
||||
}
|
||||
} = socket
|
||||
) do
|
||||
mass_value =
|
||||
cond do
|
||||
is_integer(mass) ->
|
||||
mass
|
||||
|
||||
is_binary(mass) ->
|
||||
case Integer.parse(mass) do
|
||||
{int_val, _} -> int_val
|
||||
:error -> nil
|
||||
end
|
||||
|
||||
true ->
|
||||
nil
|
||||
end
|
||||
|
||||
case WandererApp.Api.MapChainPassages.by_id(passage_id) do
|
||||
{:ok, passage} when not is_nil(passage) ->
|
||||
WandererApp.Api.MapChainPassages.update_mass(passage, %{mass: mass_value})
|
||||
|
||||
_ ->
|
||||
Logger.warning("update_passage_mass: passage not found id=#{passage_id}")
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(event, body, socket),
|
||||
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
|
||||
@@ -326,15 +326,9 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
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
|
||||
# Always prefer forward sig values over K162 defaults
|
||||
fwd_time = Map.get(decoded, "time_status") || signature_time_status
|
||||
fwd_mass = Map.get(decoded, "mass_status") || signature_mass_status
|
||||
|
||||
{fwd_time, fwd_mass}
|
||||
else
|
||||
@@ -373,6 +367,29 @@ defmodule WandererAppWeb.MapSignaturesEventHandler do
|
||||
mass_status: signature_mass_status
|
||||
})
|
||||
end
|
||||
|
||||
# Update K162's custom_info to match the resolved connection values
|
||||
if not is_nil(signature_time_status) or not is_nil(signature_mass_status) do
|
||||
updated_custom_info =
|
||||
(signature.custom_info || "{}")
|
||||
|> Jason.decode!()
|
||||
|> then(fn decoded ->
|
||||
decoded
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_time_status),
|
||||
do: Map.put(d, "time_status", signature_time_status),
|
||||
else: d
|
||||
end)
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_mass_status),
|
||||
do: Map.put(d, "mass_status", signature_mass_status),
|
||||
else: d
|
||||
end)
|
||||
end)
|
||||
|> Jason.encode!()
|
||||
|
||||
WandererApp.Api.MapSystemSignature.update(signature, %{custom_info: updated_custom_info})
|
||||
end
|
||||
end
|
||||
|
||||
WandererApp.Map.Server.Impl.broadcast!(map_id, :signatures_updated, solar_system_source)
|
||||
|
||||
@@ -88,7 +88,8 @@ defmodule WandererAppWeb.MapEventHandler do
|
||||
"update_connection_mass_status",
|
||||
"update_connection_ship_size_type",
|
||||
"update_connection_locked",
|
||||
"update_connection_custom_info"
|
||||
"update_connection_custom_info",
|
||||
"update_passage_mass"
|
||||
]
|
||||
|
||||
@map_activity_events [
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.98.0"
|
||||
@version "1.99.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
defmodule WandererApp.Repo.Migrations.AddMassToMapChainPassages 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(:map_chain_passages_v1) do
|
||||
add :mass, :bigint
|
||||
end
|
||||
end
|
||||
|
||||
def down do
|
||||
alter table(:map_chain_passages_v1) do
|
||||
remove :mass
|
||||
end
|
||||
|
||||
alter table(:maps_v1) do
|
||||
modify :scopes, {:array, :text}, default: nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,177 @@
|
||||
{
|
||||
"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?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "ship_type_id",
|
||||
"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": "mass",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "solar_system_source_id",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"allow_nil?": true,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": false,
|
||||
"references": null,
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "solar_system_target_id",
|
||||
"type": "bigint"
|
||||
},
|
||||
{
|
||||
"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?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "map_chain_passages_v1_map_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "maps_v1"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "map_id",
|
||||
"type": "uuid"
|
||||
},
|
||||
{
|
||||
"allow_nil?": false,
|
||||
"default": "nil",
|
||||
"generated?": false,
|
||||
"precision": null,
|
||||
"primary_key?": true,
|
||||
"references": {
|
||||
"deferrable": false,
|
||||
"destination_attribute": "id",
|
||||
"destination_attribute_default": null,
|
||||
"destination_attribute_generated": null,
|
||||
"index?": false,
|
||||
"match_type": null,
|
||||
"match_with": null,
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"name": "map_chain_passages_v1_character_id_fkey",
|
||||
"on_delete": null,
|
||||
"on_update": null,
|
||||
"primary_key?": true,
|
||||
"schema": null,
|
||||
"table": "character_v1"
|
||||
},
|
||||
"scale": null,
|
||||
"size": null,
|
||||
"source": "character_id",
|
||||
"type": "uuid"
|
||||
}
|
||||
],
|
||||
"base_filter": null,
|
||||
"check_constraints": [],
|
||||
"custom_indexes": [],
|
||||
"custom_statements": [],
|
||||
"has_create_action": true,
|
||||
"hash": "00AA9FB7759FCDF16C5C627E6735E0B568E517A360F2002AFE00018BD6CD8F2A",
|
||||
"identities": [],
|
||||
"multitenancy": {
|
||||
"attribute": null,
|
||||
"global": null,
|
||||
"strategy": null
|
||||
},
|
||||
"repo": "Elixir.WandererApp.Repo",
|
||||
"schema": null,
|
||||
"table": "map_chain_passages_v1"
|
||||
}
|
||||
277
priv/resource_snapshots/repo/maps_v1/20260331192521.json
Normal file
277
priv/resource_snapshots/repo/maps_v1/20260331192521.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": "21B2A84E49086754B40476C11B4EA5F576E8537449FB776941098773C5CD705F",
|
||||
"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"
|
||||
}
|
||||
359
test/unit/map/server/k162_backlink_preference_test.exs
Normal file
359
test/unit/map/server/k162_backlink_preference_test.exs
Normal file
@@ -0,0 +1,359 @@
|
||||
defmodule WandererApp.Map.Server.K162BacklinkPreferenceTest do
|
||||
@moduledoc """
|
||||
Tests for the K162 back-link preference fix.
|
||||
|
||||
Verifies that when a K162 (return wormhole) signature is linked to a system
|
||||
where a forward signature (e.g., H296) already set mass/time on the connection,
|
||||
the forward sig values take precedence over K162 form defaults.
|
||||
|
||||
Also verifies that the K162's custom_info is updated to reflect resolved values.
|
||||
"""
|
||||
use WandererApp.DataCase, async: false
|
||||
|
||||
import Mox
|
||||
|
||||
alias WandererApp.Api.{MapSystem, MapSystemSignature}
|
||||
alias WandererApp.Map.Server.SignaturesImpl
|
||||
alias WandererAppWeb.Factory
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
setup do
|
||||
Mox.set_mox_global()
|
||||
|
||||
# Setup DDRT mocks
|
||||
Test.DDRTMock
|
||||
|> stub(:init_tree, fn _name, _opts -> :ok end)
|
||||
|> stub(:insert, fn _data, _tree_name -> {:ok, %{}} end)
|
||||
|> stub(:update, fn _id, _data, _tree_name -> {:ok, %{}} end)
|
||||
|> stub(:delete, fn _ids, _tree_name -> {:ok, %{}} end)
|
||||
|> stub(:query, fn _bbox, _tree_name -> {:ok, []} end)
|
||||
|
||||
# Setup CachedInfo mocks
|
||||
WandererApp.CachedInfo.Mock
|
||||
|> stub(:get_system_static_info, fn
|
||||
30_000_142 ->
|
||||
{:ok,
|
||||
%{
|
||||
solar_system_id: 30_000_142,
|
||||
solar_system_name: "Jita",
|
||||
system_class: 7,
|
||||
security: "0.9"
|
||||
}}
|
||||
|
||||
30_000_143 ->
|
||||
{:ok,
|
||||
%{
|
||||
solar_system_id: 30_000_143,
|
||||
solar_system_name: "Perimeter",
|
||||
system_class: 7,
|
||||
security: "0.9"
|
||||
}}
|
||||
|
||||
_ ->
|
||||
{:error, :not_found}
|
||||
end)
|
||||
|
||||
character = Factory.create_character()
|
||||
map = Factory.create_map(%{owner_id: character.id})
|
||||
|
||||
%{map: map, character: character}
|
||||
end
|
||||
|
||||
describe "find_forward_signature/2" do
|
||||
test "finds forward signature in target system that links back to source", %{map: map} do
|
||||
# System A (source) at solar_system_id 30_000_142
|
||||
{:ok, system_a} =
|
||||
MapSystem.create(%{map_id: map.id, solar_system_id: 30_000_142, name: "System A"})
|
||||
|
||||
# System B (target) at solar_system_id 30_000_143
|
||||
{:ok, system_b} =
|
||||
MapSystem.create(%{map_id: map.id, solar_system_id: 30_000_143, name: "System B"})
|
||||
|
||||
# Forward sig in System A: H296 linking to System B (solar_system_id)
|
||||
_forward_sig =
|
||||
Factory.insert(:map_system_signature, %{
|
||||
system_id: system_a.id,
|
||||
eve_id: "FWD-001",
|
||||
type: "H296",
|
||||
group: "Wormhole",
|
||||
linked_system_id: 30_000_143,
|
||||
custom_info:
|
||||
Jason.encode!(%{"time_status" => 1, "mass_status" => 1, "k162Type" => nil})
|
||||
})
|
||||
|
||||
# find_forward_signature looks in the target system (system_b.id)
|
||||
# for a signature linking back to the source solar_system_id (30_000_142)
|
||||
# But the forward sig is in system_a, linking TO system_b.
|
||||
# So we need to call it with system_a.id (where the forward sig lives)
|
||||
# and source_solar_system_id = 30_000_143 (what the forward sig links to)
|
||||
#
|
||||
# Wait - re-reading the function:
|
||||
# find_forward_signature(target_system_uuid, source_solar_system_id)
|
||||
# It looks in target_system_uuid for sigs with linked_system_id == source_solar_system_id
|
||||
#
|
||||
# In the K162 linking scenario:
|
||||
# - K162 is in System B, user links it to System A
|
||||
# - We call find_forward_signature(target_system.id = System A, source_solar_system = System B's solar_system_id)
|
||||
# - This finds the H296 in System A that has linked_system_id = System B's solar_system_id
|
||||
|
||||
result = SignaturesImpl.find_forward_signature(system_a.id, 30_000_143)
|
||||
assert result != nil
|
||||
assert result.eve_id == "FWD-001"
|
||||
assert result.type == "H296"
|
||||
end
|
||||
|
||||
test "returns nil when no forward signature exists", %{map: map} do
|
||||
{:ok, system_a} =
|
||||
MapSystem.create(%{map_id: map.id, solar_system_id: 30_000_142, name: "System A"})
|
||||
|
||||
result = SignaturesImpl.find_forward_signature(system_a.id, 30_000_143)
|
||||
assert is_nil(result)
|
||||
end
|
||||
|
||||
test "returns nil when signatures exist but none link back to source", %{map: map} do
|
||||
{:ok, system_a} =
|
||||
MapSystem.create(%{map_id: map.id, solar_system_id: 30_000_142, name: "System A"})
|
||||
|
||||
# Signature in System A but not linked to anything
|
||||
_unlinked_sig =
|
||||
Factory.insert(:map_system_signature, %{
|
||||
system_id: system_a.id,
|
||||
eve_id: "UNL-001",
|
||||
type: "H296",
|
||||
group: "Wormhole"
|
||||
})
|
||||
|
||||
result = SignaturesImpl.find_forward_signature(system_a.id, 30_000_143)
|
||||
assert is_nil(result)
|
||||
end
|
||||
end
|
||||
|
||||
describe "back-link preference logic" do
|
||||
test "forward sig time_status and mass_status take precedence over K162 defaults" do
|
||||
# This tests the fixed logic: `Map.get(decoded, "time_status") || signature_time_status`
|
||||
# Forward sig has time_status=1 (16h EOL), mass_status=1 (half mass)
|
||||
# K162 has time_status=0 (24h), mass_status=0 (normal)
|
||||
|
||||
forward_custom_info = %{"time_status" => 1, "mass_status" => 1}
|
||||
k162_time_status = 0
|
||||
k162_mass_status = 0
|
||||
|
||||
# Fixed logic: always prefer forward sig values
|
||||
fwd_time = Map.get(forward_custom_info, "time_status") || k162_time_status
|
||||
fwd_mass = Map.get(forward_custom_info, "mass_status") || k162_mass_status
|
||||
|
||||
# Forward sig values (1) should take precedence
|
||||
assert fwd_time == 1
|
||||
assert fwd_mass == 1
|
||||
end
|
||||
|
||||
test "K162 values used when forward sig has nil values" do
|
||||
# Forward sig has no time/mass set
|
||||
forward_custom_info = %{}
|
||||
k162_time_status = 2
|
||||
k162_mass_status = 2
|
||||
|
||||
fwd_time = Map.get(forward_custom_info, "time_status") || k162_time_status
|
||||
fwd_mass = Map.get(forward_custom_info, "mass_status") || k162_mass_status
|
||||
|
||||
# K162 values should be used as fallback
|
||||
assert fwd_time == 2
|
||||
assert fwd_mass == 2
|
||||
end
|
||||
|
||||
test "0 values from forward sig are truthy in Elixir and take precedence" do
|
||||
# 0 is truthy in Elixir (only nil and false are falsy)
|
||||
forward_custom_info = %{"time_status" => 0, "mass_status" => 0}
|
||||
k162_time_status = 2
|
||||
k162_mass_status = 2
|
||||
|
||||
fwd_time = Map.get(forward_custom_info, "time_status") || k162_time_status
|
||||
fwd_mass = Map.get(forward_custom_info, "mass_status") || k162_mass_status
|
||||
|
||||
# 0 is truthy in Elixir, so forward sig's 0 values should take precedence
|
||||
assert fwd_time == 0
|
||||
assert fwd_mass == 0
|
||||
end
|
||||
|
||||
test "old logic (is_nil check) would incorrectly keep K162 defaults" do
|
||||
# Demonstrates the bug with old logic:
|
||||
# if is_nil(signature_time_status), do: Map.get(decoded, "time_status"), else: signature_time_status
|
||||
# When K162 has time_status=0, is_nil(0) is false, so K162's 0 was kept
|
||||
|
||||
forward_custom_info = %{"time_status" => 1, "mass_status" => 1}
|
||||
k162_time_status = 0
|
||||
k162_mass_status = 0
|
||||
|
||||
# Old logic (broken):
|
||||
old_fwd_time =
|
||||
if is_nil(k162_time_status),
|
||||
do: Map.get(forward_custom_info, "time_status"),
|
||||
else: k162_time_status
|
||||
|
||||
old_fwd_mass =
|
||||
if is_nil(k162_mass_status),
|
||||
do: Map.get(forward_custom_info, "mass_status"),
|
||||
else: k162_mass_status
|
||||
|
||||
# Old logic would keep K162 defaults (0) instead of forward sig values (1)
|
||||
assert old_fwd_time == 0
|
||||
assert old_fwd_mass == 0
|
||||
|
||||
# New logic (fixed):
|
||||
new_fwd_time = Map.get(forward_custom_info, "time_status") || k162_time_status
|
||||
new_fwd_mass = Map.get(forward_custom_info, "mass_status") || k162_mass_status
|
||||
|
||||
# New logic correctly uses forward sig values
|
||||
assert new_fwd_time == 1
|
||||
assert new_fwd_mass == 1
|
||||
end
|
||||
end
|
||||
|
||||
describe "K162 custom_info update after linking" do
|
||||
test "K162 signature custom_info is updated with resolved values", %{map: map} do
|
||||
{:ok, system_a} =
|
||||
MapSystem.create(%{map_id: map.id, solar_system_id: 30_000_142, name: "System A"})
|
||||
|
||||
# K162 in system A with default values
|
||||
k162_sig =
|
||||
Factory.insert(:map_system_signature, %{
|
||||
system_id: system_a.id,
|
||||
eve_id: "K162-001",
|
||||
type: "K162",
|
||||
group: "Wormhole",
|
||||
custom_info:
|
||||
Jason.encode!(%{"time_status" => 0, "mass_status" => 0, "k162Type" => "C2"})
|
||||
})
|
||||
|
||||
# Simulate what the linking code does: update K162's custom_info
|
||||
# with resolved values from the forward signature
|
||||
signature_time_status = 1
|
||||
signature_mass_status = 1
|
||||
|
||||
updated_custom_info =
|
||||
(k162_sig.custom_info || "{}")
|
||||
|> Jason.decode!()
|
||||
|> then(fn decoded ->
|
||||
decoded
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_time_status),
|
||||
do: Map.put(d, "time_status", signature_time_status),
|
||||
else: d
|
||||
end)
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_mass_status),
|
||||
do: Map.put(d, "mass_status", signature_mass_status),
|
||||
else: d
|
||||
end)
|
||||
end)
|
||||
|> Jason.encode!()
|
||||
|
||||
{:ok, updated_sig} =
|
||||
MapSystemSignature.update(k162_sig, %{custom_info: updated_custom_info})
|
||||
|
||||
# Verify the custom_info was updated
|
||||
decoded = Jason.decode!(updated_sig.custom_info)
|
||||
assert decoded["time_status"] == 1
|
||||
assert decoded["mass_status"] == 1
|
||||
# k162Type should be preserved
|
||||
assert decoded["k162Type"] == "C2"
|
||||
end
|
||||
|
||||
test "K162 custom_info update preserves existing fields", %{map: map} do
|
||||
{:ok, system_a} =
|
||||
MapSystem.create(%{map_id: map.id, solar_system_id: 30_000_142, name: "System A"})
|
||||
|
||||
k162_sig =
|
||||
Factory.insert(:map_system_signature, %{
|
||||
system_id: system_a.id,
|
||||
eve_id: "K162-002",
|
||||
type: "K162",
|
||||
group: "Wormhole",
|
||||
custom_info:
|
||||
Jason.encode!(%{
|
||||
"time_status" => 0,
|
||||
"mass_status" => 0,
|
||||
"k162Type" => "C5",
|
||||
"extra_field" => "preserved"
|
||||
})
|
||||
})
|
||||
|
||||
# Only update time_status, leave mass_status nil (should not update)
|
||||
signature_time_status = 2
|
||||
signature_mass_status = nil
|
||||
|
||||
updated_custom_info =
|
||||
(k162_sig.custom_info || "{}")
|
||||
|> Jason.decode!()
|
||||
|> then(fn decoded ->
|
||||
decoded
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_time_status),
|
||||
do: Map.put(d, "time_status", signature_time_status),
|
||||
else: d
|
||||
end)
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_mass_status),
|
||||
do: Map.put(d, "mass_status", signature_mass_status),
|
||||
else: d
|
||||
end)
|
||||
end)
|
||||
|> Jason.encode!()
|
||||
|
||||
{:ok, updated_sig} =
|
||||
MapSystemSignature.update(k162_sig, %{custom_info: updated_custom_info})
|
||||
|
||||
decoded = Jason.decode!(updated_sig.custom_info)
|
||||
# time_status should be updated
|
||||
assert decoded["time_status"] == 2
|
||||
# mass_status should remain unchanged (nil signature_mass_status)
|
||||
assert decoded["mass_status"] == 0
|
||||
# Other fields should be preserved
|
||||
assert decoded["k162Type"] == "C5"
|
||||
assert decoded["extra_field"] == "preserved"
|
||||
end
|
||||
|
||||
test "K162 custom_info update handles nil initial custom_info", %{map: map} do
|
||||
{:ok, system_a} =
|
||||
MapSystem.create(%{map_id: map.id, solar_system_id: 30_000_142, name: "System A"})
|
||||
|
||||
k162_sig =
|
||||
Factory.insert(:map_system_signature, %{
|
||||
system_id: system_a.id,
|
||||
eve_id: "K162-003",
|
||||
type: "K162",
|
||||
group: "Wormhole"
|
||||
# custom_info is nil by default
|
||||
})
|
||||
|
||||
signature_time_status = 1
|
||||
signature_mass_status = 1
|
||||
|
||||
updated_custom_info =
|
||||
(k162_sig.custom_info || "{}")
|
||||
|> Jason.decode!()
|
||||
|> then(fn decoded ->
|
||||
decoded
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_time_status),
|
||||
do: Map.put(d, "time_status", signature_time_status),
|
||||
else: d
|
||||
end)
|
||||
|> then(fn d ->
|
||||
if not is_nil(signature_mass_status),
|
||||
do: Map.put(d, "mass_status", signature_mass_status),
|
||||
else: d
|
||||
end)
|
||||
end)
|
||||
|> Jason.encode!()
|
||||
|
||||
{:ok, updated_sig} =
|
||||
MapSystemSignature.update(k162_sig, %{custom_info: updated_custom_info})
|
||||
|
||||
decoded = Jason.decode!(updated_sig.custom_info)
|
||||
assert decoded["time_status"] == 1
|
||||
assert decoded["mass_status"] == 1
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user