mirror of
https://github.com/wanderer-industries/wanderer
synced 2026-03-14 15:46:00 +00:00
Compare commits
1 Commits
main
...
feat-conte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18d50329bc |
@@ -2,15 +2,6 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.96.6](https://github.com/wanderer-industries/wanderer/compare/v1.96.5...v1.96.6) (2026-03-13)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: Fixed tracking issues
|
||||
|
||||
## [v1.96.5](https://github.com/wanderer-industries/wanderer/compare/v1.96.4...v1.96.5) (2026-02-27)
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
import {
|
||||
MASS_STATE_NAMES,
|
||||
MASS_STATE_NAMES_ORDER,
|
||||
SHIP_SIZES_NAMES,
|
||||
SHIP_SIZES_NAMES_ORDER,
|
||||
SHIP_SIZES_NAMES_SHORT,
|
||||
SHIP_SIZES_SIZE,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
import { ConnectionType, MassState, ShipSizeStatus, SolarSystemConnection, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { PrimeIcons } from 'primereact/api';
|
||||
@@ -14,6 +6,8 @@ import { MenuItem } from 'primereact/menuitem';
|
||||
import React, { RefObject, useMemo } from 'react';
|
||||
import { Edge } from 'reactflow';
|
||||
import { LifetimeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/LifetimeActionsWrapper.tsx';
|
||||
import { MassStatusActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/MassStatusActionsWrapper.tsx';
|
||||
import { ShipSizeActionsWrapper } from '@/hooks/Mapper/components/map/components/ContextMenuConnection/ShipSizeActionsWrapper.tsx';
|
||||
import classes from './ContextMenuConnection.module.scss';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic.ts';
|
||||
import { isNullsecSpace } from '@/hooks/Mapper/components/map/helpers/isKnownSpace.ts';
|
||||
@@ -86,16 +80,28 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
return <LifetimeActionsWrapper lifetime={edge.data?.time_status} onChangeLifetime={onChangeTimeState} />;
|
||||
},
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
className: clsx(classes.FastActions, '!h-[54px]'),
|
||||
template: () => {
|
||||
return (
|
||||
<MassStatusActionsWrapper
|
||||
massStatus={edge.data?.mass_status}
|
||||
onChangeMassStatus={onChangeMassState}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: `Frigate`,
|
||||
className: clsx({
|
||||
[classes.ConnectionFrigate]: isFrigateSize,
|
||||
}),
|
||||
icon: PrimeIcons.CLOUD,
|
||||
command: () =>
|
||||
onChangeShipSizeStatus(
|
||||
edge.data?.ship_size_type === ShipSizeStatus.small ? ShipSizeStatus.large : ShipSizeStatus.small,
|
||||
),
|
||||
className: clsx(classes.FastActions, '!h-[64px]'),
|
||||
template: () => {
|
||||
return (
|
||||
<ShipSizeActionsWrapper shipSize={edge.data?.ship_size_type} onChangeShipSize={onChangeShipSizeStatus} />
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: `Save mass`,
|
||||
@@ -105,41 +111,6 @@ export const ContextMenuConnection: React.FC<ContextMenuConnectionProps> = ({
|
||||
icon: PrimeIcons.LOCK,
|
||||
command: () => onToggleMassSave(!edge.data?.locked),
|
||||
},
|
||||
...(!isFrigateSize
|
||||
? [
|
||||
{
|
||||
label: `Mass status`,
|
||||
icon: PrimeIcons.CHART_PIE,
|
||||
items: MASS_STATE_NAMES_ORDER.map(x => ({
|
||||
label: MASS_STATE_NAMES[x],
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.mass_status === x,
|
||||
}),
|
||||
command: () => onChangeMassState(x),
|
||||
})),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: `Ship Size`,
|
||||
icon: PrimeIcons.CLOUD,
|
||||
items: SHIP_SIZES_NAMES_ORDER.map(x => ({
|
||||
label: (
|
||||
<div className="grid grid-cols-[20px_120px_1fr_40px] gap-2 items-center">
|
||||
<div className="text-[12px] font-bold text-stone-400">{SHIP_SIZES_NAMES_SHORT[x]}</div>
|
||||
<div>{SHIP_SIZES_NAMES[x]}</div>
|
||||
<div></div>
|
||||
<div className="flex justify-end whitespace-nowrap text-[12px] font-bold text-stone-500">
|
||||
{SHIP_SIZES_SIZE[x]} t.
|
||||
</div>
|
||||
</div>
|
||||
) as unknown as string, // TODO my lovely kostyl
|
||||
className: clsx({
|
||||
[classes.SelectedItem]: edge.data?.ship_size_type === x,
|
||||
}),
|
||||
command: () => onChangeShipSizeStatus(x),
|
||||
})),
|
||||
},
|
||||
...(bothNullsec
|
||||
? [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import {
|
||||
WdMassStatusSelector,
|
||||
WdMassStatusSelectorProps,
|
||||
} from '@/hooks/Mapper/components/ui-kit/WdMassStatusSelector.tsx';
|
||||
|
||||
export const MassStatusActionsWrapper = (props: WdMassStatusSelectorProps) => {
|
||||
return (
|
||||
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
|
||||
<div className="text-[12px] text-stone-500 font-semibold">Mass status:</div>
|
||||
|
||||
<WdMassStatusSelector {...props} />
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { LayoutEventBlocker } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdShipSizeSelector, WdShipSizeSelectorProps } from '@/hooks/Mapper/components/ui-kit/WdShipSizeSelector.tsx';
|
||||
|
||||
export const ShipSizeActionsWrapper = (props: WdShipSizeSelectorProps) => {
|
||||
return (
|
||||
<LayoutEventBlocker className="flex flex-col gap-1 w-[100%] h-full px-2 pt-[4px]">
|
||||
<div className="text-[12px] text-stone-500 font-semibold">Ship size:</div>
|
||||
|
||||
<WdShipSizeSelector {...props} />
|
||||
</LayoutEventBlocker>
|
||||
);
|
||||
};
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
import { SystemsSettingsProvider } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/Provider.tsx';
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { MassState, OutCommand, SignatureGroup, SystemSignature, TimeStatus } from '@/hooks/Mapper/types';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
@@ -15,6 +15,7 @@ type SystemSignaturePrepared = Omit<SystemSignature, 'linked_system'> & {
|
||||
linked_system: string;
|
||||
k162Type: string;
|
||||
time_status: TimeStatus;
|
||||
mass_status: MassState;
|
||||
};
|
||||
|
||||
export interface MapSettingsProps {
|
||||
@@ -59,6 +60,7 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
custom_info: JSON.stringify({
|
||||
k162Type: values.k162Type,
|
||||
time_status: values.time_status,
|
||||
mass_status: values.mass_status,
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -139,16 +141,19 @@ export const SignatureSettings = ({ systemId, show, onHide, signatureData }: Map
|
||||
|
||||
let k162Type = null;
|
||||
let time_status = TimeStatus._24h;
|
||||
let mass_status = MassState.normal;
|
||||
if (custom_info) {
|
||||
const customInfo = JSON.parse(custom_info);
|
||||
k162Type = customInfo.k162Type;
|
||||
time_status = customInfo.time_status;
|
||||
mass_status = customInfo.mass_status ?? MassState.normal;
|
||||
}
|
||||
|
||||
signatureForm.reset({
|
||||
linked_system: linked_system?.solar_system_id.toString() ?? undefined,
|
||||
k162Type: k162Type,
|
||||
time_status: time_status,
|
||||
mass_status: mass_status,
|
||||
...rest,
|
||||
});
|
||||
}, [signatureForm, signatureData]);
|
||||
|
||||
@@ -5,6 +5,7 @@ import { SignatureK162TypeSelect } from '@/hooks/Mapper/components/mapRootConten
|
||||
import { SignatureLeadsToSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLeadsToSelect';
|
||||
import { SignatureLifetimeSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureLifetimeSelect.tsx';
|
||||
import { SignatureTempName } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureTempName.tsx';
|
||||
import { SignatureMassStatusSelect } from '@/hooks/Mapper/components/mapRootContent/components/SignatureSettings/components/SignatureMassStatusSelect.tsx';
|
||||
|
||||
export const SignatureGroupContentWormholes = () => {
|
||||
const { watch } = useFormContext<SystemSignature>();
|
||||
@@ -34,6 +35,11 @@ export const SignatureGroupContentWormholes = () => {
|
||||
<SignatureLifetimeSelect name="time_status" />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>Mass status:</span>
|
||||
<SignatureMassStatusSelect name="mass_status" />
|
||||
</div>
|
||||
|
||||
<label className="grid grid-cols-[100px_250px_1fr] gap-2 items-center text-[14px]">
|
||||
<span>Temp. Name:</span>
|
||||
<SignatureTempName />
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { MassState, SystemSignature } from '@/hooks/Mapper/types';
|
||||
import { WdMassStatusSelector } from '@/hooks/Mapper/components/ui-kit/WdMassStatusSelector.tsx';
|
||||
|
||||
export interface SignatureMassStatusSelectProps {
|
||||
name: string;
|
||||
defaultValue?: MassState;
|
||||
}
|
||||
|
||||
export const SignatureMassStatusSelect = ({
|
||||
name,
|
||||
defaultValue = MassState.normal,
|
||||
}: SignatureMassStatusSelectProps) => {
|
||||
const { control } = useFormContext<SystemSignature>();
|
||||
|
||||
return (
|
||||
<div className="my-1">
|
||||
<Controller
|
||||
// @ts-ignore
|
||||
name={name}
|
||||
control={control}
|
||||
defaultValue={defaultValue}
|
||||
render={({ field }) => {
|
||||
// @ts-ignore
|
||||
return <WdMassStatusSelector massStatus={field.value} onChangeMassStatus={e => field.onChange(e)} />;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,3 +2,4 @@ export * from './SignatureGroupSelect';
|
||||
export * from './SignatureGroupContent';
|
||||
export * from './SignatureK162TypeSelect';
|
||||
export * from './SignatureLifetimeSelect';
|
||||
export * from './SignatureMassStatusSelect';
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
|
||||
import { MassState } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
|
||||
|
||||
const MASS_STATUS = [
|
||||
{
|
||||
id: MassState.verge,
|
||||
label: 'Verge',
|
||||
className: 'bg-red-400 hover:!bg-red-400',
|
||||
inactiveClassName: 'bg-red-400/30',
|
||||
description: 'Mass status: Verge of collapse',
|
||||
},
|
||||
{
|
||||
id: MassState.half,
|
||||
label: 'Half',
|
||||
className: 'bg-orange-300 hover:!bg-orange-300',
|
||||
inactiveClassName: 'bg-orange-300/30',
|
||||
description: 'Mass status: Half',
|
||||
},
|
||||
{
|
||||
id: MassState.normal,
|
||||
label: 'Normal',
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
description: 'Mass status: Normal',
|
||||
},
|
||||
];
|
||||
|
||||
export interface WdMassStatusSelectorProps {
|
||||
massStatus?: MassState;
|
||||
onChangeMassStatus(massStatus: MassState): void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WdMassStatusSelector = ({
|
||||
massStatus = MassState.normal,
|
||||
onChangeMassStatus,
|
||||
className,
|
||||
}: WdMassStatusSelectorProps) => {
|
||||
return (
|
||||
<form>
|
||||
<div className={clsx('grid grid-cols-[auto_auto_auto] gap-1', className)}>
|
||||
{MASS_STATUS.map(x => (
|
||||
<WdButton
|
||||
key={x.id}
|
||||
outlined={false}
|
||||
value={x.label}
|
||||
tooltip={x.description}
|
||||
tooltipOptions={BUILT_IN_TOOLTIP_OPTIONS}
|
||||
size="small"
|
||||
className={clsx(
|
||||
`py-[1px] justify-center min-w-auto w-auto border-0 text-[12px] font-bold leading-[20px]`,
|
||||
{ [x.inactiveClassName]: massStatus !== x.id },
|
||||
x.className,
|
||||
)}
|
||||
onClick={() => onChangeMassStatus(x.id)}
|
||||
>
|
||||
{x.label}
|
||||
</WdButton>
|
||||
))}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,76 @@
|
||||
import { WdButton } from '@/hooks/Mapper/components/ui-kit/WdButton.tsx';
|
||||
import { ShipSizeStatus } from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
import { BUILT_IN_TOOLTIP_OPTIONS } from './constants.ts';
|
||||
import {
|
||||
SHIP_SIZES_DESCRIPTION,
|
||||
SHIP_SIZES_NAMES,
|
||||
SHIP_SIZES_NAMES_ORDER,
|
||||
SHIP_SIZES_NAMES_SHORT,
|
||||
SHIP_SIZES_SIZE,
|
||||
} from '@/hooks/Mapper/components/map/constants.ts';
|
||||
|
||||
const SHIP_SIZE_STYLES: Record<ShipSizeStatus, { className: string; inactiveClassName: string }> = {
|
||||
[ShipSizeStatus.small]: {
|
||||
className: 'bg-indigo-400 hover:!bg-indigo-400',
|
||||
inactiveClassName: 'bg-indigo-400/30',
|
||||
},
|
||||
[ShipSizeStatus.medium]: {
|
||||
className: 'bg-cyan-500 hover:!bg-cyan-500',
|
||||
inactiveClassName: 'bg-cyan-500/30',
|
||||
},
|
||||
[ShipSizeStatus.large]: {
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
},
|
||||
[ShipSizeStatus.freight]: {
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
},
|
||||
[ShipSizeStatus.capital]: {
|
||||
className: 'bg-indigo-300 hover:!bg-indigo-300',
|
||||
inactiveClassName: 'bg-indigo-300/30',
|
||||
},
|
||||
};
|
||||
|
||||
export interface WdShipSizeSelectorProps {
|
||||
shipSize?: ShipSizeStatus;
|
||||
onChangeShipSize(shipSize: ShipSizeStatus): void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WdShipSizeSelector = ({
|
||||
shipSize = ShipSizeStatus.large,
|
||||
onChangeShipSize,
|
||||
className,
|
||||
}: WdShipSizeSelectorProps) => {
|
||||
return (
|
||||
<form>
|
||||
<div className={clsx('grid grid-cols-[1fr_1fr_1fr_1fr_1fr] gap-1', className)}>
|
||||
{SHIP_SIZES_NAMES_ORDER.map(size => {
|
||||
const style = SHIP_SIZE_STYLES[size];
|
||||
const tooltip = `${SHIP_SIZES_NAMES[size]} • ${SHIP_SIZES_SIZE[size]} t. ${SHIP_SIZES_DESCRIPTION[size]}`;
|
||||
|
||||
return (
|
||||
<WdButton
|
||||
key={size}
|
||||
outlined={false}
|
||||
value={SHIP_SIZES_NAMES_SHORT[size]}
|
||||
tooltip={tooltip}
|
||||
tooltipOptions={BUILT_IN_TOOLTIP_OPTIONS}
|
||||
size="small"
|
||||
className={clsx(
|
||||
`py-[1px] justify-center min-w-auto w-auto border-0 text-[11px] font-bold leading-[20px]`,
|
||||
{ [style.inactiveClassName]: shipSize !== size },
|
||||
style.className,
|
||||
)}
|
||||
onClick={() => onChangeShipSize(size)}
|
||||
>
|
||||
<span className="text-[11px] font-bold">{SHIP_SIZES_NAMES_SHORT[size]}</span>
|
||||
</WdButton>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -31,6 +31,7 @@ export type SignatureCustomInfo = {
|
||||
k162Type?: string;
|
||||
time_status?: number;
|
||||
isCrit?: boolean;
|
||||
mass_status?: number;
|
||||
};
|
||||
|
||||
export type SystemSignature = {
|
||||
|
||||
@@ -215,11 +215,8 @@ defmodule WandererApp.Character.TrackingUtils do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if a character has permission to be tracked on a map.
|
||||
Returns {:ok, :allowed} or {:error, reason}.
|
||||
"""
|
||||
def check_character_tracking_permission(character, map_id) do
|
||||
# Check if a character has permission to be tracked on a map
|
||||
defp check_character_tracking_permission(character, map_id) do
|
||||
with {:ok, %{acls: acls, owner_id: owner_id}} <-
|
||||
WandererApp.MapRepo.get(map_id,
|
||||
acls: [
|
||||
|
||||
@@ -754,9 +754,6 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
new_expires_at: token.expires_at
|
||||
)
|
||||
|
||||
# Clear any previous invalid_grant failure counter on successful refresh
|
||||
WandererApp.Cache.delete("character:#{character_id}:invalid_grant_count")
|
||||
|
||||
{:ok, _character} =
|
||||
character
|
||||
|> WandererApp.Api.Character.update(%{
|
||||
@@ -789,12 +786,12 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
expires_at_datetime = DateTime.from_unix!(expires_at)
|
||||
time_since_expiry = DateTime.diff(DateTime.utc_now(), expires_at_datetime, :second)
|
||||
|
||||
# Track consecutive invalid_grant failures before permanently invalidating tokens.
|
||||
# EVE SSO can return invalid_grant for transient server issues, so we require
|
||||
# 3 consecutive failures before wiping tokens.
|
||||
fail_key = "character:#{character_id}:invalid_grant_count"
|
||||
count = WandererApp.Cache.lookup!(fail_key, 0) + 1
|
||||
WandererApp.Cache.put(fail_key, count, ttl: :timer.hours(2))
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Invalid grant error during token refresh",
|
||||
character_id: character_id,
|
||||
error_message: error_message,
|
||||
time_since_expiry_seconds: time_since_expiry,
|
||||
original_expires_at: expires_at
|
||||
)
|
||||
|
||||
# Emit telemetry for token refresh failures
|
||||
:telemetry.execute([:wanderer_app, :token, :refresh_failed], %{count: 1}, %{
|
||||
@@ -803,28 +800,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
time_since_expiry: time_since_expiry
|
||||
})
|
||||
|
||||
if count >= 3 do
|
||||
Logger.warning("TOKEN_REFRESH_FAILED: Invalid grant error (#{count}/3, invalidating tokens)",
|
||||
character_id: character_id,
|
||||
error_message: error_message,
|
||||
time_since_expiry_seconds: time_since_expiry,
|
||||
original_expires_at: expires_at
|
||||
)
|
||||
|
||||
WandererApp.Cache.delete(fail_key)
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :invalid_grant}
|
||||
else
|
||||
Logger.warning(
|
||||
"TOKEN_REFRESH_FAILED: Invalid grant error (#{count}/3, deferring invalidation)",
|
||||
character_id: character_id,
|
||||
error_message: error_message,
|
||||
time_since_expiry_seconds: time_since_expiry,
|
||||
original_expires_at: expires_at
|
||||
)
|
||||
|
||||
{:error, :token_refresh_failed}
|
||||
end
|
||||
invalidate_character_tokens(character, character_id, expires_at, scopes)
|
||||
{:error, :invalid_grant}
|
||||
end
|
||||
|
||||
defp handle_refresh_token_result(
|
||||
|
||||
@@ -159,7 +159,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
|
||||
if is_same_user_as_owner do
|
||||
# All characters from the map owner's account have full access
|
||||
{:ok, character_id}
|
||||
:ok
|
||||
else
|
||||
[character_permissions] =
|
||||
WandererApp.Permissions.check_characters_access([character], acls)
|
||||
@@ -179,12 +179,12 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
{:remove_character, character_id, :no_track_permission}
|
||||
|
||||
_ ->
|
||||
{:ok, character_id}
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:ok, character_id}
|
||||
:ok
|
||||
end
|
||||
end,
|
||||
timeout: :timer.seconds(60),
|
||||
@@ -193,26 +193,7 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
)
|
||||
|> Enum.reduce([], fn
|
||||
{:ok, {:remove_character, character_id, reason}}, acc ->
|
||||
# Track consecutive permission failures - only remove after 3 consecutive hourly failures
|
||||
fail_key = "map_#{map_id}:char_#{character_id}:perm_fail_count"
|
||||
count = WandererApp.Cache.lookup!(fail_key, 0) + 1
|
||||
WandererApp.Cache.put(fail_key, count, ttl: :timer.hours(4))
|
||||
|
||||
if count >= 3 do
|
||||
WandererApp.Cache.delete(fail_key)
|
||||
[{character_id, reason} | acc]
|
||||
else
|
||||
Logger.info(
|
||||
"[CharacterCleanup] Character #{character_id} permission fail #{count}/3 on map #{map_id}, deferring removal"
|
||||
)
|
||||
|
||||
acc
|
||||
end
|
||||
|
||||
{:ok, {:ok, character_id}}, acc ->
|
||||
# Character passed permission check - clear any previous failure counter
|
||||
WandererApp.Cache.delete("map_#{map_id}:char_#{character_id}:perm_fail_count")
|
||||
acc
|
||||
[{character_id, reason} | acc]
|
||||
|
||||
{:ok, _result}, acc ->
|
||||
acc
|
||||
@@ -985,48 +966,13 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
character_id: character_id
|
||||
}) do
|
||||
{:ok, %{tracked: false}} ->
|
||||
# Was previously untracked (by system cleanup or user).
|
||||
# If character now has valid tokens, check permissions and auto-restore tracking.
|
||||
# This handles the case where re-auth gives fresh tokens but DB still says tracked: false.
|
||||
if not is_nil(character.access_token) do
|
||||
case WandererApp.Character.TrackingUtils.check_character_tracking_permission(
|
||||
character,
|
||||
map_id
|
||||
) do
|
||||
{:ok, :allowed} ->
|
||||
Logger.info(
|
||||
"[CharactersImpl] Auto-restoring tracking for character #{character_id} on map #{map_id} - " <>
|
||||
"character has valid tokens and permissions after reconnect"
|
||||
)
|
||||
# Was explicitly untracked (e.g., by permission cleanup) - don't re-enable
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Skipping re-track for character #{character_id} on map #{map_id} - " <>
|
||||
"character was explicitly untracked"
|
||||
end)
|
||||
|
||||
add_character(map_id, character, true)
|
||||
|
||||
WandererApp.MapCharacterSettingsRepo.track(%{
|
||||
map_id: map_id,
|
||||
character_id: character_id
|
||||
})
|
||||
|
||||
WandererApp.Character.TrackerManager.update_track_settings(character_id, %{
|
||||
map_id: map_id,
|
||||
track: true
|
||||
})
|
||||
|
||||
_ ->
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Skipping re-track for character #{character_id} on map #{map_id} - " <>
|
||||
"character lacks permissions"
|
||||
end)
|
||||
|
||||
add_character(map_id, character, false)
|
||||
end
|
||||
else
|
||||
Logger.debug(fn ->
|
||||
"[CharactersImpl] Skipping re-track for character #{character_id} on map #{map_id} - " <>
|
||||
"character has no valid access token"
|
||||
end)
|
||||
|
||||
add_character(map_id, character, false)
|
||||
end
|
||||
add_character(map_id, character, false)
|
||||
|
||||
_ ->
|
||||
# New character or already tracked - enable tracking
|
||||
|
||||
Reference in New Issue
Block a user