mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-01-14 02:41:07 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
922f296f17 | ||
|
|
71dc20c933 | ||
|
|
80f7d34d3d | ||
|
|
113fe1c695 | ||
|
|
0228e68a1d | ||
|
|
3424667af1 | ||
|
|
6c7b28a6c1 | ||
|
|
3988079cd3 | ||
|
|
f5d407fee0 | ||
|
|
a857422c46 | ||
|
|
ec6717d0ef | ||
|
|
56dacdcbbd | ||
|
|
c8e17b1691 | ||
|
|
19c7fe59ee | ||
|
|
682100c231 | ||
|
|
f9ac79cdcc | ||
|
|
f09f220645 |
36
CHANGELOG.md
36
CHANGELOG.md
@@ -2,6 +2,42 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* remove actor context requirement from sig api
|
||||
|
||||
## [v1.91.9](https://github.com/wanderer-industries/wanderer/compare/v1.91.8...v1.91.9) (2026-01-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed rally point cancel logic
|
||||
|
||||
## [v1.91.8](https://github.com/wanderer-industries/wanderer/compare/v1.91.7...v1.91.8) (2026-01-06)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed rally point cancel logic
|
||||
|
||||
## [v1.91.7](https://github.com/wanderer-industries/wanderer/compare/v1.91.6...v1.91.7) (2026-01-05)
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
|
||||
const {
|
||||
storedSettings: { interfaceSettings },
|
||||
data: { systemSignatures: mapSystemSignatures },
|
||||
data: { systemSignatures: mapSystemSignatures, pings },
|
||||
} = useMapRootState();
|
||||
|
||||
const systemStaticInfo = useMemo(() => {
|
||||
@@ -108,7 +108,6 @@ export const useSolarSystemNode = (props: NodeProps<MapSolarSystemType>): SolarS
|
||||
visibleNodes,
|
||||
showKSpaceBG,
|
||||
isThickConnections,
|
||||
pings,
|
||||
systemHighlighted,
|
||||
},
|
||||
outCommand,
|
||||
|
||||
@@ -121,6 +121,7 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!ping) {
|
||||
setIsShow(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,27 +162,26 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
};
|
||||
}, [interfaceSettings]);
|
||||
|
||||
if (!ping) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isShowSelectedSystem = selectedSystem != null && selectedSystem !== ping.solar_system_id;
|
||||
const isShowSelectedSystem = ping && selectedSystem != null && selectedSystem !== ping.solar_system_id;
|
||||
|
||||
// Only render Toast when there's a ping
|
||||
return (
|
||||
<>
|
||||
<Toast
|
||||
position={placement as never}
|
||||
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
|
||||
ref={toast}
|
||||
content={({ message }) => (
|
||||
<section
|
||||
className={clsx(
|
||||
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
|
||||
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
|
||||
{ping && (
|
||||
<Toast
|
||||
key={ping.id}
|
||||
position={placement as never}
|
||||
className={clsx('!max-w-[initial] w-[500px]', hasLeftOffset ? offsets.withLeftMenu : offsets.default)}
|
||||
ref={toast}
|
||||
content={({ message }) => (
|
||||
<section
|
||||
className={clsx(
|
||||
'flex flex-col p-3 w-full border border-stone-800 shadow-md animate-fadeInDown rounded-[5px]',
|
||||
'bg-gradient-to-tr from-transparent to-sky-700/60 bg-stone-900/70',
|
||||
)}
|
||||
>
|
||||
<div className="flex gap-3">
|
||||
<i className={clsx('pi text-yellow-500 text-2xl', 'relative top-[2px]', ICONS[ping.type])}></i>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex justify-between">
|
||||
<div>
|
||||
@@ -253,28 +253,33 @@ export const PingsInterface = ({ hasLeftOffset }: PingsInterfaceProps) => {
|
||||
{/*/>*/}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
></Toast>
|
||||
)}
|
||||
></Toast>
|
||||
)}
|
||||
|
||||
<WdButton
|
||||
icon="pi pi-bell"
|
||||
severity="warning"
|
||||
aria-label="Notification"
|
||||
size="small"
|
||||
className="w-[33px] h-[33px]"
|
||||
outlined
|
||||
onClick={handleClickShow}
|
||||
disabled={isShow}
|
||||
/>
|
||||
{ping && (
|
||||
<>
|
||||
<WdButton
|
||||
icon="pi pi-bell"
|
||||
severity="warning"
|
||||
aria-label="Notification"
|
||||
size="small"
|
||||
className="w-[33px] h-[33px]"
|
||||
outlined
|
||||
onClick={handleClickShow}
|
||||
disabled={isShow}
|
||||
/>
|
||||
|
||||
<ConfirmPopup
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="Are you sure you want to delete ping?"
|
||||
icon="pi pi-exclamation-triangle text-orange-400"
|
||||
accept={removePing}
|
||||
/>
|
||||
<ConfirmPopup
|
||||
target={cfRef.current}
|
||||
visible={cfVisible}
|
||||
onHide={cfHide}
|
||||
message="Are you sure you want to delete ping?"
|
||||
icon="pi pi-exclamation-triangle text-orange-400"
|
||||
accept={removePing}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,3 +10,4 @@ export * from './useCommandComments';
|
||||
export * from './useGetCacheCharacter';
|
||||
export * from './useCommandsActivity';
|
||||
export * from './useCommandPings';
|
||||
export * from './useCommandPingBlocked';
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useToast } from '@/hooks/Mapper/ToastProvider';
|
||||
import { CommandPingBlocked } from '@/hooks/Mapper/types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useCommandPingBlocked = () => {
|
||||
const { show } = useToast();
|
||||
|
||||
const pingBlocked = useCallback(
|
||||
({ message }: CommandPingBlocked) => {
|
||||
show({
|
||||
severity: 'warn',
|
||||
summary: 'Cannot create ping',
|
||||
detail: message,
|
||||
life: 5000,
|
||||
});
|
||||
},
|
||||
[show],
|
||||
);
|
||||
|
||||
return { pingBlocked };
|
||||
};
|
||||
@@ -14,8 +14,8 @@ export const useCommandPings = () => {
|
||||
ref.current.update({ pings });
|
||||
}, []);
|
||||
|
||||
const pingCancelled = useCallback(({ type, id }: CommandPingCancelled) => {
|
||||
const newPings = ref.current.pings.filter(x => x.id !== id && x.type !== type);
|
||||
const pingCancelled = useCallback(({ id }: CommandPingCancelled) => {
|
||||
const newPings = ref.current.pings.filter(x => x.id !== id);
|
||||
ref.current.update({ pings: newPings });
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -63,7 +63,6 @@ export const useComments = ({ outCommand }: UseCommentsProps): UseCommentsData =
|
||||
|
||||
const removeComment = useCallback((systemId: number, commentId: string) => {
|
||||
const cSystem = commentBySystemsRef.current.get(systemId);
|
||||
console.log('cSystem', cSystem);
|
||||
if (!cSystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CommandLinkSignatureToSystem,
|
||||
CommandMapUpdated,
|
||||
CommandPingAdded,
|
||||
CommandPingBlocked,
|
||||
CommandPingCancelled,
|
||||
CommandPresentCharacters,
|
||||
CommandRemoveConnections,
|
||||
@@ -29,6 +30,7 @@ import { ForwardedRef, useImperativeHandle } from 'react';
|
||||
|
||||
import {
|
||||
useCommandComments,
|
||||
useCommandPingBlocked,
|
||||
useCommandPings,
|
||||
useCommandsCharacters,
|
||||
useCommandsConnections,
|
||||
@@ -61,6 +63,7 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
const mapUserRoutes = useUserRoutes();
|
||||
const { addComment, removeComment } = useCommandComments();
|
||||
const { pingAdded, pingCancelled } = useCommandPings();
|
||||
const { pingBlocked } = useCommandPingBlocked();
|
||||
const { characterActivityData, trackingCharactersData, userSettingsUpdated } = useCommandsActivity();
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
@@ -172,6 +175,9 @@ export const useMapRootHandlers = (ref: ForwardedRef<MapHandlers>) => {
|
||||
case Commands.pingCancelled:
|
||||
pingCancelled(data as CommandPingCancelled);
|
||||
break;
|
||||
case Commands.pingBlocked:
|
||||
pingBlocked(data as CommandPingBlocked);
|
||||
break;
|
||||
default:
|
||||
console.warn(`JOipP Interface handlers: Unknown command: ${type}`, data);
|
||||
break;
|
||||
|
||||
@@ -41,6 +41,7 @@ export enum Commands {
|
||||
refreshTrackingData = 'refresh_tracking_data',
|
||||
pingAdded = 'ping_added',
|
||||
pingCancelled = 'ping_cancelled',
|
||||
pingBlocked = 'ping_blocked',
|
||||
}
|
||||
|
||||
export type Command =
|
||||
@@ -77,7 +78,8 @@ export type Command =
|
||||
| Commands.showTracking
|
||||
| Commands.refreshTrackingData
|
||||
| Commands.pingAdded
|
||||
| Commands.pingCancelled;
|
||||
| Commands.pingCancelled
|
||||
| Commands.pingBlocked;
|
||||
|
||||
export type CommandInit = {
|
||||
systems: SolarSystemRawType[];
|
||||
@@ -161,6 +163,10 @@ export type CommandUpdateTracking = {
|
||||
};
|
||||
export type CommandPingAdded = PingData[];
|
||||
export type CommandPingCancelled = Pick<PingData, 'type' | 'id'>;
|
||||
export type CommandPingBlocked = {
|
||||
reason: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface UserSettings {
|
||||
primaryCharacterId?: string;
|
||||
@@ -212,6 +218,7 @@ export interface CommandData {
|
||||
[Commands.refreshTrackingData]: CommandRefreshTrackingData;
|
||||
[Commands.pingAdded]: CommandPingAdded;
|
||||
[Commands.pingCancelled]: CommandPingCancelled;
|
||||
[Commands.pingBlocked]: CommandPingBlocked;
|
||||
}
|
||||
|
||||
export interface MapHandlers {
|
||||
|
||||
@@ -80,6 +80,10 @@ defmodule WandererApp.Api.MapPing do
|
||||
|
||||
filter(expr(inserted_at <= ^arg(:inserted_before)))
|
||||
end
|
||||
|
||||
# Admin action for cleanup - no actor filtering
|
||||
read :all_pings do
|
||||
end
|
||||
end
|
||||
|
||||
attributes do
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule WandererApp.Map.Manager do
|
||||
@maps_queue :maps_queue
|
||||
@check_maps_queue_interval :timer.seconds(1)
|
||||
|
||||
@pings_cleanup_interval :timer.minutes(10)
|
||||
@pings_cleanup_interval :timer.minutes(5)
|
||||
@pings_expire_minutes 60
|
||||
|
||||
# Test-aware async task runner
|
||||
@@ -99,6 +99,7 @@ defmodule WandererApp.Map.Manager do
|
||||
def handle_info(:cleanup_pings, state) do
|
||||
try do
|
||||
cleanup_expired_pings()
|
||||
cleanup_orphaned_pings()
|
||||
{:noreply, state}
|
||||
rescue
|
||||
e ->
|
||||
@@ -141,6 +142,51 @@ defmodule WandererApp.Map.Manager do
|
||||
end
|
||||
end
|
||||
|
||||
defp cleanup_orphaned_pings() do
|
||||
case WandererApp.MapPingsRepo.get_orphaned_pings() do
|
||||
{:ok, []} ->
|
||||
:ok
|
||||
|
||||
{:ok, orphaned_pings} ->
|
||||
Logger.info(
|
||||
"[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 ->
|
||||
reason =
|
||||
cond do
|
||||
is_nil(ping.system) -> "system deleted"
|
||||
is_nil(ping.character) -> "character deleted"
|
||||
is_nil(ping.map) -> "map deleted"
|
||||
not is_nil(system) and system.visible == false -> "system hidden (visible=false)"
|
||||
true -> "unknown"
|
||||
end
|
||||
|
||||
Logger.warning(
|
||||
"[cleanup_orphaned_pings] Destroying orphaned ping #{ping_id} (map_id: #{map_id}, reason: #{reason})"
|
||||
)
|
||||
|
||||
# Broadcast cancellation if map_id is still valid
|
||||
if map_id do
|
||||
Server.Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: nil,
|
||||
type: type
|
||||
})
|
||||
end
|
||||
|
||||
Ash.destroy!(ping)
|
||||
end)
|
||||
|
||||
Logger.info("[cleanup_orphaned_pings] Cleaned up #{length(orphaned_pings)} orphaned pings")
|
||||
:ok
|
||||
|
||||
{:error, error} ->
|
||||
Logger.error("Failed to fetch orphaned pings: #{inspect(error)}")
|
||||
{:error, error}
|
||||
end
|
||||
end
|
||||
|
||||
defp start_maps() do
|
||||
chunks =
|
||||
@maps_queue
|
||||
|
||||
@@ -78,7 +78,8 @@ 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.by_map_id_and_solar_system_id(map_id, solar_system_id) do
|
||||
{:ok, system} <-
|
||||
MapSystem.read_by_map_and_solar_system(%{map_id: map_id, solar_system_id: solar_system_id}) do
|
||||
attrs =
|
||||
params
|
||||
|> Map.put("system_id", system.id)
|
||||
|
||||
@@ -72,17 +72,24 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
type: type
|
||||
} = _ping_info
|
||||
) do
|
||||
case WandererApp.MapPingsRepo.get_by_id(ping_id) do
|
||||
|
||||
result = WandererApp.MapPingsRepo.get_by_id(ping_id)
|
||||
|
||||
case result do
|
||||
{:ok,
|
||||
%{system: %{id: system_id, name: system_name, solar_system_id: solar_system_id}} = ping} ->
|
||||
with {:ok, character} <- WandererApp.Character.get_character(character_id),
|
||||
:ok <- WandererApp.MapPingsRepo.destroy(ping) do
|
||||
Logger.debug("Ping #{ping_id} destroyed successfully, broadcasting :ping_cancelled")
|
||||
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: solar_system_id,
|
||||
type: type
|
||||
})
|
||||
|
||||
Logger.debug("Broadcast :ping_cancelled sent for ping #{ping_id}")
|
||||
|
||||
# Broadcast rally point removal events to external clients (webhooks/SSE)
|
||||
if type == 1 do
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_removed, %{
|
||||
@@ -107,18 +114,45 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
Logger.error("Failed to destroy ping: #{inspect(error, pretty: true)}")
|
||||
end
|
||||
|
||||
# Handle case where ping exists but system was deleted (nil)
|
||||
{:ok, %{system: nil} = ping} ->
|
||||
case WandererApp.MapPingsRepo.destroy(ping) do
|
||||
:ok ->
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: nil,
|
||||
type: type
|
||||
})
|
||||
|
||||
error ->
|
||||
Logger.error("Failed to destroy orphaned ping: #{inspect(error, pretty: true)}")
|
||||
end
|
||||
|
||||
{:error, %Ash.Error.Query.NotFound{}} ->
|
||||
# Ping already deleted (possibly by cascade deletion from map/system/character removal,
|
||||
# auto-expiry, or concurrent cancellation). This is not an error - the desired state
|
||||
# (ping is gone) is already achieved. Just broadcast the cancellation event.
|
||||
Logger.debug(
|
||||
"Ping #{ping_id} not found during cancellation - already deleted, skipping broadcast"
|
||||
)
|
||||
# auto-expiry, or concurrent cancellation). Broadcast cancellation so frontend updates.
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: nil,
|
||||
type: type
|
||||
})
|
||||
|
||||
:ok
|
||||
|
||||
error ->
|
||||
Logger.error("Failed to fetch ping for cancellation: #{inspect(error, pretty: true)}")
|
||||
{:error, %Ash.Error.Invalid{errors: [%Ash.Error.Query.NotFound{} | _]}} ->
|
||||
# Same as above, but Ash wraps NotFound inside Invalid in some cases
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: nil,
|
||||
type: type
|
||||
})
|
||||
|
||||
:ok
|
||||
|
||||
other ->
|
||||
Logger.error(
|
||||
"Failed to cancel ping #{ping_id}: unexpected result from get_by_id: #{inspect(other, pretty: true)}"
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -167,6 +167,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
|
||||
|
||||
@@ -29,6 +29,34 @@ defmodule WandererApp.MapPingsRepo do
|
||||
def get_by_inserted_before(inserted_before_date),
|
||||
do: WandererApp.Api.MapPing.by_inserted_before(inserted_before_date)
|
||||
|
||||
@doc """
|
||||
Returns all pings that have orphaned relationships (nil system, character, or map)
|
||||
or where the system has been soft-deleted (visible = false).
|
||||
These pings should be cleaned up as they can no longer be properly displayed or cancelled.
|
||||
"""
|
||||
def get_orphaned_pings() do
|
||||
# Use :all_pings action which has no actor filtering (unlike primary :read)
|
||||
case WandererApp.Api.MapPing |> Ash.Query.for_read(:all_pings) |> Ash.read() do
|
||||
{:ok, pings} ->
|
||||
# Load relationships and filter for orphaned ones
|
||||
orphaned =
|
||||
pings
|
||||
|> Enum.map(fn ping ->
|
||||
{:ok, loaded} = ping |> Ash.load([:system, :character, :map], authorize?: false)
|
||||
loaded
|
||||
end)
|
||||
|> Enum.filter(fn ping ->
|
||||
is_nil(ping.system) or is_nil(ping.character) or is_nil(ping.map) or
|
||||
(not is_nil(ping.system) and ping.system.visible == false)
|
||||
end)
|
||||
|
||||
{:ok, orphaned}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
def create(ping), do: ping |> WandererApp.Api.MapPing.new()
|
||||
def create!(ping), do: ping |> WandererApp.Api.MapPing.new!()
|
||||
|
||||
@@ -38,4 +66,24 @@ defmodule WandererApp.MapPingsRepo do
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes all pings for a given map. Use with caution - for cleanup purposes.
|
||||
"""
|
||||
def delete_all_for_map(map_id) do
|
||||
case get_by_map(map_id) do
|
||||
{:ok, pings} ->
|
||||
Logger.info("[MapPingsRepo] Deleting #{length(pings)} pings for map #{map_id}")
|
||||
|
||||
Enum.each(pings, fn ping ->
|
||||
Logger.info("[MapPingsRepo] Deleting ping #{ping.id} (type: #{ping.type})")
|
||||
Ash.destroy!(ping)
|
||||
end)
|
||||
|
||||
{:ok, length(pings)}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,14 +51,18 @@ defmodule WandererAppWeb.MapPingsEventHandler do
|
||||
map_ui_ping(ping_info)
|
||||
])
|
||||
|
||||
def handle_server_event(%{event: :ping_cancelled, payload: ping_info}, socket),
|
||||
do:
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("ping_cancelled", %{
|
||||
id: ping_info.id,
|
||||
solar_system_id: ping_info.solar_system_id,
|
||||
type: ping_info.type
|
||||
})
|
||||
def handle_server_event(%{event: :ping_cancelled, payload: ping_info}, socket) do
|
||||
Logger.debug(
|
||||
"handle_server_event :ping_cancelled - id: #{ping_info.id}, is_version_valid?: #{inspect(socket.assigns[:is_version_valid?])}"
|
||||
)
|
||||
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("ping_cancelled", %{
|
||||
id: ping_info.id,
|
||||
solar_system_id: ping_info.solar_system_id,
|
||||
type: ping_info.type
|
||||
})
|
||||
end
|
||||
|
||||
def handle_server_event(event, socket),
|
||||
do: MapCoreEventHandler.handle_server_event(event, socket)
|
||||
@@ -81,12 +85,41 @@ defmodule WandererAppWeb.MapPingsEventHandler do
|
||||
when not is_nil(main_character_id) do
|
||||
{:ok, pings} = WandererApp.MapPingsRepo.get_by_map(map_id)
|
||||
|
||||
no_exisiting_pings =
|
||||
# Filter out orphaned pings (system/character deleted or system hidden)
|
||||
# These should not block new ping creation
|
||||
valid_pings =
|
||||
pings
|
||||
|> Enum.filter(fn ping ->
|
||||
not is_nil(ping.system) and not is_nil(ping.character) and
|
||||
(is_nil(ping.system.visible) or ping.system.visible == true)
|
||||
end)
|
||||
|
||||
existing_rally_pings =
|
||||
valid_pings
|
||||
|> Enum.filter(fn %{type: type} ->
|
||||
type == 1
|
||||
end)
|
||||
|> Enum.empty?()
|
||||
|
||||
no_exisiting_pings = Enum.empty?(existing_rally_pings)
|
||||
orphaned_count = length(pings) - length(valid_pings)
|
||||
|
||||
# Log detailed info about existing pings for debugging
|
||||
if length(existing_rally_pings) > 0 do
|
||||
ping_details =
|
||||
existing_rally_pings
|
||||
|> Enum.map(fn p ->
|
||||
"id=#{p.id}, type=#{p.type}, system_id=#{inspect(p.system_id)}, character_id=#{inspect(p.character_id)}, inserted_at=#{p.inserted_at}"
|
||||
end)
|
||||
|> Enum.join("; ")
|
||||
|
||||
Logger.warning(
|
||||
"add_ping BLOCKED: map_id=#{map_id}, existing_rally_pings=#{length(existing_rally_pings)}: [#{ping_details}]"
|
||||
)
|
||||
else
|
||||
Logger.debug(
|
||||
"add_ping check: map_id=#{map_id}, total_pings=#{length(pings)}, valid_pings=#{length(valid_pings)}, orphaned=#{orphaned_count}, rally_pings=0, can_create=true"
|
||||
)
|
||||
end
|
||||
|
||||
if no_exisiting_pings do
|
||||
map_id
|
||||
@@ -97,9 +130,16 @@ defmodule WandererAppWeb.MapPingsEventHandler do
|
||||
character_id: main_character_id,
|
||||
user_id: current_user.id
|
||||
})
|
||||
end
|
||||
|
||||
{:noreply, socket}
|
||||
{:noreply, socket}
|
||||
else
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("ping_blocked", %{
|
||||
reason: "rally_point_exists",
|
||||
message: "A rally point already exists on this map"
|
||||
})}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_ui_event(
|
||||
@@ -128,6 +168,80 @@ defmodule WandererAppWeb.MapPingsEventHandler do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Catch add_ping when main_character_id is nil
|
||||
def handle_ui_event(
|
||||
"add_ping",
|
||||
_event,
|
||||
%{assigns: %{main_character_id: nil}} = socket
|
||||
) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("ping_blocked", %{
|
||||
reason: "no_main_character",
|
||||
message: "Please select a main character to create pings"
|
||||
})}
|
||||
end
|
||||
|
||||
# Catch add_ping when has_tracked_characters? is false
|
||||
def handle_ui_event(
|
||||
"add_ping",
|
||||
_event,
|
||||
%{assigns: %{has_tracked_characters?: false}} = socket
|
||||
) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("ping_blocked", %{
|
||||
reason: "no_tracked_characters",
|
||||
message: "Please add a tracked character to create pings"
|
||||
})}
|
||||
end
|
||||
|
||||
# Catch add_ping when subscription is not active
|
||||
def handle_ui_event(
|
||||
"add_ping",
|
||||
_event,
|
||||
%{assigns: %{is_subscription_active?: false}} = socket
|
||||
) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("ping_blocked", %{
|
||||
reason: "subscription_inactive",
|
||||
message: "Map subscription is not active"
|
||||
})}
|
||||
end
|
||||
|
||||
# Catch add_ping when user doesn't have update_system permission
|
||||
def handle_ui_event(
|
||||
"add_ping",
|
||||
_event,
|
||||
%{assigns: %{user_permissions: %{update_system: false}}} = socket
|
||||
) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> MapEventHandler.push_map_event("ping_blocked", %{
|
||||
reason: "no_permission",
|
||||
message: "You don't have permission to create pings on this map"
|
||||
})}
|
||||
end
|
||||
|
||||
# Catch cancel_ping failures with feedback
|
||||
def handle_ui_event(
|
||||
"cancel_ping",
|
||||
_event,
|
||||
%{assigns: %{main_character_id: nil}} = socket
|
||||
) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
# Catch-all for cancel_ping to debug why it doesn't match
|
||||
def handle_ui_event(
|
||||
"cancel_ping",
|
||||
event,
|
||||
%{assigns: assigns} = socket
|
||||
) do
|
||||
{:noreply, socket}
|
||||
end
|
||||
|
||||
def handle_ui_event(event, body, socket),
|
||||
do: MapCoreEventHandler.handle_ui_event(event, body, socket)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user