Compare commits

...

3 Commits

Author SHA1 Message Date
CI
d407efe805 chore: [skip ci] 2025-10-27 23:52:49 +00:00
CI
021e04d87a chore: release version v1.83.3 2025-10-27 23:52:49 +00:00
Dmitry Popov
7844c9db34 fix(Core): Fixed old map API for systems & added small QOL improvements 2025-10-28 00:52:04 +01:00
14 changed files with 235 additions and 129 deletions

View File

@@ -2,6 +2,15 @@
<!-- changelog -->
## [v1.83.3](https://github.com/wanderer-industries/wanderer/compare/v1.83.2...v1.83.3) (2025-10-27)
### Bug Fixes:
* Core: Fixed old map API for systems & added small QOL improvements
## [v1.83.2](https://github.com/wanderer-industries/wanderer/compare/v1.83.1...v1.83.2) (2025-10-22)

View File

@@ -30,7 +30,7 @@ defmodule WandererApp.Api.Map do
# Routes configuration
routes do
base("/maps")
get(:read)
get(:by_slug, route: "/:slug")
index :read
post(:new)
patch(:update)

View File

@@ -16,6 +16,17 @@ defmodule WandererApp.Api.MapSystem do
includes([:map])
default_fields([
:name,
:solar_system_id,
:status,
:custom_name,
:description,
:tag,
:temporary_name,
:labels
])
derive_filter?(true)
derive_sort?(true)

View File

@@ -101,11 +101,11 @@ defmodule WandererApp.Map.Server do
end
end
def add_system(map_id, system_info, user_id, character_id) when is_binary(map_id),
def add_system(map_id, system_info, user_id, character_id, opts \\ []) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.cast({&Impl.add_system/4, [system_info, user_id, character_id]})
|> GenServer.cast({&Impl.add_system/5, [system_info, user_id, character_id, opts]})
def paste_connections(map_id, connections, user_id, character_id) when is_binary(map_id),
do:
@@ -113,11 +113,11 @@ defmodule WandererApp.Map.Server do
|> map_pid!
|> GenServer.cast({&Impl.paste_connections/4, [connections, user_id, character_id]})
def paste_systems(map_id, systems, user_id, character_id) when is_binary(map_id),
def paste_systems(map_id, systems, user_id, character_id, opts \\ []) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.cast({&Impl.paste_systems/4, [systems, user_id, character_id]})
|> GenServer.cast({&Impl.paste_systems/5, [systems, user_id, character_id, opts]})
def add_system_comment(map_id, comment_info, user_id, character_id) when is_binary(map_id),
do:

View File

@@ -35,20 +35,37 @@ 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
do_create_system(map_id, user_id, char_id, params)
{:ok, solar_system_id} = fetch_system_id(params)
update_existing = fetch_update_existing(params, false)
map_id
|> WandererApp.Map.check_location(%{solar_system_id: solar_system_id})
|> case do
{:ok, _location} ->
do_create_system(map_id, user_id, char_id, params)
{:error, :already_exists} ->
if update_existing do
do_update_system(map_id, user_id, char_id, solar_system_id, params)
else
:ok
end
end
end
defp do_create_system(map_id, user_id, char_id, params) do
with {:ok, system_id} <- fetch_system_id(params),
update_existing <- fetch_update_existing(params, false),
coords <- normalize_coordinates(params),
:ok <-
Server.add_system(
map_id,
%{solar_system_id: system_id, coordinates: coords},
%{solar_system_id: system_id, coordinates: coords, extra: params},
user_id,
char_id
char_id,
update_existing: update_existing
) do
# System creation is async, but if add_system returns :ok,
# System creation is async, but if add_system returns :ok,
# it means the operation was queued successfully
{:ok, %{solar_system_id: system_id}}
else
@@ -63,15 +80,26 @@ defmodule WandererApp.Map.Operations.Systems do
end
@spec update_system(Plug.Conn.t(), integer(), map()) :: {:ok, map()} | {:error, atom()}
def update_system(%{assigns: %{map_id: map_id}} = _conn, system_id, attrs) do
with {:ok, current} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id),
x_raw <- Map.get(attrs, "position_x", Map.get(attrs, :position_x, current.position_x)),
y_raw <- Map.get(attrs, "position_y", Map.get(attrs, :position_y, current.position_y)),
def update_system(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
_conn,
solar_system_id,
attrs
) do
do_update_system(map_id, user_id, char_id, solar_system_id, attrs)
end
def update_system(_conn, _solar_system_id, _attrs), do: {:error, :missing_params}
defp do_update_system(map_id, user_id, char_id, solar_system_id, params) do
with {:ok, current} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id),
x_raw <- Map.get(params, "position_x", Map.get(params, :position_x, current.position_x)),
y_raw <- Map.get(params, "position_y", Map.get(params, :position_y, current.position_y)),
{:ok, x} <- parse_int(x_raw, "position_x"),
{:ok, y} <- parse_int(y_raw, "position_y"),
coords = %{x: x, y: y},
:ok <- apply_system_updates(map_id, system_id, attrs, coords),
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, system_id) do
:ok <- apply_system_updates(map_id, solar_system_id, params, coords),
{:ok, system} <- MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do
{:ok, system}
else
{:error, reason} when is_binary(reason) ->
@@ -84,8 +112,6 @@ defmodule WandererApp.Map.Operations.Systems do
end
end
def update_system(_conn, _system_id, _attrs), do: {:error, :missing_params}
@spec delete_system(Plug.Conn.t(), integer()) :: {:ok, integer()} | {:error, atom()}
def delete_system(
%{assigns: %{map_id: map_id, owner_character_id: char_id, owner_user_id: user_id}} =
@@ -148,6 +174,15 @@ defmodule WandererApp.Map.Operations.Systems do
defp fetch_system_id(_), do: {:error, "Missing system identifier (id)"}
defp fetch_update_existing(%{"update_existing" => update_existing}, _default),
do: update_existing
defp fetch_update_existing(%{update_existing: update_existing}, _default)
when not is_nil(update_existing),
do: update_existing
defp fetch_update_existing(_, default), do: default
defp parse_int(val, _field) when is_integer(val), do: {:ok, val}
defp parse_int(val, field) when is_binary(val) do
@@ -232,6 +267,15 @@ defmodule WandererApp.Map.Operations.Systems do
labels: Enum.join(labels, ",")
})
"custom_name" ->
{:ok, solar_system_info} =
WandererApp.CachedInfo.get_system_static_info(system_id)
Server.update_system_name(map_id, %{
solar_system_id: system_id,
name: val || solar_system_info.solar_system_name
})
"temporary_name" ->
Server.update_system_temporary_name(map_id, %{
solar_system_id: system_id,

View File

@@ -157,9 +157,9 @@ defmodule WandererApp.Map.Server.Impl do
state
end
defdelegate add_system(state, system_info, user_id, character_id), to: SystemsImpl
defdelegate add_system(state, system_info, user_id, character_id, opts \\ []), to: SystemsImpl
defdelegate paste_systems(state, systems, user_id, character_id), to: SystemsImpl
defdelegate paste_systems(state, systems, user_id, character_id, opts), to: SystemsImpl
defdelegate add_system_comment(state, comment_info, user_id, character_id), to: SystemsImpl

View File

@@ -46,11 +46,12 @@ defmodule WandererApp.Map.Server.SystemsImpl do
solar_system_id: solar_system_id
} = system_info,
user_id,
character_id
character_id,
opts
) do
case map_id |> WandererApp.Map.check_location(%{solar_system_id: solar_system_id}) do
{:ok, _location} ->
state |> _add_system(system_info, user_id, character_id)
state |> do_add_system(system_info, user_id, character_id)
{:error, :already_exists} ->
state
@@ -61,7 +62,8 @@ defmodule WandererApp.Map.Server.SystemsImpl do
%{map_id: map_id} = state,
systems,
user_id,
character_id
character_id,
opts
) do
systems
|> Enum.each(fn %{
@@ -72,15 +74,23 @@ defmodule WandererApp.Map.Server.SystemsImpl do
case map_id |> WandererApp.Map.check_location(%{solar_system_id: solar_system_id}) do
{:ok, _location} ->
state
|> _add_system(
%{solar_system_id: solar_system_id, coordinates: coordinates, extra_info: system},
user_id,
character_id
)
if opts |> Keyword.get(:add_not_existing, true) do
state
|> do_add_system(
%{solar_system_id: solar_system_id, coordinates: coordinates, extra_info: system},
user_id,
character_id
)
else
:ok
end
{:error, :already_exists} ->
:ok
if opts |> Keyword.get(:update_existing, false) do
:ok
else
:ok
end
end
end)
@@ -304,7 +314,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
map_id
|> WandererApp.MapSystemRepo.remove_from_map(solar_system_id)
|> case do
{:ok, _} ->
{:ok, result} ->
:ok = WandererApp.Map.remove_system(map_id, solar_system_id)
@ddrt.delete([solar_system_id], rtree_name)
Impl.broadcast!(map_id, :systems_removed, [solar_system_id])
@@ -538,7 +548,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
def maybe_add_system(_map_id, _location, _old_location, _rtree_name, _map_opts), do: :ok
defp _add_system(
defp do_add_system(
%{map_id: map_id, map_opts: map_opts, rtree_name: rtree_name} = state,
%{
solar_system_id: solar_system_id,
@@ -633,7 +643,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
# ADDITIVE: Also broadcast to external event system (webhooks/WebSocket)
Logger.debug(fn ->
"SystemsImpl._add_system calling ExternalEvents.broadcast for map #{map_id}, system: #{solar_system_id}"
"SystemsImpl.do_add_system calling ExternalEvents.broadcast for map #{map_id}, system: #{solar_system_id}"
end)
WandererApp.ExternalEvents.broadcast(map_id, :add_system, %{

View File

@@ -128,76 +128,92 @@ defmodule WandererAppWeb.Layouts do
def sidebar_nav_links(assigns) do
~H"""
<ul class="text-center flex flex-col w-full">
<div class="dropdown dropdown-right">
<div tabindex="0" role="button">
<li class="flex-1 w-full h-14 block text-gray-400 hover:text-white p-3">
<.icon name="hero-bars-3-solid" class="w-6 h-6" />
</li>
<ul class="text-center flex flex-col w-full h-full justify-between">
<div>
<div class="dropdown dropdown-right">
<div tabindex="0" role="button">
<li class="flex-1 w-full h-14 block text-gray-400 hover:text-white p-3">
<.icon name="hero-bars-3-solid" class="w-6 h-6" />
</li>
</div>
<ul
tabindex="0"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
>
<li><a href="/changelog">Changelog</a></li>
<li><a href="/news">News</a></li>
<li><a href="/license">License</a></li>
<li><a href="/contacts">Contact Us</a></li>
</ul>
</div>
<div :if={@show_sidebar}>
<.nav_link
href="/last"
active={@active_tab == :map}
icon="hero-viewfinder-circle-solid"
tip="Map"
/>
<.nav_link href="/maps" active={@active_tab == :maps} icon="hero-map-solid" tip="Maps" />
<.nav_link
href="/access-lists"
active={@active_tab == :access_lists}
icon="hero-user-group-solid"
tip="Access Lists"
/>
<.nav_link
href="/characters"
active={@active_tab == :characters}
icon="hero-user-plus-solid"
tip="Characters"
/>
<.nav_link
href="/tracking"
active={@active_tab == :characters_tracking}
icon="hero-signal-solid"
tip="Characters Tracking"
/>
</div>
<ul
tabindex="0"
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
>
<li><a href="/changelog">Changelog</a></li>
<li><a href="/news">News</a></li>
<li><a href="/license">License</a></li>
<li><a href="/contacts">Contact Us</a></li>
</ul>
</div>
<.nav_link
href="/last"
active={@active_tab == :map}
icon="hero-viewfinder-circle-solid"
tip="Map"
/>
<.nav_link href="/maps" active={@active_tab == :maps} icon="hero-map-solid" tip="Maps" />
<.nav_link
href="/access-lists"
active={@active_tab == :access_lists}
icon="hero-user-group-solid"
tip="Access Lists"
/>
<.nav_link
href="/characters"
active={@active_tab == :characters}
icon="hero-user-plus-solid"
tip="Characters"
/>
<.nav_link
href="/tracking"
active={@active_tab == :characters_tracking}
icon="hero-signal-solid"
tip="Characters Tracking"
/>
<div class="absolute bottom-0 left-0 border-t border-gray-600 dropdown dropdown-right dropdown-end">
<div tabindex="0" role="button" class="h-full w-full text-gray-400 hover:text-white block p-4">
<.icon name="hero-user-solid" class="w-6 h-6" />
<div>
<div
:if={@show_sidebar}
class="bottom-0 left-0 border-t border-gray-600 dropdown dropdown-right dropdown-end"
>
<div
tabindex="0"
role="button"
class="h-full w-full text-gray-400 hover:text-white block p-4"
>
<.icon name="hero-user-solid" class="w-6 h-6" />
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li :if={@show_admin}>
<.link navigate="/admin">
Admin
</.link>
</li>
<li :if={@show_admin}>
<.link navigate="/admin/errors">
Errors
</.link>
</li>
<li :if={@map_subscriptions_enabled}>
<.link navigate="/profile">
Profile
</.link>
</li>
<li>
<.link navigate="/auth/signout">
Logout
</.link>
</li>
</ul>
</div>
<div
phx-click="toggle_sidebar"
class="z-10 flex-1 absolute bottom-0 left-0 w-full h-2 block text-gray-400 hover:bg-[#444]"
>
</div>
<ul tabindex="0" class="dropdown-content menu bg-base-100 rounded-box z-[1] w-52 p-2 shadow">
<li :if={@show_admin}>
<.link navigate="/admin">
Admin
</.link>
</li>
<li :if={@show_admin}>
<.link navigate="/admin/errors">
Errors
</.link>
</li>
<li :if={@map_subscriptions_enabled}>
<.link navigate="/profile">
Profile
</.link>
</li>
<li>
<.link navigate="/auth/signout">
Logout
</.link>
</li>
</ul>
</div>
</ul>
"""

View File

@@ -9,23 +9,30 @@
>
{@inner_content}
</main>
<aside class="h-full w-14 left-0 absolute bg-gray-400 bg-opacity-5 text-gray-200 shadow-lg border-r border-stone-800 bg-opacity-70 bg-neutral-900">
<aside class={[
"h-12 w-14 left-0 absolute bg-gray-400 bg-opacity-5 text-gray-200 shadow-lg border-r border-b border-stone-800 bg-opacity-70 bg-neutral-900",
classes("h-full": @show_sidebar)
]}>
<.sidebar_nav_links
active_tab={@active_tab}
show_admin={@show_admin}
show_sidebar={@show_sidebar}
map_subscriptions_enabled={@map_subscriptions_enabled?}
/>
<.ping_container :if={@show_sidebar} rtt_class={@rtt_class} />
<.donate_container :if={@show_sidebar} />
<.feedback_container :if={@show_sidebar} />
<.youtube_container :if={@show_sidebar} />
<div :if={@show_sidebar}>
{live_render(@socket, WandererAppWeb.ServerStatusLive,
container: {:div, class: ""},
id: "server-status"
)}
</div>
</aside>
<.ping_container rtt_class={@rtt_class} />
<.donate_container />
<.feedback_container />
<.youtube_container />
<.new_version_banner app_version={@app_version} enabled={@map_subscriptions_enabled?} />
</div>
{live_render(@socket, WandererAppWeb.ServerStatusLive,
container: {:div, class: ""},
id: "server-status"
)}
<.live_component module={WandererAppWeb.Alerts} id="notifications" view_flash={@flash} />

View File

@@ -97,7 +97,12 @@ defmodule WandererAppWeb.MapSystemAPIController do
tag: %Schema{type: :string, nullable: true, description: "Custom tag"},
locked: %Schema{type: :boolean, description: "Lock flag"},
temporary_name: %Schema{type: :string, nullable: true, description: "Temporary name"},
labels: %Schema{type: :string, description: "Comma-separated list of labels"}
labels: %Schema{type: :string, description: "Comma-separated list of labels"},
update_existing: %Schema{
type: :boolean,
nullable: true,
description: "Update existing system"
}
},
required: ~w(solar_system_id)a,
example: %{
@@ -107,7 +112,8 @@ defmodule WandererAppWeb.MapSystemAPIController do
position_x: 100,
position_y: 200,
visible: true,
labels: "market,hub"
labels: "market,hub",
update_existing: false
}
}
@@ -508,7 +514,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
end
end
operation(:delete,
operation(:delete_batch,
summary: "Batch Delete Systems and Connections",
parameters: [
map_identifier: [
@@ -523,7 +529,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
responses: ResponseSchemas.standard_responses(@batch_delete_response_schema)
)
def delete(conn, params) do
def delete_batch(conn, params) do
system_ids = Map.get(params, "system_ids", [])
connection_ids = Map.get(params, "connection_ids", [])
@@ -560,7 +566,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
end
end
operation(:delete_single,
operation(:delete,
summary: "Delete a single Map System",
parameters: [
map_identifier: [
@@ -580,7 +586,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
responses: ResponseSchemas.standard_responses(@delete_response_schema)
)
def delete_single(conn, %{"id" => id}) do
def delete(conn, %{"id" => id}) do
with {:ok, sid} <- APIUtils.parse_int(id),
{:ok, _} <- Operations.delete_system(conn, sid) do
APIUtils.respond_data(conn, %{deleted: true})
@@ -599,7 +605,7 @@ defmodule WandererAppWeb.MapSystemAPIController do
reason: reason
})
_ ->
error ->
conn
|> put_status(:bad_request)
|> APIUtils.respond_data(%{deleted: false, error: "Invalid system ID format"})

View File

@@ -165,11 +165,11 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
if is_binary(map.public_api_key) &&
Crypto.secure_compare(map.public_api_key, token) do
# Get the map owner
case User.by_id(map.owner_id, load: :characters) do
case User.by_id(map.owner.user_id, load: :characters) do
{:ok, user} ->
{:ok, user, map}
{:error, _} ->
{:error, _error} ->
{:error, "Authentication failed", :map_owner_not_found}
end
else
@@ -184,16 +184,14 @@ defmodule WandererAppWeb.Plugs.CheckJsonApiAuth do
# Helper to resolve map by ID or slug
defp resolve_map_identifier(identifier) do
alias WandererApp.Api.Map
# Try as UUID first
case Map.by_id(identifier) do
case WandererApp.Api.Map.by_id(identifier, load: :owner) do
{:ok, map} ->
{:ok, map}
_ ->
# Try as slug
Map.get_map_by_slug(identifier)
WandererApp.Api.Map.get_map_by_slug(identifier, load: :owner)
end
end

View File

@@ -21,19 +21,24 @@ defmodule WandererAppWeb.Nav do
|> attach_hook(:active_tab, :handle_params, &set_active_tab/3)
|> attach_hook(:ping, :handle_event, &handle_event/3)
|> assign(
rtt_class: _rtt_class(),
rtt_class: rtt_class(),
show_admin: show_admin,
show_sidebar: true,
map_subscriptions_enabled?: WandererApp.Env.map_subscriptions_enabled?(),
app_version: WandererApp.Env.vsn()
)}
end
defp handle_event("ping", %{"rtt" => rtt}, socket) do
{:halt,
{:cont,
socket
|> rate_limited_ping_broadcast(socket.assigns.current_user, rtt)
|> push_event("pong", %{})
|> assign(:rtt_class, _rtt_class(rtt))}
|> assign(:rtt_class, rtt_class(rtt))}
end
defp handle_event("toggle_sidebar", _, socket) do
{:cont, socket |> assign(:show_sidebar, not socket.assigns.show_sidebar)}
end
defp handle_event(_, _, socket), do: {:cont, socket}
@@ -79,9 +84,9 @@ defmodule WandererAppWeb.Nav do
defp rate_limited_ping_broadcast(socket, _user, _rtt), do: socket
defp _rtt_class(rtt \\ 0)
defp rtt_class(rtt \\ 0)
defp _rtt_class(rtt) when is_integer(rtt) do
defp rtt_class(rtt) when is_integer(rtt) do
cond do
rtt < 100 -> ""
rtt < 200 -> "text-yellow-500"
@@ -89,5 +94,5 @@ defmodule WandererAppWeb.Nav do
end
end
defp _rtt_class(_), do: ""
defp rtt_class(_), do: ""
end

View File

@@ -287,7 +287,7 @@ defmodule WandererAppWeb.Router do
patch "/connections", MapConnectionAPIController, :update
delete "/connections", MapConnectionAPIController, :delete
delete "/systems", MapSystemAPIController, :delete
delete "/systems", MapSystemAPIController, :delete_batch
resources "/systems", MapSystemAPIController, only: [:index, :show, :create, :update, :delete]
resources "/connections", MapConnectionAPIController,

View File

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