mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-01 13:33:02 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98a03d1e59 | ||
|
|
2088393c79 | ||
|
|
093042b88a | ||
|
|
e5ef35c186 | ||
|
|
1cd23d5efd | ||
|
|
ead5818a3f | ||
|
|
a5f66ada68 | ||
|
|
0919742853 | ||
|
|
f3efffd259 | ||
|
|
f85317983c | ||
|
|
76f709b768 | ||
|
|
e3b2356302 | ||
|
|
3d810211ee | ||
|
|
7453795dc5 | ||
|
|
9de7cd99ee | ||
|
|
51489c1aa5 | ||
|
|
25dd6de770 |
@@ -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"
|
||||
|
||||
73
CHANGELOG.md
73
CHANGELOG.md
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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');
|
||||
})
|
||||
|
||||
BIN
assets/static/images/news/01-05-map-public-api/generate-key.png
Normal file
BIN
assets/static/images/news/01-05-map-public-api/generate-key.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -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: %{
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
47
lib/wanderer_app_web/components/character_activity.ex
Normal file
47
lib/wanderer_app_web/components/character_activity.ex
Normal 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
|
||||
@@ -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}
|
||||
|
||||
249
lib/wanderer_app_web/controllers/api_controller.ex
Normal file
249
lib/wanderer_app_web/controllers/api_controller.ex
Normal 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
|
||||
15
lib/wanderer_app_web/controllers/plugs/check_api_disabled.ex
Normal file
15
lib/wanderer_app_web/controllers/plugs/check_api_disabled.ex
Normal 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
|
||||
62
lib/wanderer_app_web/controllers/plugs/check_map_api_key.ex
Normal file
62
lib/wanderer_app_web/controllers/plugs/check_map_api_key.ex
Normal file
@@ -0,0 +1,62 @@
|
||||
defmodule WandererAppWeb.Plugs.CheckMapApiKey do
|
||||
@moduledoc """
|
||||
A plug that checks the "Authorization: Bearer <token>" header
|
||||
against the map’s 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
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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" /> 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">
|
||||
|
||||
@@ -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
|
||||
|
||||
2
mix.exs
2
mix.exs
@@ -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
|
||||
[
|
||||
|
||||
161
priv/posts/2025/01-05-map-public-api.md
Normal file
161
priv/posts/2025/01-05-map-public-api.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## 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, you’ll 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 you’re 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
|
||||
21
priv/repo/migrations/20250103005559_add_temporary_name.exs
Normal file
21
priv/repo/migrations/20250103005559_add_temporary_name.exs
Normal 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
|
||||
21
priv/repo/migrations/20250103121539_add_public_api_key.exs
Normal file
21
priv/repo/migrations/20250103121539_add_public_api_key.exs
Normal 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
|
||||
Reference in New Issue
Block a user