mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-01-30 02:26:03 +00:00
Compare commits
29 Commits
v1.91.10
...
multiple-s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f4ddc8dc8b | ||
|
|
ac9b46e24d | ||
|
|
40d0a0777a | ||
|
|
608792d99a | ||
|
|
dc9e0c821e | ||
|
|
79d4fd0e43 | ||
|
|
5d03c1ecc7 | ||
|
|
2eef05495e | ||
|
|
f724455a1e | ||
|
|
33bbb3425c | ||
|
|
a919bd9038 | ||
|
|
8ae34cd94a | ||
|
|
2f38da52e8 | ||
|
|
89d7df0ba2 | ||
|
|
ba0c10d2e4 | ||
|
|
996c88d839 | ||
|
|
80e998cf79 | ||
|
|
d2bcb89fa1 | ||
|
|
922f296f17 | ||
|
|
71dc20c933 | ||
|
|
80f7d34d3d | ||
|
|
113fe1c695 | ||
|
|
5550844912 | ||
|
|
0228e68a1d | ||
|
|
a7d6b06332 | ||
|
|
8f6da817db | ||
|
|
378f22a1ef | ||
|
|
14730097b2 | ||
|
|
e8bff3098a |
22
CHANGELOG.md
22
CHANGELOG.md
@@ -2,6 +2,28 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.92.0](https://github.com/wanderer-industries/wanderer/compare/v1.91.11...v1.92.0) (2026-01-14)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Added ability to select a range of wh classes for k162.
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Show c1/c2/c3 or c4/c5 or link signature modal
|
||||
|
||||
## [v1.91.11](https://github.com/wanderer-industries/wanderer/compare/v1.91.10...v1.91.11) (2026-01-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* allow sig api when map relay is off
|
||||
|
||||
## [v1.91.10](https://github.com/wanderer-industries/wanderer/compare/v1.91.9...v1.91.10) (2026-01-07)
|
||||
|
||||
|
||||
|
||||
@@ -3,9 +3,9 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { useSystemInfo } from '@/hooks/Mapper/components/hooks';
|
||||
import {
|
||||
SOLAR_SYSTEM_CLASS_IDS,
|
||||
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
|
||||
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
|
||||
SOLAR_SYSTEM_CLASS_IDS,
|
||||
SOLAR_SYSTEM_CLASSES_TO_CLASS_GROUPS,
|
||||
WORMHOLES_ADDITIONAL_INFO_BY_SHORT_NAME,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { SystemSignaturesContent } from '@/hooks/Mapper/components/mapInterface/widgets/SystemSignatures/SystemSignaturesContent';
|
||||
import { K162_TYPES_MAP } from '@/hooks/Mapper/constants.ts';
|
||||
@@ -91,7 +91,7 @@ export const SystemLinkSignatureDialog = ({ data, setVisible }: SystemLinkSignat
|
||||
|
||||
if (k162TypeInfo) {
|
||||
// Check if the k162Type matches our target system class
|
||||
return customInfo.k162Type === targetSystemClassGroup;
|
||||
return k162TypeInfo.value.includes(targetSystemClassGroup);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, ClipboardEvent, useRef } from 'react';
|
||||
import React, { useCallback, ClipboardEvent, useRef, useState } from 'react';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import useMaxWidth from '@/hooks/Mapper/hooks/useMaxWidth';
|
||||
import {
|
||||
@@ -13,7 +13,9 @@ import { Widget } from '@/hooks/Mapper/components/mapInterface/components';
|
||||
|
||||
import { SystemStructuresContent } from './SystemStructuresContent/SystemStructuresContent';
|
||||
import { useSystemStructures } from './hooks/useSystemStructures';
|
||||
import { processSnippetText } from './helpers';
|
||||
import { processSnippetText, StructureItem } from './helpers';
|
||||
import { SystemStructuresOwnersDialog } from './SystemStructuresOwnersDialog/SystemStructuresOwnersDialog';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export const SystemStructures: React.FC = () => {
|
||||
const {
|
||||
@@ -24,6 +26,7 @@ export const SystemStructures: React.FC = () => {
|
||||
const isNotSelectedSystem = selectedSystems.length !== 1;
|
||||
|
||||
const { structures, handleUpdateStructures } = useSystemStructures({ systemId, outCommand });
|
||||
const [showEditDialog, setShowEditDialog] = useState(false);
|
||||
|
||||
const labelRef = useRef<HTMLDivElement>(null);
|
||||
const isCompact = useMaxWidth(labelRef, 260);
|
||||
@@ -48,6 +51,18 @@ export const SystemStructures: React.FC = () => {
|
||||
[processClipboard],
|
||||
);
|
||||
|
||||
const handleSave = (updatedStructures: StructureItem[]) => {
|
||||
handleUpdateStructures(updatedStructures)
|
||||
}
|
||||
|
||||
const handleOpenDialog = useCallback(() => {
|
||||
setShowEditDialog(true)
|
||||
}, [])
|
||||
|
||||
const handleCloseDialog = useCallback(() => {
|
||||
setShowEditDialog(false)
|
||||
}, [])
|
||||
|
||||
const handlePasteTimer = useCallback(async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
@@ -71,8 +86,19 @@ export const SystemStructures: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<LayoutEventBlocker className="flex gap-2.5">
|
||||
{structures.length > 1 && (
|
||||
<WdImgButton
|
||||
className={clsx(PrimeIcons.USER_EDIT, 'text-sky-400 hover:text-sky-200 transition duration-300')}
|
||||
onClick={handleOpenDialog}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
// @ts-ignore
|
||||
content: 'Update all structure owners',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<WdImgButton
|
||||
className={`${PrimeIcons.CLOCK} text-sky-400 hover:text-sky-200 transition duration-300`}
|
||||
className={clsx(PrimeIcons.CLOCK, 'text-sky-400 hover:text-sky-200 transition duration-300')}
|
||||
onClick={handlePasteTimer}
|
||||
tooltip={{
|
||||
position: TooltipPosition.left,
|
||||
@@ -117,6 +143,15 @@ export const SystemStructures: React.FC = () => {
|
||||
<SystemStructuresContent structures={structures} onUpdateStructures={handleUpdateStructures} />
|
||||
)}
|
||||
</Widget>
|
||||
|
||||
{showEditDialog && (
|
||||
<SystemStructuresOwnersDialog
|
||||
visible={showEditDialog}
|
||||
structures={structures}
|
||||
onClose={handleCloseDialog}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
|
||||
.systemStructuresOwnersDialog {
|
||||
|
||||
.p-dialog-content {
|
||||
background-color: var(--surface-800) !important;
|
||||
}
|
||||
|
||||
.p-dialog-header {
|
||||
background-color: var(--surface-700);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.p-dialog-header-icon,
|
||||
.p-dialog-header-title {
|
||||
color: var(--gray-200);
|
||||
}
|
||||
|
||||
.p-inputtext {
|
||||
background-color: #2a2a2a !important;
|
||||
color: #ddd !important;
|
||||
font-size: 12px !important;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
}
|
||||
|
||||
.p-dialog-footer {
|
||||
.p-button {
|
||||
font-size: 12px !important;
|
||||
padding: 0.3rem 0.75rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { AutoComplete } from 'primereact/autocomplete';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { StructureItem } from '../helpers';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useToast } from '@/hooks/Mapper/ToastProvider';
|
||||
|
||||
interface StructuresOwnersEditDialogProps {
|
||||
visible: boolean;
|
||||
structures: StructureItem[];
|
||||
onClose: () => void;
|
||||
onSave: (updatedStuctures: StructureItem[]) => void;
|
||||
}
|
||||
|
||||
export const SystemStructuresOwnersDialog: React.FC<StructuresOwnersEditDialogProps> = ({
|
||||
visible,
|
||||
structures,
|
||||
onClose,
|
||||
onSave,
|
||||
}) => {
|
||||
const [ownerInput, setOwnerInput] = useState('');
|
||||
const [ownerSuggestions, setOwnerSuggestions] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
const { outCommand } = useMapRootState();
|
||||
const { show } = useToast();
|
||||
|
||||
const [prevQuery, setPrevQuery] = useState('');
|
||||
const [prevResults, setPrevResults] = useState<{ label: string; value: string }[]>([]);
|
||||
const [editData, setEditData] = useState<StructureItem[]>(structures)
|
||||
|
||||
// Searching corporation owners via auto-complete
|
||||
const searchOwners = useCallback(
|
||||
async (e: { query: string }) => {
|
||||
const newQuery = e.query.trim();
|
||||
if (!newQuery) {
|
||||
setOwnerSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If user typed more text but we have partial match in prevResults
|
||||
if (newQuery.startsWith(prevQuery) && prevResults.length > 0) {
|
||||
const filtered = prevResults.filter(item => item.label.toLowerCase().includes(newQuery.toLowerCase()));
|
||||
setOwnerSuggestions(filtered);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO fix it
|
||||
const { results = [] } = await outCommand({
|
||||
type: OutCommand.getCorporationNames,
|
||||
data: { search: newQuery },
|
||||
});
|
||||
setOwnerSuggestions(results);
|
||||
setPrevQuery(newQuery);
|
||||
setPrevResults(results);
|
||||
} catch (err) {
|
||||
show({
|
||||
severity: 'error',
|
||||
summary: 'Failed to fetch owners',
|
||||
detail: `${err}`,
|
||||
life: 10000,
|
||||
})
|
||||
}
|
||||
},
|
||||
[prevQuery, prevResults, outCommand],
|
||||
);
|
||||
|
||||
|
||||
// when user picks a corp from auto-complete
|
||||
const handleSelectOwner = (selected: { label: string; value: string }) => {
|
||||
setOwnerInput(selected.label);
|
||||
|
||||
setEditData(structures.map(item => {
|
||||
return { ...item, ownerName: selected.label, ownerId: selected.value }
|
||||
}))
|
||||
};
|
||||
|
||||
const handleSaveClick = async () => {
|
||||
if (!editData) return;
|
||||
|
||||
// fetch corporation ticker if we have an ownerId
|
||||
for (const structure of editData) {
|
||||
if (structure.ownerId) {
|
||||
try {
|
||||
// TODO fix it
|
||||
const { ticker } = await outCommand({
|
||||
type: OutCommand.getCorporationTicker,
|
||||
data: { corp_id: structure.ownerId },
|
||||
});
|
||||
structure.ownerTicker = ticker ?? '';
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch ticker:', err);
|
||||
structure.ownerTicker = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
onSave(editData);
|
||||
onClose()
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
visible={visible}
|
||||
onHide={onClose}
|
||||
header={'Update All Structure Owners'}
|
||||
className={clsx('myStructuresOwnersDialog', 'text-stone-200 w-full max-w-md')}
|
||||
>
|
||||
<div className="flex flex-col gap-2 text-[14px]">
|
||||
<div className="flex gap-2">
|
||||
Updating the corporation name below will update all structures currently
|
||||
saved within the system.
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<label className="grid grid-cols-[100px_1fr] gap-2 items-start mt-2">
|
||||
<span className="mt-1">Structures to update:</span>
|
||||
<ul>
|
||||
{structures && structures.map((item, i) => (
|
||||
<li key={i}>{item.structureType || 'Unknown Type'} - {item.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center">
|
||||
<span>Owner:</span>
|
||||
<AutoComplete
|
||||
id="owner"
|
||||
value={ownerInput}
|
||||
suggestions={ownerSuggestions}
|
||||
completeMethod={searchOwners}
|
||||
minLength={3}
|
||||
delay={400}
|
||||
field="label"
|
||||
placeholder="Corporation name..."
|
||||
onChange={e => setOwnerInput(e.value)}
|
||||
onSelect={e => handleSelectOwner(e.value)}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end items-center gap-2 mt-4">
|
||||
<WdButton label="Save" className="p-button-sm" onClick={handleSaveClick} />
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -13,6 +13,26 @@ export const renderK162Type = (option: K162Type) => {
|
||||
return renderNoValue();
|
||||
}
|
||||
|
||||
if (['c1_c2_c3', 'c4_c5'].includes(value)) {
|
||||
const arr = whClassName.split('_');
|
||||
|
||||
return (
|
||||
<div className="flex gap-1 items-center">
|
||||
{arr.map(x => (
|
||||
<WHClassView
|
||||
key={x}
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
hideWhClassName
|
||||
hideTooltip
|
||||
whClassName={x}
|
||||
noOffset
|
||||
useShortTitle
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<WHClassView
|
||||
classNameWh="!text-[11px] !font-bold"
|
||||
|
||||
@@ -88,6 +88,16 @@ export const K162_TYPES: K162Type[] = [
|
||||
value: 'ns',
|
||||
whClassName: 'C248',
|
||||
},
|
||||
{
|
||||
label: 'C1/C2/C3',
|
||||
value: 'c1_c2_c3',
|
||||
whClassName: 'E004_D382_L477',
|
||||
},
|
||||
{
|
||||
label: 'C4/C5',
|
||||
value: 'c4_c5',
|
||||
whClassName: 'M001_L614',
|
||||
},
|
||||
{
|
||||
label: 'C1',
|
||||
value: 'c1',
|
||||
|
||||
@@ -123,7 +123,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:group,
|
||||
:type,
|
||||
:custom_info,
|
||||
:deleted
|
||||
:deleted,
|
||||
:linked_system_id
|
||||
]
|
||||
end
|
||||
|
||||
@@ -140,7 +141,8 @@ defmodule WandererApp.Api.MapSystemSignature do
|
||||
:type,
|
||||
:custom_info,
|
||||
:deleted,
|
||||
:update_forced_at
|
||||
:update_forced_at,
|
||||
:linked_system_id
|
||||
]
|
||||
|
||||
primary? true
|
||||
|
||||
@@ -152,7 +152,8 @@ defmodule WandererApp.Map.Manager do
|
||||
"[cleanup_orphaned_pings] Found #{length(orphaned_pings)} orphaned pings, cleaning up..."
|
||||
)
|
||||
|
||||
Enum.each(orphaned_pings, fn %{id: ping_id, map_id: map_id, type: type, system: system} = ping ->
|
||||
Enum.each(orphaned_pings, fn %{id: ping_id, map_id: map_id, type: type, system: system} =
|
||||
ping ->
|
||||
reason =
|
||||
cond do
|
||||
is_nil(ping.system) -> "system deleted"
|
||||
@@ -178,7 +179,10 @@ defmodule WandererApp.Map.Manager do
|
||||
Ash.destroy!(ping)
|
||||
end)
|
||||
|
||||
Logger.info("[cleanup_orphaned_pings] Cleaned up #{length(orphaned_pings)} orphaned pings")
|
||||
Logger.info(
|
||||
"[cleanup_orphaned_pings] Cleaned up #{length(orphaned_pings)} orphaned pings"
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
|
||||
@@ -126,4 +126,12 @@ defmodule WandererApp.Map.Operations do
|
||||
@doc "Delete a signature in a map"
|
||||
@spec delete_signature(String.t(), String.t()) :: :ok | {:error, String.t()}
|
||||
defdelegate delete_signature(map_id, sig_id), to: Signatures
|
||||
|
||||
@doc "Link a signature to a target system"
|
||||
@spec link_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
defdelegate link_signature(conn, sig_id, params), to: Signatures
|
||||
|
||||
@doc "Unlink a signature from its target system"
|
||||
@spec unlink_signature(Plug.Conn.t(), String.t()) :: {:ok, map()} | {:error, atom()}
|
||||
defdelegate unlink_signature(conn, sig_id), to: Signatures
|
||||
end
|
||||
|
||||
@@ -63,13 +63,31 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
if is_nil(src_info) or is_nil(tgt_info) do
|
||||
{:error, :invalid_system_info}
|
||||
else
|
||||
# Get wormhole_type for ship size inference
|
||||
wormhole_type = attrs["wormhole_type"]
|
||||
|
||||
# Build extra_info map with optional connection attributes
|
||||
extra_info =
|
||||
%{}
|
||||
|> maybe_add_extra("time_status", attrs["time_status"])
|
||||
|> maybe_add_extra("mass_status", attrs["mass_status"])
|
||||
|> maybe_add_extra("locked", attrs["locked"])
|
||||
|> maybe_add_extra("wormhole_type", wormhole_type)
|
||||
|
||||
info = %{
|
||||
solar_system_source_id: src_info.solar_system_id,
|
||||
solar_system_target_id: tgt_info.solar_system_id,
|
||||
character_id: char_id,
|
||||
type: parse_type(attrs["type"]),
|
||||
ship_size_type:
|
||||
resolve_ship_size(attrs["type"], attrs["ship_size_type"], src_info, tgt_info)
|
||||
resolve_ship_size(
|
||||
attrs["type"],
|
||||
attrs["ship_size_type"],
|
||||
wormhole_type,
|
||||
src_info,
|
||||
tgt_info
|
||||
),
|
||||
extra_info: if(extra_info == %{}, do: nil, else: extra_info)
|
||||
}
|
||||
|
||||
case Server.add_connection(map_id, info) do
|
||||
@@ -95,10 +113,11 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
# Determines the ship size for a connection, applying wormhole-specific rules
|
||||
# for C1, C13, and C4⇄NS links, falling back to the caller's provided size or Large.
|
||||
defp resolve_ship_size(type_val, ship_size_val, src_info, tgt_info) do
|
||||
# If wormhole_type is provided (e.g., "H296"), infer ship size from it.
|
||||
defp resolve_ship_size(type_val, ship_size_val, wormhole_type, src_info, tgt_info) do
|
||||
case parse_type(type_val) do
|
||||
@connection_type_wormhole ->
|
||||
wormhole_ship_size(ship_size_val, src_info, tgt_info)
|
||||
wormhole_ship_size(ship_size_val, wormhole_type, src_info, tgt_info)
|
||||
|
||||
_other ->
|
||||
# Stargates and others just use the parsed or default size
|
||||
@@ -108,15 +127,45 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
# -- Wormhole‑specific sizing rules ----------------------------------------
|
||||
|
||||
defp wormhole_ship_size(ship_size_val, src, tgt) do
|
||||
defp wormhole_ship_size(ship_size_val, wormhole_type, src, tgt) do
|
||||
# First, try to infer from wormhole_type (e.g., "H296", "C5", etc.)
|
||||
inferred_size = infer_ship_size_from_wormhole_type(wormhole_type)
|
||||
# Parse ship_size_val early to handle string values correctly
|
||||
parsed_ship_size = parse_ship_size(ship_size_val, nil)
|
||||
|
||||
cond do
|
||||
c1_system?(src, tgt) -> @medium_ship_size
|
||||
c13_system?(src, tgt) -> @small_ship_size
|
||||
c4_to_ns?(src, tgt) -> @small_ship_size
|
||||
true -> parse_ship_size(ship_size_val, @large_ship_size)
|
||||
# If user explicitly provided a ship_size_val, use it
|
||||
not is_nil(parsed_ship_size) ->
|
||||
parsed_ship_size
|
||||
|
||||
# If we could infer from wormhole_type, use that
|
||||
not is_nil(inferred_size) ->
|
||||
inferred_size
|
||||
|
||||
# Otherwise fall back to system class rules
|
||||
c1_system?(src, tgt) ->
|
||||
@medium_ship_size
|
||||
|
||||
c13_system?(src, tgt) ->
|
||||
@small_ship_size
|
||||
|
||||
c4_to_ns?(src, tgt) ->
|
||||
@small_ship_size
|
||||
|
||||
true ->
|
||||
@large_ship_size
|
||||
end
|
||||
end
|
||||
|
||||
# Infer ship size from wormhole type name using EVE static data
|
||||
defp infer_ship_size_from_wormhole_type(nil), do: nil
|
||||
defp infer_ship_size_from_wormhole_type(""), do: nil
|
||||
defp infer_ship_size_from_wormhole_type("K162"), do: nil
|
||||
|
||||
defp infer_ship_size_from_wormhole_type(wormhole_type) do
|
||||
WandererApp.Utils.EVEUtil.get_wh_size(wormhole_type)
|
||||
end
|
||||
|
||||
defp c1_system?(%{system_class: @c1_system_class}, _), do: true
|
||||
defp c1_system?(_, %{system_class: @c1_system_class}), do: true
|
||||
defp c1_system?(_, _), do: false
|
||||
@@ -162,6 +211,9 @@ defmodule WandererApp.Map.Operations.Connections do
|
||||
|
||||
defp parse_type(_), do: @connection_type_wormhole
|
||||
|
||||
defp maybe_add_extra(map, _key, nil), do: map
|
||||
defp maybe_add_extra(map, key, value), do: Map.put(map, key, value)
|
||||
|
||||
defp parse_int(nil, field), do: {:error, {:missing_field, field}}
|
||||
defp parse_int(val, _) when is_integer(val), do: {:ok, val}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
|
||||
require Logger
|
||||
alias WandererApp.Map.Operations
|
||||
alias WandererApp.Map.Operations.Connections
|
||||
alias WandererApp.Api.{Character, MapSystem, MapSystemSignature}
|
||||
alias WandererApp.Map.Server
|
||||
alias WandererApp.Utils.EVEUtil
|
||||
|
||||
@spec validate_character_eve_id(map() | nil, String.t()) ::
|
||||
{:ok, String.t()} | {:error, :invalid_character} | {:error, :unexpected_error}
|
||||
@@ -78,8 +80,7 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
)
|
||||
when is_integer(solar_system_id) do
|
||||
with {:ok, validated_char_uuid} <- validate_character_eve_id(params, char_id),
|
||||
{:ok, system} <-
|
||||
MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: solar_system_id}) do
|
||||
{:ok, system} <- ensure_system_on_map(map_id, solar_system_id, user_id, char_id) do
|
||||
attrs =
|
||||
params
|
||||
|> Map.put("system_id", system.id)
|
||||
@@ -95,6 +96,21 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
delete_connection_with_sigs: false
|
||||
}) do
|
||||
:ok ->
|
||||
# Handle linked_system_id if provided - auto-add system and create/update connection
|
||||
linked_system_id = Map.get(params, "linked_system_id")
|
||||
wormhole_type = Map.get(params, "type")
|
||||
|
||||
if is_integer(linked_system_id) and linked_system_id != solar_system_id do
|
||||
handle_linked_system(
|
||||
map_id,
|
||||
solar_system_id,
|
||||
linked_system_id,
|
||||
wormhole_type,
|
||||
user_id,
|
||||
char_id
|
||||
)
|
||||
end
|
||||
|
||||
# Try to fetch the created signature to return with proper fields
|
||||
with {:ok, sigs} <-
|
||||
MapSystemSignature.by_system_id_and_eve_ids(system.id, [attrs["eve_id"]]),
|
||||
@@ -130,6 +146,13 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
Logger.error("[create_signature] Unexpected error during character validation")
|
||||
{:error, :unexpected_error}
|
||||
|
||||
{:error, :invalid_solar_system} ->
|
||||
Logger.error(
|
||||
"[create_signature] Invalid solar_system_id: #{solar_system_id} (not a valid EVE system)"
|
||||
)
|
||||
|
||||
{:error, :invalid_solar_system}
|
||||
|
||||
_ ->
|
||||
Logger.error(
|
||||
"[create_signature] System not found for solar_system_id: #{solar_system_id}"
|
||||
@@ -148,6 +171,203 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
|
||||
def create_signature(_conn, _params), do: {:error, :missing_params}
|
||||
|
||||
# Check cache (not DB) to ensure system is actually visible on the map.
|
||||
@spec ensure_system_on_map(String.t(), integer(), String.t(), String.t()) ::
|
||||
{:ok, map()} | {:error, atom()}
|
||||
defp ensure_system_on_map(map_id, solar_system_id, user_id, char_id) do
|
||||
case WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_id}) do
|
||||
nil -> add_system_to_map(map_id, solar_system_id, user_id, char_id)
|
||||
system -> {:ok, system}
|
||||
end
|
||||
end
|
||||
|
||||
@spec add_system_to_map(String.t(), integer(), String.t(), String.t()) ::
|
||||
{:ok, map()} | {:error, atom()}
|
||||
defp add_system_to_map(map_id, solar_system_id, user_id, char_id) do
|
||||
with {:ok, static_info} when not is_nil(static_info) <-
|
||||
WandererApp.CachedInfo.get_system_static_info(solar_system_id),
|
||||
:ok <-
|
||||
Server.add_system(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_id, coordinates: nil},
|
||||
user_id,
|
||||
char_id
|
||||
),
|
||||
system when not is_nil(system) <- fetch_system_after_add(map_id, solar_system_id) do
|
||||
Logger.info("[create_signature] Auto-added system #{solar_system_id} to map #{map_id}")
|
||||
{:ok, system}
|
||||
else
|
||||
{:ok, nil} ->
|
||||
{:error, :invalid_solar_system}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, :invalid_solar_system}
|
||||
|
||||
nil ->
|
||||
Logger.error("[add_system_to_map] Failed to fetch system after add")
|
||||
{:error, :system_add_failed}
|
||||
|
||||
error ->
|
||||
Logger.error("[add_system_to_map] Failed to add system: #{inspect(error)}")
|
||||
{:error, :system_add_failed}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_system_after_add(map_id, solar_system_id) do
|
||||
case WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_id}) do
|
||||
nil ->
|
||||
case MapSystem.read_by_map_and_solar_system(%{
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id
|
||||
}) do
|
||||
{:ok, system} -> system
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
system ->
|
||||
system
|
||||
end
|
||||
end
|
||||
|
||||
# Handles the linked_system_id logic: auto-adds the linked system and creates/updates connection
|
||||
@spec handle_linked_system(
|
||||
String.t(),
|
||||
integer(),
|
||||
integer(),
|
||||
String.t() | nil,
|
||||
String.t(),
|
||||
String.t()
|
||||
) :: :ok | {:error, atom()}
|
||||
defp handle_linked_system(
|
||||
map_id,
|
||||
source_system_id,
|
||||
linked_system_id,
|
||||
wormhole_type,
|
||||
user_id,
|
||||
char_id
|
||||
) do
|
||||
# Ensure the linked system is on the map
|
||||
case ensure_system_on_map(map_id, linked_system_id, user_id, char_id) do
|
||||
{:ok, _linked_system} ->
|
||||
# Check if connection exists between the systems
|
||||
case Connections.get_connection_by_systems(map_id, source_system_id, linked_system_id) do
|
||||
{:ok, nil} ->
|
||||
# No connection exists, create one
|
||||
create_connection_with_wormhole_type(
|
||||
map_id,
|
||||
source_system_id,
|
||||
linked_system_id,
|
||||
wormhole_type,
|
||||
char_id
|
||||
)
|
||||
|
||||
{:ok, _existing_conn} ->
|
||||
# Connection exists, update wormhole type if provided
|
||||
update_connection_wormhole_type(
|
||||
map_id,
|
||||
source_system_id,
|
||||
linked_system_id,
|
||||
wormhole_type
|
||||
)
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning(
|
||||
"[handle_linked_system] Failed to check connection: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
{:error, :connection_check_failed}
|
||||
end
|
||||
|
||||
{:error, :invalid_solar_system} ->
|
||||
Logger.warning(
|
||||
"[handle_linked_system] Invalid linked_system_id: #{linked_system_id} (not a valid EVE system)"
|
||||
)
|
||||
|
||||
{:error, :invalid_linked_system}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("[handle_linked_system] Failed to add linked system: #{inspect(reason)}")
|
||||
{:error, :linked_system_add_failed}
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a connection between two systems with the specified wormhole type
|
||||
@spec create_connection_with_wormhole_type(
|
||||
String.t(),
|
||||
integer(),
|
||||
integer(),
|
||||
String.t() | nil,
|
||||
String.t()
|
||||
) :: :ok | {:error, atom()}
|
||||
defp create_connection_with_wormhole_type(
|
||||
map_id,
|
||||
source_system_id,
|
||||
target_system_id,
|
||||
wormhole_type,
|
||||
char_id
|
||||
) do
|
||||
conn_attrs = %{
|
||||
"solar_system_source" => source_system_id,
|
||||
"solar_system_target" => target_system_id,
|
||||
"type" => 0,
|
||||
"wormhole_type" => wormhole_type
|
||||
}
|
||||
|
||||
case Connections.create(conn_attrs, map_id, char_id) do
|
||||
{:ok, :created} ->
|
||||
Logger.info(
|
||||
"[create_signature] Auto-created connection #{source_system_id} <-> #{target_system_id} (type: #{wormhole_type || "unknown"})"
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
{:skip, :exists} ->
|
||||
# Connection already exists (race condition), update it instead
|
||||
update_connection_wormhole_type(map_id, source_system_id, target_system_id, wormhole_type)
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"[create_connection_with_wormhole_type] Failed to create connection: #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:error, :connection_create_failed}
|
||||
end
|
||||
end
|
||||
|
||||
# Updates the wormhole type and ship size for an existing connection
|
||||
@spec update_connection_wormhole_type(String.t(), integer(), integer(), String.t() | nil) ::
|
||||
:ok | {:error, atom()}
|
||||
defp update_connection_wormhole_type(_map_id, _source, _target, nil), do: :ok
|
||||
|
||||
defp update_connection_wormhole_type(map_id, source_system_id, target_system_id, wormhole_type) do
|
||||
# Get ship size from wormhole type
|
||||
ship_size_type = EVEUtil.get_wh_size(wormhole_type)
|
||||
|
||||
if not is_nil(ship_size_type) do
|
||||
case Server.update_connection_ship_size_type(map_id, %{
|
||||
solar_system_source_id: source_system_id,
|
||||
solar_system_target_id: target_system_id,
|
||||
ship_size_type: ship_size_type
|
||||
}) do
|
||||
:ok ->
|
||||
Logger.info(
|
||||
"[create_signature] Updated connection #{source_system_id} <-> #{target_system_id} ship_size_type to #{ship_size_type} (wormhole: #{wormhole_type})"
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Logger.warning(
|
||||
"[update_connection_wormhole_type] Failed to update ship size: #{inspect(error)}"
|
||||
)
|
||||
|
||||
{:error, :ship_size_update_failed}
|
||||
end
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@spec update_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def update_signature(
|
||||
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
|
||||
@@ -249,4 +469,161 @@ defmodule WandererApp.Map.Operations.Signatures do
|
||||
end
|
||||
|
||||
def delete_signature(_conn, _sig_id), do: {:error, :missing_params}
|
||||
|
||||
@doc """
|
||||
Links a signature to a target system, creating the association between
|
||||
the signature and the wormhole connection to that system.
|
||||
|
||||
This also:
|
||||
- Updates the signature's group to "Wormhole"
|
||||
- Sets the target system's linked_sig_eve_id
|
||||
- Copies temporary_name from signature to target system
|
||||
- Updates connection time_status and ship_size_type from signature data
|
||||
"""
|
||||
@spec link_signature(Plug.Conn.t(), String.t(), map()) :: {:ok, map()} | {:error, atom()}
|
||||
def link_signature(
|
||||
%{assigns: %{map_id: map_id}} = _conn,
|
||||
sig_id,
|
||||
%{"solar_system_target" => solar_system_target}
|
||||
)
|
||||
when is_integer(solar_system_target) do
|
||||
with {:ok, signature} <- MapSystemSignature.by_id(sig_id),
|
||||
{:ok, source_system} <- MapSystem.by_id(signature.system_id),
|
||||
true <- source_system.map_id == map_id,
|
||||
target_system when not is_nil(target_system) <-
|
||||
WandererApp.Map.find_system_by_location(map_id, %{solar_system_id: solar_system_target}) do
|
||||
# Update signature group to Wormhole and set linked_system_id
|
||||
{:ok, updated_signature} =
|
||||
signature
|
||||
|> MapSystemSignature.update_group!(%{group: "Wormhole"})
|
||||
|> MapSystemSignature.update_linked_system(%{linked_system_id: solar_system_target})
|
||||
|
||||
# Only update target system if it doesn't already have a linked signature
|
||||
if is_nil(target_system.linked_sig_eve_id) do
|
||||
# Set the target system's linked_sig_eve_id
|
||||
Server.update_system_linked_sig_eve_id(map_id, %{
|
||||
solar_system_id: solar_system_target,
|
||||
linked_sig_eve_id: signature.eve_id
|
||||
})
|
||||
|
||||
# Copy temporary_name if present
|
||||
if not is_nil(signature.temporary_name) do
|
||||
Server.update_system_temporary_name(map_id, %{
|
||||
solar_system_id: solar_system_target,
|
||||
temporary_name: signature.temporary_name
|
||||
})
|
||||
end
|
||||
|
||||
# Update connection time_status from signature custom_info
|
||||
signature_time_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
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
if not is_nil(signature_time_status) do
|
||||
Server.update_connection_time_status(map_id, %{
|
||||
solar_system_source_id: source_system.solar_system_id,
|
||||
solar_system_target_id: solar_system_target,
|
||||
time_status: signature_time_status
|
||||
})
|
||||
end
|
||||
|
||||
# Update connection ship_size_type from signature wormhole type
|
||||
signature_ship_size_type = EVEUtil.get_wh_size(signature.type)
|
||||
|
||||
if not is_nil(signature_ship_size_type) do
|
||||
Server.update_connection_ship_size_type(map_id, %{
|
||||
solar_system_source_id: source_system.solar_system_id,
|
||||
solar_system_target_id: solar_system_target,
|
||||
ship_size_type: signature_ship_size_type
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
# Broadcast update
|
||||
Server.Impl.broadcast!(map_id, :signatures_updated, source_system.solar_system_id)
|
||||
|
||||
# Return the updated signature
|
||||
result =
|
||||
updated_signature
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, source_system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
{:ok, result}
|
||||
else
|
||||
false ->
|
||||
{:error, :not_found}
|
||||
|
||||
nil ->
|
||||
{:error, :target_system_not_found}
|
||||
|
||||
{:error, %Ash.Error.Query.NotFound{}} ->
|
||||
{:error, :not_found}
|
||||
|
||||
err ->
|
||||
Logger.error("[link_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def link_signature(_conn, _sig_id, %{"solar_system_target" => _}),
|
||||
do: {:error, :invalid_solar_system_target}
|
||||
|
||||
def link_signature(_conn, _sig_id, _params), do: {:error, :missing_params}
|
||||
|
||||
@doc """
|
||||
Unlinks a signature from its target system.
|
||||
"""
|
||||
@spec unlink_signature(Plug.Conn.t(), String.t()) :: {:ok, map()} | {:error, atom()}
|
||||
def unlink_signature(%{assigns: %{map_id: map_id}} = _conn, sig_id) do
|
||||
with {:ok, signature} <- MapSystemSignature.by_id(sig_id),
|
||||
{:ok, source_system} <- MapSystem.by_id(signature.system_id),
|
||||
:ok <- if(source_system.map_id == map_id, do: :ok, else: {:error, :not_found}),
|
||||
:ok <- if(not is_nil(signature.linked_system_id), do: :ok, else: {:error, :not_linked}) do
|
||||
# Clear the target system's linked_sig_eve_id
|
||||
Server.update_system_linked_sig_eve_id(map_id, %{
|
||||
solar_system_id: signature.linked_system_id,
|
||||
linked_sig_eve_id: nil
|
||||
})
|
||||
|
||||
# Clear the signature's linked_system_id using the wrapper for logging
|
||||
{:ok, updated_signature} =
|
||||
Server.SignaturesImpl.update_signature_linked_system(signature, %{
|
||||
linked_system_id: nil
|
||||
})
|
||||
|
||||
# Broadcast update
|
||||
Server.Impl.broadcast!(map_id, :signatures_updated, source_system.solar_system_id)
|
||||
|
||||
# Return the updated signature
|
||||
result =
|
||||
updated_signature
|
||||
|> Map.from_struct()
|
||||
|> Map.put(:solar_system_id, source_system.solar_system_id)
|
||||
|> Map.drop([:system_id, :__meta__, :system, :aggregates, :calculations])
|
||||
|
||||
{:ok, result}
|
||||
else
|
||||
{:error, :not_found} ->
|
||||
{:error, :not_found}
|
||||
|
||||
{:error, :not_linked} ->
|
||||
{:error, :not_linked}
|
||||
|
||||
{:error, %Ash.Error.Query.NotFound{}} ->
|
||||
{:error, :not_found}
|
||||
|
||||
err ->
|
||||
Logger.error("[unlink_signature] Unexpected error: #{inspect(err)}")
|
||||
{:error, :unexpected_error}
|
||||
end
|
||||
end
|
||||
|
||||
def unlink_signature(_conn, _sig_id), do: {:error, :missing_params}
|
||||
end
|
||||
|
||||
@@ -36,7 +36,8 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
# Private helper for batch upsert
|
||||
defp create_system_batch(%{map_id: map_id, user_id: user_id, char_id: char_id}, params) do
|
||||
with {:ok, solar_system_id} <- fetch_system_id(params) do
|
||||
update_existing = fetch_update_existing(params, false)
|
||||
# Default to true so re-submitting with new position updates the system
|
||||
update_existing = fetch_update_existing(params, true)
|
||||
|
||||
map_id
|
||||
|> WandererApp.Map.check_location(%{solar_system_id: solar_system_id})
|
||||
@@ -46,9 +47,13 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
|
||||
{:error, :already_exists} ->
|
||||
if update_existing do
|
||||
do_update_system(map_id, user_id, char_id, solar_system_id, params)
|
||||
# Mark as skip so it counts as "updated" not "created"
|
||||
case do_update_system(map_id, user_id, char_id, solar_system_id, params) do
|
||||
{:ok, _} -> {:skip, :updated}
|
||||
error -> error
|
||||
end
|
||||
else
|
||||
:ok
|
||||
{:skip, :already_exists}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -200,16 +205,22 @@ defmodule WandererApp.Map.Operations.Systems do
|
||||
|
||||
defp normalize_coordinates(%{"coordinates" => %{"x" => x, "y" => y}})
|
||||
when is_number(x) and is_number(y),
|
||||
do: %{x: x, y: y}
|
||||
do: %{"x" => x, "y" => y}
|
||||
|
||||
defp normalize_coordinates(%{coordinates: %{x: x, y: y}}) when is_number(x) and is_number(y),
|
||||
do: %{x: x, y: y}
|
||||
do: %{"x" => x, "y" => y}
|
||||
|
||||
defp normalize_coordinates(params) do
|
||||
%{
|
||||
x: params |> Map.get("position_x", Map.get(params, :position_x, 0)),
|
||||
y: params |> Map.get("position_y", Map.get(params, :position_y, 0))
|
||||
}
|
||||
x = params |> Map.get("position_x", Map.get(params, :position_x))
|
||||
y = params |> Map.get("position_y", Map.get(params, :position_y))
|
||||
|
||||
# Only return coordinates if both x and y are provided
|
||||
# Otherwise return nil to let the server use auto-positioning
|
||||
if is_number(x) and is_number(y) do
|
||||
%{"x" => x, "y" => y}
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
defp apply_system_updates(map_id, system_id, attrs, %{x: x, y: y}) do
|
||||
|
||||
@@ -595,6 +595,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
time_status = get_extra_info(extra_info, "time_status", time_status)
|
||||
mass_status = get_extra_info(extra_info, "mass_status", 0)
|
||||
locked = get_extra_info(extra_info, "locked", false)
|
||||
wormhole_type = get_extra_info(extra_info, "wormhole_type", nil)
|
||||
|
||||
{:ok, connection} =
|
||||
WandererApp.MapConnectionRepo.create(%{
|
||||
@@ -605,7 +606,8 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
ship_size_type: ship_size_type,
|
||||
time_status: time_status,
|
||||
mass_status: mass_status,
|
||||
locked: locked
|
||||
locked: locked,
|
||||
wormhole_type: wormhole_type
|
||||
})
|
||||
|
||||
if connection_type == @connection_type_wormhole do
|
||||
@@ -915,8 +917,10 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
if not from_is_wormhole and not to_is_wormhole do
|
||||
# Check if there's a known stargate
|
||||
case find_solar_system_jump(from_solar_system_id, to_solar_system_id) do
|
||||
{:ok, []} -> true # No stargate = wormhole connection
|
||||
_ -> false # Stargate exists or error
|
||||
# No stargate = wormhole connection
|
||||
{:ok, []} -> true
|
||||
# Stargate exists or error
|
||||
_ -> false
|
||||
end
|
||||
else
|
||||
false
|
||||
|
||||
@@ -72,7 +72,6 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
type: type
|
||||
} = _ping_info
|
||||
) do
|
||||
|
||||
result = WandererApp.MapPingsRepo.get_by_id(ping_id)
|
||||
|
||||
case result do
|
||||
|
||||
@@ -109,8 +109,10 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
nil ->
|
||||
MapSystemSignature.create!(sig)
|
||||
|
||||
_ ->
|
||||
:noop
|
||||
existing ->
|
||||
# If signature already exists, update it instead of ignoring
|
||||
# This handles the case where frontend sends existing sigs as "added"
|
||||
apply_update_signature(map_id, existing, sig)
|
||||
end
|
||||
end)
|
||||
|
||||
@@ -167,6 +169,9 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
updated_count: length(updated_ids),
|
||||
removed_count: length(removed_ids)
|
||||
})
|
||||
|
||||
# Always return :ok - external event failures should not affect the main operation
|
||||
:ok
|
||||
end
|
||||
|
||||
defp remove_signature(map_id, sig, system, delete_conn?) do
|
||||
@@ -324,6 +329,7 @@ defmodule WandererApp.Map.Server.SignaturesImpl do
|
||||
group: sig["group"],
|
||||
type: Map.get(sig, "type"),
|
||||
custom_info: Map.get(sig, "custom_info"),
|
||||
linked_system_id: Map.get(sig, "linked_system_id"),
|
||||
# Use character_eve_id from sig if provided, otherwise use the default
|
||||
character_eve_id: Map.get(sig, "character_eve_id", character_eve_id),
|
||||
deleted: false
|
||||
|
||||
@@ -41,14 +41,18 @@
|
||||
<div class="absolute rounded-m top-0 left-0 w-full h-full bg-gradient-to-b from-transparent to-black opacity-75 group-hover:opacity-25 transition-opacity duration-300">
|
||||
</div>
|
||||
<div class="absolute w-full bottom-2 p-4">
|
||||
<% {first_part, second_part} = case String.split(post.title, ":", parts: 2) do
|
||||
[first, second] -> {first, second}
|
||||
[first] -> {first, nil}
|
||||
end %>
|
||||
<% {first_part, second_part} =
|
||||
case String.split(post.title, ":", parts: 2) do
|
||||
[first, second] -> {first, second}
|
||||
[first] -> {first, nil}
|
||||
end %>
|
||||
<h3 class="!m-0 !text-s font-bold break-normal ccp-font whitespace-nowrap text-white">
|
||||
{first_part}
|
||||
</h3>
|
||||
<p :if={second_part} class="!m-0 !text-s text-white text-ellipsis overflow-hidden whitespace-nowrap ccp-font">
|
||||
<p
|
||||
:if={second_part}
|
||||
class="!m-0 !text-s text-white text-ellipsis overflow-hidden whitespace-nowrap ccp-font"
|
||||
>
|
||||
{second_part}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -487,10 +487,17 @@ defmodule WandererAppWeb.MapSystemAPIController do
|
||||
)
|
||||
|
||||
def create(conn, params) do
|
||||
# Support both batch format {"systems": [...], "connections": [...]}
|
||||
# and single system format {"solar_system_id": ..., ...}
|
||||
# Support multiple formats:
|
||||
# 1. Batch format: {"systems": [...], "connections": [...]}
|
||||
# 2. Wrapped batch format: {"data": {"systems": [...], "connections": [...]}}
|
||||
# 3. Single system format: {"solar_system_id": ..., ...}
|
||||
{systems, connections} =
|
||||
cond do
|
||||
Map.has_key?(params, "data") and is_map(params["data"]) ->
|
||||
# Wrapped batch format - extract from data wrapper
|
||||
data = params["data"]
|
||||
{Map.get(data, "systems", []), Map.get(data, "connections", [])}
|
||||
|
||||
Map.has_key?(params, "systems") ->
|
||||
# Batch format
|
||||
{Map.get(params, "systems", []), Map.get(params, "connections", [])}
|
||||
|
||||
@@ -190,9 +190,37 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
The `character_eve_id` field is optional. If provided, it must be a valid character
|
||||
that exists in the database, otherwise a 422 error will be returned. If not provided,
|
||||
the signature will be associated with the map owner's character.
|
||||
|
||||
## Auto-add System Behavior
|
||||
|
||||
If the `solar_system_id` is not already on the map, it will be automatically added.
|
||||
The system must be a valid EVE Online solar system ID.
|
||||
|
||||
## Linked System and Connection Behavior
|
||||
|
||||
If `linked_system_id` is provided (for wormhole signatures):
|
||||
- The linked system will be automatically added to the map if not present
|
||||
- A connection will be created between the source and linked systems if one doesn't exist
|
||||
- If a connection already exists, its ship size will be updated based on the wormhole `type`
|
||||
- The wormhole `type` (e.g., "H296", "C2", "K162") is used to determine connection ship size:
|
||||
- H296 → XL/Freighter size (1B kg max mass)
|
||||
- N770, D845 → Large size (375M kg max mass)
|
||||
- etc.
|
||||
"""
|
||||
operation(:create,
|
||||
summary: "Create a new signature",
|
||||
description: """
|
||||
Creates a new cosmic signature in the specified solar system.
|
||||
|
||||
**Auto-add behavior**: If the solar_system_id is not already on the map, it will be
|
||||
automatically added. The system must be a valid EVE Online solar system ID.
|
||||
|
||||
**Linked system behavior**: If linked_system_id is provided:
|
||||
- The linked system is auto-added to the map if not present
|
||||
- A wormhole connection is auto-created between the systems
|
||||
- The connection's ship_size_type is inferred from the wormhole type (e.g., H296 → XL)
|
||||
- If the connection already exists, its ship size is updated based on the wormhole type
|
||||
""",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
@@ -218,7 +246,7 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
error: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
description:
|
||||
"Error type (e.g., 'invalid_character', 'system_not_found', 'missing_params')"
|
||||
"Error type (e.g., 'invalid_character', 'invalid_solar_system', 'missing_params')"
|
||||
}
|
||||
},
|
||||
example: %{error: "invalid_character"}
|
||||
@@ -311,4 +339,117 @@ defmodule WandererAppWeb.MapSystemSignatureAPIController do
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Link a signature to a target system.
|
||||
|
||||
This creates the association between a wormhole signature and the system it leads to.
|
||||
It also updates the connection's time_status and ship_size_type based on the signature data.
|
||||
"""
|
||||
operation(:link,
|
||||
summary: "Link a signature to a target system",
|
||||
description: """
|
||||
Links a wormhole signature to its destination system. This operation:
|
||||
- Sets the signature's linked_system_id to the target system
|
||||
- Updates the signature's group to "Wormhole"
|
||||
- Sets the target system's linked_sig_eve_id (if not already set)
|
||||
- Copies temporary_name from signature to target system
|
||||
- Updates the connection's time_status and ship_size_type from signature data
|
||||
""",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true
|
||||
],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
request_body:
|
||||
{"Link request", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
solar_system_target: %OpenApiSpex.Schema{
|
||||
type: :integer,
|
||||
description: "Target solar system ID to link to"
|
||||
}
|
||||
},
|
||||
required: [:solar_system_target],
|
||||
example: %{solar_system_target: 31_001_922}
|
||||
}},
|
||||
responses: [
|
||||
ok:
|
||||
{"Linked signature", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: @signature_schema.example}
|
||||
}},
|
||||
unprocessable_entity:
|
||||
{"Error", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
description: "Error type"
|
||||
}
|
||||
},
|
||||
example: %{error: "target_system_not_found"}
|
||||
}}
|
||||
]
|
||||
)
|
||||
|
||||
def link(conn, %{"id" => id} = params) do
|
||||
case MapOperations.link_signature(conn, id, params) do
|
||||
{:ok, sig} -> json(conn, %{data: sig})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unlink a signature from its target system.
|
||||
"""
|
||||
operation(:unlink,
|
||||
summary: "Unlink a signature from its target system",
|
||||
description: "Removes the link between a signature and its destination system.",
|
||||
parameters: [
|
||||
map_identifier: [
|
||||
in: :path,
|
||||
description: "Map identifier (UUID or slug)",
|
||||
type: :string,
|
||||
required: true
|
||||
],
|
||||
id: [in: :path, description: "Signature UUID", type: :string, required: true]
|
||||
],
|
||||
responses: [
|
||||
ok:
|
||||
{"Unlinked signature", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{data: @signature_schema},
|
||||
example: %{data: Map.put(@signature_schema.example, :linked_system_id, nil)}
|
||||
}},
|
||||
unprocessable_entity:
|
||||
{"Error", "application/json",
|
||||
%OpenApiSpex.Schema{
|
||||
type: :object,
|
||||
properties: %{
|
||||
error: %OpenApiSpex.Schema{
|
||||
type: :string,
|
||||
description: "Error type"
|
||||
}
|
||||
},
|
||||
example: %{error: "not_linked"}
|
||||
}}
|
||||
]
|
||||
)
|
||||
|
||||
def unlink(conn, %{"id" => id}) do
|
||||
case MapOperations.unlink_signature(conn, id) do
|
||||
{:ok, sig} -> json(conn, %{data: sig})
|
||||
{:error, error} -> conn |> put_status(:unprocessable_entity) |> json(%{error: error})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -34,7 +34,9 @@
|
||||
<.icon name="hero-gift-solid" class="w-4 h-4 text-green-400 flex-shrink-0" />
|
||||
<span class="text-sm text-gray-300">
|
||||
Support development by using promocode
|
||||
<code class="ml-1 px-1.5 py-0.5 bg-stone-800 rounded text-green-400 text-xs font-mono">WANDERER</code>
|
||||
<code class="ml-1 px-1.5 py-0.5 bg-stone-800 rounded text-green-400 text-xs font-mono">
|
||||
WANDERER
|
||||
</code>
|
||||
<span class="ml-1">at official</span>
|
||||
</span>
|
||||
<a
|
||||
|
||||
@@ -299,6 +299,8 @@ defmodule WandererAppWeb.Router do
|
||||
resources "/structures", MapSystemStructureAPIController, except: [:new, :edit]
|
||||
get "/structure-timers", MapSystemStructureAPIController, :structure_timers
|
||||
resources "/signatures", MapSystemSignatureAPIController, except: [:new, :edit]
|
||||
post "/signatures/:id/link", MapSystemSignatureAPIController, :link
|
||||
delete "/signatures/:id/link", MapSystemSignatureAPIController, :unlink
|
||||
get "/user-characters", MapAPIController, :show_user_characters
|
||||
get "/tracked-characters", MapAPIController, :show_tracked_characters
|
||||
end
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -3,7 +3,7 @@ defmodule WandererApp.MixProject do
|
||||
|
||||
@source_url "https://github.com/wanderer-industries/wanderer"
|
||||
|
||||
@version "1.91.10"
|
||||
@version "1.92.0"
|
||||
|
||||
def project do
|
||||
[
|
||||
|
||||
@@ -26,9 +26,9 @@ defmodule WandererApp.Map.CorporationChangePermissionTest do
|
||||
|
||||
setup :verify_on_exit!
|
||||
|
||||
@test_corp_id_a 98000001
|
||||
@test_corp_id_b 98000002
|
||||
@test_alliance_id_a 99000001
|
||||
@test_corp_id_a 98_000_001
|
||||
@test_corp_id_b 98_000_002
|
||||
@test_alliance_id_a 99_000_001
|
||||
|
||||
setup do
|
||||
# Configure the PubSubMock to forward to real Phoenix.PubSub for broadcast testing
|
||||
@@ -70,7 +70,8 @@ defmodule WandererApp.Map.CorporationChangePermissionTest do
|
||||
simulate_corporation_change(character, @test_corp_id_b)
|
||||
|
||||
# Should receive :update_permissions broadcast
|
||||
assert_receive :update_permissions, 1000,
|
||||
assert_receive :update_permissions,
|
||||
1000,
|
||||
"Should receive :update_permissions when corporation changes"
|
||||
end
|
||||
|
||||
@@ -94,7 +95,8 @@ defmodule WandererApp.Map.CorporationChangePermissionTest do
|
||||
simulate_alliance_removal(character)
|
||||
|
||||
# Should receive :update_permissions broadcast
|
||||
assert_receive :update_permissions, 1000,
|
||||
assert_receive :update_permissions,
|
||||
1000,
|
||||
"Should receive :update_permissions when alliance is removed"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,6 +116,7 @@ defmodule WandererApp.Map.Server.AclScopesPropagationTest do
|
||||
|
||||
# Fetch again to confirm persistence
|
||||
{:ok, refetched_map} = WandererApp.MapRepo.get(map.id, [])
|
||||
|
||||
assert refetched_map.scopes == [:wormholes, :hi, :low, :null],
|
||||
"Refetched map should have updated scopes"
|
||||
end
|
||||
|
||||
@@ -577,35 +577,55 @@ defmodule WandererApp.Map.Server.MapScopesTest do
|
||||
# All should be valid because no stargates exist in test data = wormhole connections
|
||||
|
||||
# Hi-Sec combinations
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @ls_system_id) == true,
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @ls_system_id) ==
|
||||
true,
|
||||
"Hi->Low should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @ns_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @ns_system_id) ==
|
||||
true,
|
||||
"Hi->Null should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @pochven_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @hs_system_id, @pochven_id) ==
|
||||
true,
|
||||
"Hi->Pochven should be valid"
|
||||
|
||||
# Low-Sec combinations
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @hs_system_id) == true,
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @hs_system_id) ==
|
||||
true,
|
||||
"Low->Hi should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @ns_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @ns_system_id) ==
|
||||
true,
|
||||
"Low->Null should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @pochven_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ls_system_id, @pochven_id) ==
|
||||
true,
|
||||
"Low->Pochven should be valid"
|
||||
|
||||
# Null-Sec combinations
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @hs_system_id) == true,
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @hs_system_id) ==
|
||||
true,
|
||||
"Null->Hi should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @ls_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @ls_system_id) ==
|
||||
true,
|
||||
"Null->Low should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @pochven_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @ns_system_id, @pochven_id) ==
|
||||
true,
|
||||
"Null->Pochven should be valid"
|
||||
|
||||
# Pochven combinations
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @hs_system_id) == true,
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @hs_system_id) ==
|
||||
true,
|
||||
"Pochven->Hi should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @ls_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @ls_system_id) ==
|
||||
true,
|
||||
"Pochven->Low should be valid"
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @ns_system_id) == true,
|
||||
|
||||
assert ConnectionsImpl.is_connection_valid([:wormholes], @pochven_id, @ns_system_id) ==
|
||||
true,
|
||||
"Pochven->Null should be valid"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -464,9 +464,8 @@ defmodule WandererApp.Map.Operations.SignaturesTest do
|
||||
Task.async(fn ->
|
||||
params = %{"solar_system_id" => 30_000_140 + i}
|
||||
result = Signatures.create_signature(conn, params)
|
||||
# We expect either system_not_found (system doesn't exist in test)
|
||||
# or the MapTestHelpers would have caught the map server error
|
||||
assert {:error, :system_not_found} = result
|
||||
# Fake solar_system_ids aren't in EVE static data, so we get :invalid_solar_system
|
||||
assert {:error, :invalid_solar_system} = result
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user