Compare commits

..

17 Commits

Author SHA1 Message Date
CI
98a03d1e59 chore: release version v1.35.0 2025-01-07 23:05:04 +00:00
guarzo
2088393c79 feat(Map): add "temporary system names" toggle (#86)
If enabled and set, a temporary name is displayed instead of the system name. The original system name appears on a secondary row if no custom name exists. Temporary names are removed when the system is removed from the map.
2025-01-08 03:04:36 +04:00
CI
093042b88a chore: release version v1.33.1
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-07 12:47:07 +00:00
Dmitry Popov
e5ef35c186 hotfix: Fix map behaviour for 'Allow only tracked characters' map option 2025-01-07 13:46:34 +01:00
CI
1cd23d5efd chore: release version v1.33.0
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-07 09:17:09 +00:00
guarzo
ead5818a3f feat(Map): api to allow systematic access to visible systems and tracked characters (#89)
* feat: add limited api for system and tracked characters
* env variable to disable public api key
2025-01-07 13:16:39 +04:00
CI
a5f66ada68 chore: release version v1.32.7
Some checks are pending
Build / 🚀 Deploy to test env (fly.io) (push) Waiting to run
Build / 🛠 Build (1.17, 18.x, 27) (push) Waiting to run
Build / 🛠 Build Docker Images (linux/amd64) (push) Blocked by required conditions
Build / 🏷 Create Release (push) Blocked by required conditions
2025-01-06 22:24:26 +00:00
Dmitry Popov
0919742853 Revert "hotfix: add ARM64 compatible Docker image (#94)" (#99)
This reverts commit f85317983c.
2025-01-07 02:23:59 +04:00
CI
f3efffd259 chore: release version v1.32.6 2025-01-06 21:56:57 +00:00
Tsuro Tsero
f85317983c hotfix: add ARM64 compatible Docker image (#94) 2025-01-07 01:56:22 +04:00
CI
76f709b768 chore: release version v1.32.5
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-04 20:23:09 +00:00
guarzo
e3b2356302 fix(map): prevent deselect on click to map (#96)
Fixes #80

- Prevents single node deselection on background / same node click
- Allows deseletion of all nodes if multiple are currently selected
2025-01-05 00:22:41 +04:00
CI
3d810211ee chore: release version v1.32.4
Some checks failed
Build / 🚀 Deploy to test env (fly.io) (push) Has been cancelled
Build / 🛠 Build (1.17, 18.x, 27) (push) Has been cancelled
Build / 🛠 Build Docker Images (linux/amd64) (push) Has been cancelled
Build / 🏷 Create Release (push) Has been cancelled
2025-01-02 19:56:48 +00:00
Dmitry Popov
7453795dc5 Merge branch 'main' of github.com:wanderer-industries/wanderer 2025-01-02 20:56:18 +01:00
Dmitry Popov
9de7cd99ee fix(Map): Fix 'Character Activity' modal 2025-01-02 20:56:14 +01:00
CI
51489c1aa5 chore: release version v1.32.3 2025-01-02 17:23:05 +00:00
Dmitry Popov
25dd6de770 fix(Map): Fix 'Allow only tracked characters' saving 2025-01-02 18:22:32 +01:00
38 changed files with 986 additions and 59 deletions

View File

@@ -6,3 +6,4 @@ export EVE_CLIENT_WITH_WALLET_ID="<EVE_CLIENT_WITH_WALLET_ID>"
export EVE_CLIENT_WITH_WALLET_SECRET="<EVE_CLIENT_WITH_WALLET_SECRET>"
export GIT_SHA="1111"
export WANDERER_INVITES="false"
export WANDERER_PUBLIC_API_DISABLED="false"

View File

@@ -2,6 +2,79 @@
<!-- changelog -->
## [v1.35.0](https://github.com/wanderer-industries/wanderer/compare/v1.34.0...v1.35.0) (2025-01-07)
### Features:
* Map: add "temporary system names" toggle (#86)
## [v1.34.0](https://github.com/wanderer-industries/wanderer/compare/v1.33.1...v1.34.0) (2025-01-07)
### Features:
* Map: api to allow systematic access to visible systems and tracked characters (#89)
* add limited api for system and tracked characters
## [v1.33.1](https://github.com/wanderer-industries/wanderer/compare/v1.33.0...v1.33.1) (2025-01-07)
## [v1.33.0](https://github.com/wanderer-industries/wanderer/compare/v1.32.7...v1.33.0) (2025-01-07)
### Features:
* Map: api to allow systematic access to visible systems and tracked characters (#89)
* add limited api for system and tracked characters
## [v1.32.7](https://github.com/wanderer-industries/wanderer/compare/v1.32.6...v1.32.7) (2025-01-06)
## [v1.32.6](https://github.com/wanderer-industries/wanderer/compare/v1.32.5...v1.32.6) (2025-01-06)
## [v1.32.5](https://github.com/wanderer-industries/wanderer/compare/v1.32.4...v1.32.5) (2025-01-04)
### Bug Fixes:
* map: prevent deselect on click to map (#96)
## [v1.32.4](https://github.com/wanderer-industries/wanderer/compare/v1.32.3...v1.32.4) (2025-01-02)
### Bug Fixes:
* Map: Fix 'Character Activity' modal
## [v1.32.3](https://github.com/wanderer-industries/wanderer/compare/v1.32.2...v1.32.3) (2025-01-02)
### Bug Fixes:
* Map: Fix 'Allow only tracked characters' saving
## [v1.32.2](https://github.com/wanderer-industries/wanderer/compare/v1.32.1...v1.32.2) (2025-01-02)

View File

@@ -88,6 +88,23 @@ export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseC
setSystem(undefined);
}, []);
const onSystemTemporaryName = useCallback((temporaryName?: string) => {
const { system, outCommand } = ref.current;
if (!system) {
return;
}
outCommand({
type: OutCommand.updateSystemTemporaryName,
data: {
system_id: system,
value: temporaryName ?? '',
},
});
setSystem(undefined);
}, []);
const onSystemStatus = useCallback((status: number) => {
const { system, outCommand } = ref.current;
if (!system) {
@@ -161,6 +178,7 @@ export const useContextMenuSystemHandlers = ({ systems, hubs, outCommand }: UseC
onLockToggle,
onHubToggle,
onSystemTag,
onSystemTemporaryName,
onSystemStatus,
onSystemLabels,
onOpenSettings,

View File

@@ -119,7 +119,7 @@ const MapComp = ({
isSoftBackground,
onAddSystem,
}: MapCompProps) => {
const { getNode } = useReactFlow();
const { getNode, getNodes } = useReactFlow();
const [nodes, , onNodesChange] = useNodesState<Node<SolarSystemRawType>>(initialNodes);
const [edges, , onEdgesChange] = useEdgesState<Edge<SolarSystemConnection>>(initialEdges);
@@ -186,6 +186,12 @@ const MapComp = ({
(changes: NodeChange[]) => {
const systemsIdsToRemove: string[] = [];
// prevents single node deselection on background / same node click
// allows deseletion of all nodes if multiple are currently selected
if (changes.length === 1 && changes[0].type == 'select' && changes[0].selected === false) {
changes[0].selected = getNodes().filter(node => node.selected).length === 1;
}
const nextChanges = changes.reduce((acc, change) => {
if (change.type !== 'remove') {
return [...acc, change];

View File

@@ -4,6 +4,8 @@ import { MapSolarSystemType } from '../../map.types';
import classes from './SolarSystemNode.module.scss';
import clsx from 'clsx';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import {
EFFECT_BACKGROUND_STYLES,
@@ -56,6 +58,8 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
const { interfaceSettings } = useMapRootState();
const { isShowUnsplashedSignatures } = interfaceSettings;
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
const {
system_class,
security,
@@ -71,9 +75,8 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
const signatures = data.system_signatures;
const { locked, name, tag, status, labels, id } = data || {};
const { locked, name, tag, status, labels, id, temporary_name: temporaryName } = data || {};
const customName = solar_system_name !== name ? name : undefined;
const {
data: {
@@ -136,6 +139,10 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
const space = showKSpaceBG ? REGIONS_MAP[region_id] : '';
const regionClass = showKSpaceBG ? SpaceToClass[space] : null;
const systemName = isTempSystemNameEnabled && temporaryName || solar_system_name;
const customName = (isTempSystemNameEnabled && temporaryName && name) || (solar_system_name !== name && name);
const [unsplashedLeft, unsplashedRight] = useMemo(() => {
if (!isShowUnsplashedSignatures) {
return [[], []];
@@ -205,7 +212,7 @@ export const SolarSystemNode = memo(({ data, selected }: WrapNodeProps<MapSolarS
'[text-shadow:_0_1px_0_rgb(0_0_0_/_40%)] flex-grow overflow-hidden text-ellipsis whitespace-nowrap font-sans',
)}
>
{solar_system_name}
{systemName}
</div>
{isWormhole && (

View File

@@ -3,6 +3,7 @@ import { InputTextarea } from 'primereact/inputtextarea';
import { Dialog } from 'primereact/dialog';
import { getSystemById } from '@/hooks/Mapper/helpers';
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
import { useMapGetOption } from '@/hooks/Mapper/mapRootProvider/hooks/api';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Button } from 'primereact/button';
import { OutCommand } from '@/hooks/Mapper/types';
@@ -22,10 +23,15 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
outCommand,
} = useMapRootState();
const isTempSystemNameEnabled = useMapGetOption('show_temp_system_name') === 'true';
const system = getSystemById(systems, systemId);
const [name, setName] = useState('');
const [label, setLabel] = useState('');
const [temporaryName, setTemporaryName] = useState('')
const [description, setDescription] = useState('');
const inputRef = useRef<HTMLInputElement>();
@@ -38,14 +44,15 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
setName(system.name || '');
setLabel(labels.customLabel);
setTemporaryName(system.temporary_name || '');
setDescription(system.description || '');
}, [system]);
const ref = useRef({ name, description, label, outCommand, systemId, system });
ref.current = { name, description, label, outCommand, systemId, system };
const ref = useRef({ name, description, temporaryName, label, outCommand, systemId, system });
ref.current = { name, description, label, temporaryName, outCommand, systemId, system };
const handleSave = useCallback(() => {
const { name, description, label, outCommand, systemId, system } = ref.current;
const { name, description, label, temporaryName, outCommand, systemId, system } = ref.current;
const outLabel = new LabelsManager(system?.labels ?? '');
outLabel.updateCustomLabel(label);
@@ -58,6 +65,14 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
},
});
outCommand({
type: OutCommand.updateSystemTemporaryName,
data: {
system_id: systemId,
value: temporaryName,
},
});
outCommand({
type: OutCommand.updateSystemName,
data: {
@@ -167,6 +182,35 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
</IconField>
</div>
{isTempSystemNameEnabled &&
<div className="flex flex-col gap-1">
<label htmlFor="username">Temporary Name</label>
<IconField>
{temporaryName !== '' && (
<WdImgButton
className="pi pi-trash text-red-400"
textSize={WdImageSize.large}
tooltip={{
content: 'Remove temporary name',
className: 'pi p-input-icon',
position: TooltipPosition.top,
}}
onClick={() => setTemporaryName('')}
/>
)}
<InputText
id="temporaryName"
aria-describedby="temporaryName"
autoComplete="off"
value={temporaryName}
maxLength={10}
onChange={e => setTemporaryName(e.target.value)}
/>
</IconField>
</div>
}
<div className="flex flex-col gap-1">
<label htmlFor="username">Description</label>
<InputTextarea

View File

@@ -129,6 +129,7 @@ export enum OutCommand {
updateConnectionCustomInfo = 'update_connection_custom_info',
updateSignatures = 'update_signatures',
updateSystemName = 'update_system_name',
updateSystemTemporaryName = 'update_system_temporary_name',
updateSystemDescription = 'update_system_description',
updateSystemLabels = 'update_system_labels',
updateSystemLocked = 'update_system_locked',

View File

@@ -116,6 +116,7 @@ export type SolarSystemRawType = {
tag: string | null;
status: number;
name: string | null;
temporary_name: string | null;
system_static_info: SolarSystemStaticInfoRaw;
system_signatures: SystemSignature[];

View File

@@ -1,17 +1,14 @@
export default {
mounted() {
const hook = this;
const url = hook.el.dataset.url;
const button = hook.el;
const button = this.el;
button.addEventListener('click', function () {
// Get the URL from the data attribute
button.classList.remove('copied');
// Copy the URL to the clipboard
navigator.clipboard
.writeText(url)
.writeText(button.dataset.url)
.then(() => {
button.classList.add('copied');
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -48,6 +48,11 @@ port =
scheme = System.get_env("WEB_EXTERNAL_SCHEME", "http")
public_api_disabled =
config_dir
|> get_var_from_path_or_env("WANDERER_PUBLIC_API_DISABLED", "false")
|> String.to_existing_atom()
map_subscriptions_enabled =
config_dir
|> get_var_from_path_or_env("WANDERER_MAP_SUBSCRIPTIONS_ENABLED", "false")
@@ -83,6 +88,7 @@ config :wanderer_app,
admins: admins,
corp_id: System.get_env("WANDERER_CORP_ID", "-1") |> String.to_integer(),
corp_wallet: System.get_env("WANDERER_CORP_WALLET", ""),
public_api_disabled: public_api_disabled,
map_subscriptions_enabled: map_subscriptions_enabled,
wallet_tracking_enabled: wallet_tracking_enabled,
subscription_settings: %{

View File

@@ -21,6 +21,7 @@ defmodule WandererApp.Api.Map do
define(:update_options, action: :update_options)
define(:assign_owner, action: :assign_owner)
define(:mark_as_deleted, action: :mark_as_deleted)
define(:update_api_key, action: :update_api_key)
define(:by_id,
get_by: [:id],
@@ -122,6 +123,11 @@ defmodule WandererApp.Api.Map do
change(set_attribute(:deleted, true))
end
update :update_api_key do
accept [:public_api_key]
end
end
attributes do
@@ -141,6 +147,10 @@ defmodule WandererApp.Api.Map do
attribute :description, :string
attribute :personal_note, :string
attribute :public_api_key, :string do
allow_nil? true
end
attribute :hubs, {:array, :string} do
allow_nil?(true)

View File

@@ -46,6 +46,7 @@ defmodule WandererApp.Api.MapSystem do
define(:update_locked, action: :update_locked)
define(:update_status, action: :update_status)
define(:update_tag, action: :update_tag)
define(:update_temporary_name, action: :update_temporary_name)
define(:update_labels, action: :update_labels)
define(:update_position, action: :update_position)
define(:update_visible, action: :update_visible)
@@ -102,6 +103,10 @@ defmodule WandererApp.Api.MapSystem do
accept [:tag]
end
update :update_temporary_name do
accept [:temporary_name]
end
update :update_labels do
accept [:labels]
end
@@ -141,6 +146,10 @@ defmodule WandererApp.Api.MapSystem do
allow_nil? true
end
attribute :temporary_name, :string do
allow_nil? true
end
attribute :labels, :string do
allow_nil? true
end

View File

@@ -9,6 +9,7 @@ defmodule WandererApp.Env do
def custom_route_base_url, do: get_key(:custom_route_base_url, "<CUSTOM_ROUTE_BASE_URL>")
def invites, do: get_key(:invites, false)
def map_subscriptions_enabled?, do: get_key(:map_subscriptions_enabled, false)
def public_api_disabled?, do: get_key(:public_api_disabled, false)
def wallet_tracking_enabled?, do: get_key(:wallet_tracking_enabled, false)
def admins, do: get_key(:admins, [])
def admin_username, do: get_key(:admin_username)

View File

@@ -129,6 +129,12 @@ defmodule WandererApp.Map.Server do
|> map_pid!
|> GenServer.cast({&Impl.update_system_tag/2, [update]})
def update_system_temporary_name(map_id, update) when is_binary(map_id),
do:
map_id
|> map_pid!
|> GenServer.cast({&Impl.update_system_temporary_name/2, [update]})
def update_system_locked(map_id, update) when is_binary(map_id),
do:
map_id

View File

@@ -150,6 +150,8 @@ defmodule WandererApp.Map.Server.Impl do
defdelegate update_system_tag(state, update), to: SystemsImpl
defdelegate update_system_temporary_name(state, update), to: SystemsImpl
defdelegate update_system_locked(state, update), to: SystemsImpl
defdelegate update_system_labels(state, update), to: SystemsImpl
@@ -201,7 +203,7 @@ defmodule WandererApp.Map.Server.Impl do
| map: map |> WandererApp.Map.update_subscription_settings!(subscription_settings)
}
def handle_event(:update_characters, %{map_id: map_id} = state) do
def handle_event(:update_characters, state) do
Process.send_after(self(), :update_characters, @update_characters_timeout)
CharactersImpl.update_characters(state)
@@ -259,7 +261,7 @@ defmodule WandererApp.Map.Server.Impl do
state
end
def handle_event(:cleanup_systems, %{map_id: map_id} = state) do
def handle_event(:cleanup_systems, state) do
Process.send_after(self(), :cleanup_systems, @systems_cleanup_timeout)
state |> SystemsImpl.cleanup_systems()
@@ -320,6 +322,8 @@ defmodule WandererApp.Map.Server.Impl do
layout: options |> Map.get("layout", "left_to_right"),
store_custom_labels:
options |> Map.get("store_custom_labels", "false") |> String.to_existing_atom(),
show_temp_system_name:
options |> Map.get("show_temp_system_name", "false") |> String.to_existing_atom(),
restrict_offline_showing:
options |> Map.get("restrict_offline_showing", "false") |> String.to_existing_atom()
]
@@ -427,7 +431,8 @@ defmodule WandererApp.Map.Server.Impl do
"name" => name,
"position" => %{"x" => x, "y" => y},
"status" => status,
"tag" => tag
"tag" => tag,
"temporary_name" => temporary_name,
} = _system,
acc ->
acc
@@ -446,6 +451,7 @@ defmodule WandererApp.Map.Server.Impl do
})
|> update_system_status(%{solar_system_id: id |> String.to_integer(), status: status})
|> update_system_tag(%{solar_system_id: id |> String.to_integer(), tag: tag})
|> update_system_temporary_name(%{solar_system_id: id |> String.to_integer(), temporary_name: temporary_name})
|> update_system_locked(%{solar_system_id: id |> String.to_integer(), locked: locked})
|> update_system_labels(%{solar_system_id: id |> String.to_integer(), labels: labels})
end)

View File

@@ -122,6 +122,13 @@ defmodule WandererApp.Map.Server.SystemsImpl do
),
do: state |> update_system(:update_tag, [:tag], update)
def update_system_temporary_name(
state,
update
) do
state |> update_system(:update_temporary_name, [:temporary_name], update)
end
def update_system_locked(
state,
update
@@ -286,6 +293,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|> WandererApp.MapSystemRepo.update_visible!(%{visible: true})
|> WandererApp.MapSystemRepo.cleanup_tags()
|> WandererApp.MapSystemRepo.cleanup_temporary_name()
@ddrt.insert(
{existing_system.solar_system_id,
@@ -404,6 +412,7 @@ defmodule WandererApp.Map.Server.SystemsImpl do
|> WandererApp.MapSystemRepo.update_position!(%{position_x: x, position_y: y})
|> WandererApp.MapSystemRepo.cleanup_labels!(map_opts)
|> WandererApp.MapSystemRepo.cleanup_tags!()
|> WandererApp.MapSystemRepo.cleanup_temporary_name!()
|> WandererApp.MapSystemRepo.update_visible(%{visible: true})
end

View File

@@ -4,6 +4,7 @@ defmodule WandererApp.MapRepo do
@default_map_options %{
"layout" => "left_to_right",
"store_custom_labels" => "false",
"show_temp_system_name" => "false",
"restrict_offline_showing" => "false"
}

View File

@@ -16,11 +16,13 @@ defmodule WandererApp.MapSystemRepo do
end
end
def get_all_by_map(map_id),
do: WandererApp.Api.MapSystem.read_all_by_map(%{map_id: map_id})
def get_all_by_map(map_id) do
WandererApp.Api.MapSystem.read_all_by_map(%{map_id: map_id})
end
def get_visible_by_map(map_id),
do: WandererApp.Api.MapSystem.read_visible_by_map(%{map_id: map_id})
def get_visible_by_map(map_id) do
WandererApp.Api.MapSystem.read_visible_by_map(%{map_id: map_id})
end
def remove_from_map(map_id, solar_system_id) do
WandererApp.Api.MapSystem.read_by_map_and_solar_system!(%{
@@ -59,6 +61,20 @@ defmodule WandererApp.MapSystemRepo do
})
end
def cleanup_temporary_name(system) do
system
|> WandererApp.Api.MapSystem.update_temporary_name(%{
temporary_name: nil
})
end
def cleanup_temporary_name!(system) do
system
|> WandererApp.Api.MapSystem.update_temporary_name!(%{
temporary_name: nil
})
end
def get_filtered_labels(labels, true) when is_binary(labels) do
labels
|> Jason.decode!()
@@ -99,6 +115,11 @@ defmodule WandererApp.MapSystemRepo do
system
|> WandererApp.Api.MapSystem.update_tag(update)
def update_temporary_name(system, update) do
system
|> WandererApp.Api.MapSystem.update_temporary_name(update)
end
def update_labels(system, update),
do:
system

View File

@@ -0,0 +1,47 @@
defmodule WandererAppWeb.CharacterActivity do
use WandererAppWeb, :live_component
use LiveViewEvents
@impl true
def mount(socket) do
{:ok, socket}
end
@impl true
def update(
assigns,
socket
) do
{:ok,
socket
|> handle_info_or_assign(assigns)}
end
def render(assigns) do
~H"""
<div id={@id}>
<.table class="!max-h-[80vh] !overflow-y-auto" id="activity-tbl" rows={@activity}>
<:col :let={row} label="Character">
<.character_item character={row.character} />
</:col>
<:col :let={row} label="Passages">
<%= row.count %>
</:col>
</.table>
</div>
"""
end
def character_item(assigns) do
~H"""
<div class="flex items-center gap-3">
<div class="avatar">
<div class="rounded-md w-12 h-12">
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
</div>
</div>
<%= @character.name %>
</div>
"""
end
end

View File

@@ -393,6 +393,7 @@ defmodule WandererAppWeb.CoreComponents do
data-pc-name="checkbox"
data-pc-section="root"
>
<input type="hidden" name={@name} value="false" disabled={@rest[:disabled]} />
<input
id={@id}
name={@name}

View File

@@ -0,0 +1,249 @@
defmodule WandererAppWeb.APIController do
use WandererAppWeb, :controller
import Ash.Query, only: [filter: 2]
alias WandererApp.Api
alias WandererApp.MapSystemRepo
alias WandererApp.MapCharacterSettingsRepo
alias WandererApp.Api.Character
# -----------------------------------------------------------------
# SYSTEMS
# -----------------------------------------------------------------
@doc """
GET /api/systems
Requires either `?map_id=<UUID>` **OR** `?slug=<map-slug>` in the query params.
If `?all=true` is provided, **all** systems are returned.
Otherwise, only "visible" systems are returned.
Examples:
GET /api/systems?map_id=466e922b-e758-485e-9b86-afae06b88363
GET /api/systems?slug=my-unique-wormhole-map
GET /api/systems?map_id=<UUID>&all=true
"""
def list_systems(conn, params) do
with {:ok, map_id} <- fetch_map_id(params) do
# Decide which function to call based on the "all" param
repo_fun =
if params["all"] == "true" do
&MapSystemRepo.get_all_by_map/1
else
&MapSystemRepo.get_visible_by_map/1
end
case repo_fun.(map_id) do
{:ok, systems} ->
data = Enum.map(systems, &map_system_to_json/1)
json(conn, %{data: data})
{:error, reason} ->
conn
|> put_status(:not_found)
|> json(%{error: "Could not fetch systems for map_id=#{map_id}: #{inspect(reason)}"})
end
else
{:error, msg} ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
end
end
@doc """
GET /api/system
Requires 'id' (the solar_system_id)
plus either ?map_id=<UUID> or ?slug=<map-slug>.
Example:
GET /api/system?id=31002229&map_id=466e922b-e758-485e-9b86-afae06b88363
GET /api/system?id=31002229&slug=my-unique-wormhole-map
"""
def show_system(conn, params) do
with {:ok, solar_system_str} <- require_param(params, "id"),
{:ok, solar_system_id} <- parse_int(solar_system_str),
{:ok, map_id} <- fetch_map_id(params) do
case MapSystemRepo.get_by_map_and_solar_system_id(map_id, solar_system_id) do
{:ok, system} ->
data = map_system_to_json(system)
json(conn, %{data: data})
{:error, :not_found} ->
conn
|> put_status(:not_found)
|> json(%{error: "System not found in map=#{map_id}"})
end
else
{:error, msg} ->
conn
|> put_status(:bad_request)
|> json(%{error: msg})
end
end
# -----------------------------------------------------------------
# Characters
# -----------------------------------------------------------------
@doc """
GET /api/tracked_characters_with_info
Example usage:
GET /api/tracked_characters_with_info?map_id=<uuid>
GET /api/tracked_characters_with_info?slug=<map-slug>
Returns a list of tracked records, plus their fully-loaded `character` data.
"""
def tracked_characters_with_info(conn, params) do
with {:ok, map_id} <- fetch_map_id(params),
{:ok, settings_list} <- get_tracked_by_map_ids(map_id),
{:ok, char_list} <-
read_characters_by_ids_wrapper(Enum.map(settings_list, & &1.character_id)) do
chars_by_id = Map.new(char_list, &{&1.id, &1})
data =
Enum.map(settings_list, fn setting ->
found_char = Map.get(chars_by_id, setting.character_id)
%{
id: setting.id,
map_id: setting.map_id,
character_id: setting.character_id,
tracked: setting.tracked,
inserted_at: setting.inserted_at,
updated_at: setting.updated_at,
character:
if found_char do
character_to_json(found_char)
else
%{}
end
}
end)
json(conn, %{data: data})
else
{:error, :get_tracked_error, reason} ->
conn
|> put_status(:not_found)
|> json(%{error: "No tracked records found for map_id: #{inspect(reason)}"})
{:error, :read_characters_by_ids_error, reason} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "Could not load Character records: #{inspect(reason)}"})
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{error: message})
end
end
defp get_tracked_by_map_ids(map_id) do
case MapCharacterSettingsRepo.get_tracked_by_map_all(map_id) do
{:ok, settings_list} ->
{:ok, settings_list}
{:error, reason} ->
{:error, :get_tracked_error, reason}
end
end
defp read_characters_by_ids_wrapper(ids) do
case read_characters_by_ids(ids) do
{:ok, char_list} ->
{:ok, char_list}
{:error, reason} ->
{:error, :read_characters_by_ids_error, reason}
end
end
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
{:ok, mid}
end
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
case WandererApp.Api.Map.get_map_by_slug(slug) do
{:ok, map} ->
{:ok, map.id}
{:error, _reason} ->
{:error, "No map found for slug=#{slug}"}
end
end
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
defp require_param(params, key) do
case params[key] do
nil -> {:error, "Missing required param: #{key}"}
"" -> {:error, "Param #{key} cannot be empty"}
val -> {:ok, val}
end
end
defp parse_int(str) do
case Integer.parse(str) do
{num, ""} -> {:ok, num}
_ -> {:error, "Invalid integer for param id=#{str}"}
end
end
defp read_characters_by_ids(ids) when is_list(ids) do
if ids == [] do
{:ok, []}
else
query =
Character
|> filter(id in ^ids)
Api.read(query)
end
end
defp map_system_to_json(system) do
%{
id: system.id,
map_id: system.map_id,
solar_system_id: system.solar_system_id,
name: system.name,
custom_name: system.custom_name,
temporary_name: system.temporary_name,
description: system.description,
tag: system.tag,
labels: system.labels,
locked: system.locked,
visible: system.visible,
status: system.status,
position_x: system.position_x,
position_y: system.position_y,
inserted_at: system.inserted_at,
updated_at: system.updated_at
}
end
defp character_to_json(ch) do
%{
id: ch.id,
eve_id: ch.eve_id,
name: ch.name,
corporation_id: ch.corporation_id,
corporation_name: ch.corporation_name,
corporation_ticker: ch.corporation_ticker,
alliance_id: ch.alliance_id,
alliance_name: ch.alliance_name,
alliance_ticker: ch.alliance_ticker,
inserted_at: ch.inserted_at,
updated_at: ch.updated_at
}
end
end

View File

@@ -0,0 +1,15 @@
defmodule WandererAppWeb.Plugs.CheckApiDisabled do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
if WandererApp.Env.public_api_disabled?() do
conn
|> send_resp(403, "Public API is disabled")
|> halt()
else
conn
end
end
end

View File

@@ -0,0 +1,62 @@
defmodule WandererAppWeb.Plugs.CheckMapApiKey do
@moduledoc """
A plug that checks the "Authorization: Bearer <token>" header
against the maps stored public_api_key. Halts with 401 if invalid.
"""
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
header = get_req_header(conn, "authorization") |> List.first()
case header do
"Bearer " <> incoming_token ->
case fetch_map_id(conn.query_params) do
{:ok, map_id} ->
case WandererApp.Api.Map.by_id(map_id) do
{:ok, map} ->
if map.public_api_key == incoming_token do
conn
else
conn
|> send_resp(401, "Unauthorized (invalid token for map)")
|> halt()
end
{:error, _reason} ->
conn
|> send_resp(404, "Map not found")
|> halt()
end
{:error, msg} ->
conn
|> send_resp(400, msg)
|> halt()
end
_ ->
conn
|> send_resp(401, "Missing or invalid 'Bearer' token")
|> halt()
end
end
defp fetch_map_id(%{"map_id" => mid}) when is_binary(mid) and mid != "" do
{:ok, mid}
end
defp fetch_map_id(%{"slug" => slug}) when is_binary(slug) and slug != "" do
case WandererApp.Api.Map.get_map_by_slug(slug) do
{:ok, map} ->
{:ok, map.id}
{:error, _reason} ->
{:error, "No map found for slug=#{slug}"}
end
end
defp fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
end

View File

@@ -18,15 +18,12 @@ defmodule WandererAppWeb.MapActivityEventHandler do
do: MapCoreEventHandler.handle_server_event(event, socket)
def handle_ui_event("show_activity", _, %{assigns: %{map_id: map_id}} = socket) do
Task.async(fn ->
{:ok, character_activity} = map_id |> get_character_activity()
{:character_activity, character_activity}
end)
{:noreply,
socket
|> assign(:show_activity?, true)}
|> assign(:show_activity?, true)
|> assign_async(:character_activity, fn ->
map_id |> get_character_activity()
end)}
end
def handle_ui_event("hide_activity", _, socket),
@@ -44,6 +41,6 @@ defmodule WandererAppWeb.MapActivityEventHandler do
%{p | character: p.character |> MapEventHandler.map_ui_character_stat()}
end)
{:ok, %{jumps: jumps}}
{:ok, %{character_activity: jumps}}
end
end

View File

@@ -192,16 +192,15 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
end
def handle_ui_event(
"toggle_follow",
%{"character-id" => clicked_char_id},
%{
assigns: %{
map_id: map_id,
current_user: current_user
}
} = socket
) do
"toggle_follow",
%{"character-id" => clicked_char_id},
%{
assigns: %{
map_id: map_id,
current_user: current_user
}
} = socket
) do
{:ok, all_settings} = WandererApp.MapCharacterSettingsRepo.get_all_by_map(map_id)
# Find and filter user's characters
@@ -217,7 +216,7 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
existing = Enum.find(my_settings, &(&1.character_id == clicked_char_id))
{:ok, target_setting} =
if existing do
if not is_nil(existing) do
{:ok, existing}
else
WandererApp.MapCharacterSettingsRepo.create(%{
@@ -276,7 +275,6 @@ defmodule WandererAppWeb.MapCharactersEventHandler do
{:noreply, socket}
end
def handle_ui_event("hide_tracking", _, socket),
do: {:noreply, socket |> assign(show_tracking?: false)}

View File

@@ -162,7 +162,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
socket
)
def handle_ui_event("toggle_follow_" <> character_id, _, socket),
def handle_ui_event("toggle_follow_" <> character_id, _, socket),
do:
MapCharactersEventHandler.handle_ui_event(
"toggle_follow",
@@ -234,7 +234,7 @@ defmodule WandererAppWeb.MapCoreEventHandler do
|> MapCharactersEventHandler.add_character()}
def handle_ui_event(event, body, socket) do
Logger.warning(fn -> "unhandled map ui event: #{event} #{inspect(body)}" end)
Logger.warning(fn -> "unhandled map ui event: #{inspect(event)} #{inspect(body)}" end)
{:noreply, socket}
end

View File

@@ -231,6 +231,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
"labels" -> :update_system_labels
"locked" -> :update_system_locked
"tag" -> :update_system_tag
"temporary_name" -> :update_system_temporary_name
"status" -> :update_system_status
_ -> nil
end
@@ -242,6 +243,7 @@ defmodule WandererAppWeb.MapSystemsEventHandler do
"labels" -> :labels
"locked" -> :locked
"tag" -> :tag
"temporary_name" -> :temporary_name
"status" -> :status
_ -> :none
end

View File

@@ -50,6 +50,7 @@ defmodule WandererAppWeb.MapEventHandler do
"update_system_labels",
"update_system_locked",
"update_system_tag",
"update_system_temporary_name",
"update_system_status"
]
@@ -244,6 +245,7 @@ defmodule WandererAppWeb.MapEventHandler do
locked: locked,
tag: tag,
labels: labels,
temporary_name: temporary_name,
status: status,
visible: visible
} = _system,
@@ -269,6 +271,7 @@ defmodule WandererAppWeb.MapEventHandler do
locked: locked,
status: status,
tag: tag,
temporary_name: temporary_name,
visible: visible
}
end

View File

@@ -77,13 +77,14 @@ defmodule WandererAppWeb.MapLive do
def handle_info(:not_all_characters_tracked, %{assigns: %{map_slug: map_slug}} = socket),
do:
WandererAppWeb.MapEventHandler.handle_ui_event(
%{event: "add_character"},
"add_character",
nil,
socket
|> put_flash(
:error,
"You should enable tracking for all characters that have access to this map first!"
)
|> push_navigate(to: ~p"/tracking/#{map_slug}")
)
@impl true
@@ -101,17 +102,4 @@ defmodule WandererAppWeb.MapLive do
socket
|> assign(:active_page, :map)
end
def character_item(assigns) do
~H"""
<div class="flex items-center gap-3">
<div class="avatar">
<div class="rounded-md w-12 h-12">
<img src={member_icon_url(@character.eve_id)} alt={@character.name} />
</div>
</div>
<%= @character.name %>
</div>
"""
end
end

View File

@@ -31,6 +31,29 @@
</.link>
</div>
<.modal
:if={assigns |> Map.get(:show_activity?, false)}
id="map-activity-modal"
title="Activity of Characters"
class="!w-[500px]"
show
on_cancel={JS.push("hide_activity")}
>
<.async_result :let={character_activity} assign={@character_activity}>
<:loading>Loading...</:loading>
<:failed :let={reason}><%= reason %></:failed>
<span :if={character_activity}>
<.live_component
module={WandererAppWeb.CharacterActivity}
id="character-activity"
activity={character_activity}
notify_to={self()}
/>
</span>
</.async_result>
</.modal>
<.modal
:if={assigns |> Map.get(:show_tracking?, false)}
id="map-tracking-modal"

View File

@@ -127,6 +127,7 @@ defmodule WandererAppWeb.MapsLive do
|> assign(:page_title, "Maps - Settings")
|> assign(:map_slug, map_slug)
|> assign(:map_id, map.id)
|> assign(:public_api_key, map.public_api_key)
|> assign(:map, map)
|> assign(
export_settings: export_settings |> _get_export_map_data(),
@@ -179,6 +180,19 @@ defmodule WandererAppWeb.MapsLive do
{:noreply, socket}
end
def handle_event("generate-map-api-key", _params, socket) do
new_api_key = UUID.uuid4()
map = WandererApp.Api.Map.by_id!(socket.assigns.map_id)
{:ok, _updated_map} =
WandererApp.Api.Map.update_api_key(map, %{public_api_key: new_api_key})
{:noreply, assign(socket, public_api_key: new_api_key)}
end
@impl true
def handle_event(
"live_select_change",
@@ -203,7 +217,10 @@ defmodule WandererAppWeb.MapsLive do
socket.assigns.form,
form
|> Map.put("acls", form["acls"] || [])
|> Map.put("only_tracked_characters", form["only_tracked_characters"] || false)
|> Map.put(
"only_tracked_characters",
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
)
)
{:noreply, socket |> assign(form: form)}
@@ -593,7 +610,13 @@ defmodule WandererAppWeb.MapsLive do
scope -> scope
end
form = form |> Map.put("scope", scope)
form =
form
|> Map.put("scope", scope)
|> Map.put(
"only_tracked_characters",
(form["only_tracked_characters"] || "false") |> String.to_existing_atom()
)
map
|> WandererApp.Api.Map.update(form)
@@ -659,7 +682,12 @@ defmodule WandererAppWeb.MapsLive do
) do
options =
options_form
|> Map.take(["layout", "store_custom_labels", "restrict_offline_showing"])
|> Map.take([
"layout",
"store_custom_labels",
"show_temp_system_name",
"restrict_offline_showing"
])
{:ok, updated_map} = WandererApp.MapRepo.update_options(map, options)

View File

@@ -313,6 +313,32 @@
style="width: 146px; left: 0px;"
>
</li>
<li
:if={not WandererApp.Env.public_api_disabled?()}
class={[
"p-unselectable-text",
classes("p-tabview-selected p-highlight": @active_settings_tab == "public_api")
]}
role="presentation"
data-pc-name=""
data-pc-section="header"
>
<a
role="tab"
class="p-tabview-nav-link flex p-[10px]"
tabindex="-1"
aria-controls="pr_id_335_content"
aria-selected="false"
aria-disabled="false"
data-pc-section="headeraction"
phx-click="change_settings_tab"
phx-value-tab="public_api"
>
<span class="p-tabview-title" data-pc-section="headertitle">
<.icon name="hero-globe-alt-solid" class="w-4 h-4" />&nbsp;Public Api
</span>
</a>
</li>
</ul>
</div>
</div>
@@ -345,6 +371,11 @@
field={f[:store_custom_labels]}
label="Store system custom labels"
/>
<.input
type="checkbox"
field={f[:show_temp_system_name]}
label="Allow Temporary System Names"
/>
<.input
type="checkbox"
field={f[:restrict_offline_showing]}
@@ -377,6 +408,52 @@
</.button>
</div>
<div :if={@active_settings_tab == "public_api"and not WandererApp.Env.public_api_disabled?()} class="p-6">
<h2 class="text-lg font-semibold mb-4">Public API</h2>
<div class="flex flex-col gap-3 items-start w-full">
<div>
<input
:if={not is_nil(@public_api_key)}
class="input input-bordered text-sm truncate bg-neutral-800 text-white w-[350px]"
readonly
type="text"
value={@public_api_key}
/>
<input
:if={is_nil(@public_api_key)}
class="input input-bordered text-sm truncate bg-neutral-800 text-gray-400 w-[350px]"
readonly
type="text"
placeholder="No Public API Key yet"
/>
</div>
<div class="flex items-center gap-2">
<.button
class="btn btn-primary rounded-md"
phx-click="generate-map-api-key"
>
Generate
</.button>
<.button
phx-hook="CopyToClipboard"
id="copy-map-api-key"
data-url={@public_api_key}
disabled={is_nil(@public_api_key)}
class={
if is_nil(@public_api_key) do
"copy-link btn rounded-md transition-colors duration-300
bg-gray-500 hover:bg-gray-500 text-gray-300 cursor-not-allowed"
else
"copy-link btn rounded-md transition-colors duration-300
bg-blue-600 hover:bg-blue-700 text-white cursor-pointer"
end
}
>
Copy
</.button>
</div>
</div>
</div>
<div :if={@active_settings_tab == "balance"}>
<div class="stats w-full bg-primary text-primary-content">
<div class="stat">

View File

@@ -106,6 +106,23 @@ defmodule WandererAppWeb.Router do
pipeline :api do
plug(:accepts, ["json"])
plug WandererAppWeb.Plugs.CheckApiDisabled
plug WandererAppWeb.Plugs.CheckMapApiKey
end
if not WandererApp.Env.public_api_disabled?() do
scope "/api", WandererAppWeb do
pipe_through [:api]
# GET /api/systems?map_id=... or ?slug=...
get "/systems", APIController, :list_systems
# GET /api/system?id=... plus either map_id=... or slug=...
get "/system", APIController, :show_system
# GET /api/characters?map_id=... or slug=...
get "/characters", APIController, :tracked_characters_with_info
end
end
scope "/", WandererAppWeb do

View File

@@ -2,7 +2,7 @@ defmodule WandererApp.MixProject do
use Mix.Project
@source_url "https://github.com/wanderer-industries/wanderer"
@version "1.32.2"
@version "1.35.0"
def project do
[

View File

@@ -0,0 +1,161 @@
%{
title: "User Guide: Public API Endpoints for Map Data",
author: "Wanderer Team",
cover_image_uri: "/images/news/01-05-map-public-api/generate-key.png",
tags: ~w(map public-api guide interface),
description: "Learn how to use the Wanderer public API endpoints to retrieve system and character data from your map. This guide covers available endpoints, request examples, and sample responses."
}
---
## Introduction
As part of the Wanderer platform, a public API has been introduced to help users programmatically retrieve map data, such as system information and character tracking details. This guide explains how to use these endpoints, how to authenticate with the API, and what data to expect in the responses.
**Important:** To use these endpoints, you need a valid API key for the map in question. You can generate or copy this key from within the **Map Settings** modal in the app:
![Generate Map API Key](/images/news/01-05-map-public-api/generate-key.png "Generate Map API Key")
---
## Authentication
Each request to the Wanderer API must include a valid API key in the `Authorization` header. The format is:
Authorization: Bearer <YOUR_MAP_API_KEY>
If the API key is missing or incorrect, youll receive a `401 Unauthorized` response.
---
## Endpoints Overview
### 1. List Systems
GET /api/systems?map_id=<UUID>
GET /api/systems?slug=<map-slug>
- **Description:** Retrieves a list of systems associated with the specified map (by `map_id` or `slug`).
- **Authentication:** Required via `Authorization` header.
- **Parameters:**
- `map_id` (optional if `slug` is provided) — the UUID of the map.
- `slug` (optional if `map_id` is provided) — the slug identifier of the map.
- `all=true` (optional) — if set, returns *all* systems instead of only "visible" systems.
#### Example Request
```
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/systems?slug=some-slug"
```
#### Example Response
```
{
"data": [
{
"id": "<REDACTED_ID>",
"name": "<REDACTED_NAME>",
"status": 0,
"tag": null,
"visible": false,
"description": null,
"labels": "<REDACTED_JSON>",
"inserted_at": "2025-01-01T13:38:42.875843Z",
"updated_at": "2025-01-01T13:40:16.750234Z",
"locked": false,
"solar_system_id": <REDACTED_NUMBER>,
"map_id": "<REDACTED_ID>",
"custom_name": null,
"position_x": 1125,
"position_y": -285
},
...
]
}
```
---
### 2. Show Single System
GET /api/system?id=<SOLAR_SYSTEM_ID>&map_id=<UUID>
GET /api/system?id=<SOLAR_SYSTEM_ID>&slug=<map-slug>
- **Description:** Retrieves information for a specific system on the specified map. You must provide:
- `id` (the `solar_system_id`).
- Either `map_id` or `slug`.
- **Authentication:** Required via `Authorization` header.
#### Example Request
```
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/system?id=<REDACTED_NUMBER>&slug=<REDACTED_SLUG>"
```
#### Example Response
```
{
"data": {
"id": "<REDACTED_ID>",
"name": "<REDACTED_NAME>",
"status": 0,
"tag": null,
"visible": false,
"description": null,
"labels": "<REDACTED_JSON>",
"inserted_at": "2025-01-03T06:30:02.069090Z",
"updated_at": "2025-01-03T07:47:07.471051Z",
"locked": false,
"solar_system_id": <REDACTED_NUMBER>,
"map_id": "<REDACTED_ID>",
"custom_name": null,
"position_x": 1005,
"position_y": 765
}
}
```
---
### 3. List Tracked Characters
GET /api/characters?map_id=<UUID>
GET /api/characters?slug=<map-slug>
- **Description:** Retrieves a list of tracked characters for the specified map (by `map_id` or `slug`), including metadata such as corporation/alliance details.
- **Authentication:** Required via `Authorization` header.
#### Example Request
```
curl -H "Authorization: Bearer <REDACTED_TOKEN>" "https://wanderer.example.com/api/characters?slug=some-slug"
```
#### Example Response
```
{
"data": [
{
"id": "<REDACTED_ID>",
"character": {
"id": "<REDACTED_ID>",
"name": "<REDACTED_NAME>",
"inserted_at": "2025-01-01T05:24:18.461721Z",
"updated_at": "2025-01-03T07:45:52.294052Z",
"alliance_id": "<REDACTED>",
"alliance_name": "<REDACTED>",
"alliance_ticker": "<REDACTED>",
"corporation_id": "<REDACTED>",
"corporation_name": "<REDACTED>",
"corporation_ticker": "<REDACTED>",
"eve_id": "<REDACTED>"
},
"tracked": true,
"map_id": "<REDACTED_ID>"
},
...
]
}
```
---
## Conclusion
Using these APIs, you can programmatically retrieve system and character information from your map. Whether youre building a custom analytics dashboard, a corp management tool, or just want to explore data outside the standard UI, these endpoints provide a straightforward way to fetch up-to-date map details.
For questions or additional support, please reach out to the Wanderer Team.
Fly safe,
WANDERER TEAM

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.AddTemporaryName do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:map_system_v1) do
add :temporary_name, :text
end
end
def down do
alter table(:map_system_v1) do
remove :temporary_name
end
end
end

View File

@@ -0,0 +1,21 @@
defmodule WandererApp.Repo.Migrations.AddPublicApiKey do
@moduledoc """
Updates resources based on their most recent snapshots.
This file was autogenerated with `mix ash_postgres.generate_migrations`
"""
use Ecto.Migration
def up do
alter table(:maps_v1) do
add :public_api_key, :text
end
end
def down do
alter table(:maps_v1) do
remove :public_api_key
end
end
end