mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-11 10:15:41 +00:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dee8d0dae8 | ||
|
|
147dd5880e | ||
|
|
69991fff72 | ||
|
|
de4e1f859f | ||
|
|
8e2a19540c | ||
|
|
855c596672 | ||
|
|
36d3c0937b | ||
|
|
d8fb1f78cf | ||
|
|
98fa7e0235 | ||
|
|
e4396fe2f9 | ||
|
|
1c117903f6 | ||
|
|
88ed9cd39e | ||
|
|
b7c0b45c15 | ||
|
|
0874e3c51c | ||
|
|
369b08a9ae | ||
|
|
01192dc637 | ||
|
|
957cbcc561 | ||
|
|
7eb6d093cf | ||
|
|
a23e544a9f |
45
CHANGELOG.md
45
CHANGELOG.md
@@ -2,6 +2,51 @@
|
||||
|
||||
<!-- changelog -->
|
||||
|
||||
## [v1.87.0](https://github.com/wanderer-industries/wanderer/compare/v1.86.1...v1.87.0) (2025-11-25)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* Add support markdown for system description
|
||||
|
||||
## [v1.86.1](https://github.com/wanderer-industries/wanderer/compare/v1.86.0...v1.86.1) (2025-11-25)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* Map: Add ability to see character passage direction in list of passages
|
||||
|
||||
## [v1.86.0](https://github.com/wanderer-industries/wanderer/compare/v1.85.5...v1.86.0) (2025-11-25)
|
||||
|
||||
|
||||
|
||||
|
||||
### Features:
|
||||
|
||||
* add date filter for character activity
|
||||
|
||||
## [v1.85.5](https://github.com/wanderer-industries/wanderer/compare/v1.85.4...v1.85.5) (2025-11-24)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: fixed connections cleanup and rally points delete issues
|
||||
|
||||
## [v1.85.4](https://github.com/wanderer-industries/wanderer/compare/v1.85.3...v1.85.4) (2025-11-22)
|
||||
|
||||
|
||||
|
||||
|
||||
### Bug Fixes:
|
||||
|
||||
* core: invalidate map characters every 1 hour for any missing/revoked permissions
|
||||
|
||||
## [v1.85.3](https://github.com/wanderer-industries/wanderer/compare/v1.85.2...v1.85.3) (2025-11-22)
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import classes from './MarkdownComment.module.scss';
|
||||
import clsx from 'clsx';
|
||||
import {
|
||||
InfoDrawer,
|
||||
@@ -49,7 +48,11 @@ export const MarkdownComment = ({ text, time, characterEveId, id }: MarkdownComm
|
||||
<>
|
||||
<InfoDrawer
|
||||
labelClassName="mb-[3px]"
|
||||
className={clsx(classes.MarkdownCommentRoot, 'p-1 bg-stone-700/20 ')}
|
||||
className={clsx(
|
||||
'p-1 bg-stone-700/20',
|
||||
'text-[12px] leading-[1.2] text-stone-300 break-words',
|
||||
'bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0',
|
||||
)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
title={
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
.CERoot {
|
||||
@apply border border-stone-400/30 rounded-[2px];
|
||||
|
||||
:global {
|
||||
.cm-content {
|
||||
@apply bg-stone-600/40;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { useHotkey } from '@/hooks/Mapper/hooks';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { OutCommand } from '@/hooks/Mapper/types';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import classes from './CommentsEditor.module.scss';
|
||||
|
||||
export interface CommentsEditorProps {}
|
||||
|
||||
@@ -48,6 +49,7 @@ export const CommentsEditor = ({}: CommentsEditorProps) => {
|
||||
|
||||
return (
|
||||
<MarkdownEditor
|
||||
className={classes.CERoot}
|
||||
value={textVal}
|
||||
onChange={setTextVal}
|
||||
overlayContent={
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
.CERoot {
|
||||
@apply border border-stone-400/30 rounded-[2px];
|
||||
@apply border border-stone-500/30 rounded-[2px];
|
||||
|
||||
:global {
|
||||
.cm-content {
|
||||
@apply bg-stone-600/40;
|
||||
@apply bg-stone-950/70;
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
|
||||
@@ -44,9 +44,17 @@ export interface MarkdownEditorProps {
|
||||
overlayContent?: ReactNode;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
height?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MarkdownEditor = ({ value, onChange, overlayContent }: MarkdownEditorProps) => {
|
||||
export const MarkdownEditor = ({
|
||||
value,
|
||||
onChange,
|
||||
overlayContent,
|
||||
height = '70px',
|
||||
className,
|
||||
}: MarkdownEditorProps) => {
|
||||
const [hasShift, setHasShift] = useState(false);
|
||||
|
||||
const refData = useRef({ onChange });
|
||||
@@ -66,9 +74,9 @@ export const MarkdownEditor = ({ value, onChange, overlayContent }: MarkdownEdit
|
||||
<div className={clsx(classes.MarkdownEditor, 'relative')}>
|
||||
<CodeMirror
|
||||
value={value}
|
||||
height="70px"
|
||||
height={height}
|
||||
extensions={CODE_MIRROR_EXTENSIONS}
|
||||
className={classes.CERoot}
|
||||
className={clsx(classes.CERoot, className)}
|
||||
theme={oneDark}
|
||||
onChange={handleOnChange}
|
||||
placeholder="Start typing..."
|
||||
|
||||
@@ -8,8 +8,8 @@ import { LabelsManager } from '@/hooks/Mapper/utils/labelsManager.ts';
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { IconField } from 'primereact/iconfield';
|
||||
import { InputText } from 'primereact/inputtext';
|
||||
import { InputTextarea } from 'primereact/inputtextarea';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { MarkdownEditor } from '@/hooks/Mapper/components/mapInterface/components/MarkdownEditor';
|
||||
|
||||
interface SystemSettingsDialog {
|
||||
systemId: string;
|
||||
@@ -214,13 +214,9 @@ export const SystemSettingsDialog = ({ systemId, visible, setVisible }: SystemSe
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<label htmlFor="username">Description</label>
|
||||
<InputTextarea
|
||||
autoResize
|
||||
rows={5}
|
||||
cols={30}
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
/>
|
||||
<div className="h-[200px]">
|
||||
<MarkdownEditor value={description} onChange={e => setDescription(e)} height="180px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { isWormholeSpace } from '@/hooks/Mapper/components/map/helpers/isWormholeSpace.ts';
|
||||
import { useMemo } from 'react';
|
||||
import { getSystemById, sortWHClasses } from '@/hooks/Mapper/helpers';
|
||||
import { InfoDrawer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { InfoDrawer, MarkdownTextViewer, WHClassView, WHEffectView } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { getSystemStaticInfo } from '@/hooks/Mapper/mapRootProvider/hooks/useLoadSystemStatic';
|
||||
|
||||
interface SystemInfoContentProps {
|
||||
@@ -51,7 +51,7 @@ export const SystemInfoContent = ({ systemId }: SystemInfoContentProps) => {
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="break-words">{description}</div>
|
||||
<MarkdownTextViewer>{description}</MarkdownTextViewer>
|
||||
</InfoDrawer>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { Dialog } from 'primereact/dialog';
|
||||
import { Menu } from 'primereact/menu';
|
||||
import { MenuItem } from 'primereact/menuitem';
|
||||
import { useState, useCallback, useRef, useMemo } from 'react';
|
||||
import { CharacterActivityContent } from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/CharacterActivityContent.tsx';
|
||||
|
||||
interface CharacterActivityProps {
|
||||
@@ -6,17 +9,69 @@ interface CharacterActivityProps {
|
||||
onHide: () => void;
|
||||
}
|
||||
|
||||
const periodOptions = [
|
||||
{ value: 30, label: '30 Days' },
|
||||
{ value: 365, label: '1 Year' },
|
||||
{ value: null, label: 'All Time' },
|
||||
];
|
||||
|
||||
export const CharacterActivity = ({ visible, onHide }: CharacterActivityProps) => {
|
||||
const [selectedPeriod, setSelectedPeriod] = useState<number | null>(30);
|
||||
const menuRef = useRef<Menu>(null);
|
||||
|
||||
const handlePeriodChange = useCallback((days: number | null) => {
|
||||
setSelectedPeriod(days);
|
||||
}, []);
|
||||
|
||||
const menuItems: MenuItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
label: 'Period',
|
||||
items: periodOptions.map(option => ({
|
||||
label: option.label,
|
||||
icon: selectedPeriod === option.value ? 'pi pi-check' : undefined,
|
||||
command: () => handlePeriodChange(option.value),
|
||||
})),
|
||||
},
|
||||
],
|
||||
[selectedPeriod, handlePeriodChange],
|
||||
);
|
||||
|
||||
const selectedPeriodLabel = useMemo(
|
||||
() => periodOptions.find(opt => opt.value === selectedPeriod)?.label || 'All Time',
|
||||
[selectedPeriod],
|
||||
);
|
||||
|
||||
const headerIcons = (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="p-dialog-header-icon p-link"
|
||||
onClick={e => menuRef.current?.toggle(e)}
|
||||
aria-label="Filter options"
|
||||
>
|
||||
<span className="pi pi-bars" />
|
||||
</button>
|
||||
<Menu model={menuItems} popup ref={menuRef} />
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
header="Character Activity"
|
||||
header={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Character Activity</span>
|
||||
<span className="text-xs text-stone-400">({selectedPeriodLabel})</span>
|
||||
</div>
|
||||
}
|
||||
visible={visible}
|
||||
className="w-[550px] max-h-[90vh]"
|
||||
onHide={onHide}
|
||||
dismissableMask
|
||||
contentClassName="p-0 h-full flex flex-col"
|
||||
icons={headerIcons}
|
||||
>
|
||||
<CharacterActivityContent />
|
||||
<CharacterActivityContent selectedPeriod={selectedPeriod} />
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,16 +7,28 @@ import {
|
||||
} from '@/hooks/Mapper/components/mapRootContent/components/CharacterActivity/helpers.tsx';
|
||||
import { Column } from 'primereact/column';
|
||||
import { useMapRootState } from '@/hooks/Mapper/mapRootProvider';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useEffect } from 'react';
|
||||
import { useCharacterActivityHandlers } from '@/hooks/Mapper/components/mapRootContent/hooks/useCharacterActivityHandlers';
|
||||
|
||||
export const CharacterActivityContent = () => {
|
||||
interface CharacterActivityContentProps {
|
||||
selectedPeriod: number | null;
|
||||
}
|
||||
|
||||
export const CharacterActivityContent = ({ selectedPeriod }: CharacterActivityContentProps) => {
|
||||
const {
|
||||
data: { characterActivityData },
|
||||
} = useMapRootState();
|
||||
|
||||
const { handleShowActivity } = useCharacterActivityHandlers();
|
||||
|
||||
const activity = useMemo(() => characterActivityData?.activity || [], [characterActivityData]);
|
||||
const loading = useMemo(() => characterActivityData?.loading !== false, [characterActivityData]);
|
||||
|
||||
// Reload activity data when period changes
|
||||
useEffect(() => {
|
||||
handleShowActivity(selectedPeriod);
|
||||
}, [selectedPeriod, handleShowActivity]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full w-full">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
}
|
||||
|
||||
.SidebarOnTheMap {
|
||||
width: 400px;
|
||||
width: 460px;
|
||||
padding: 0 !important;
|
||||
|
||||
:global {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ConnectionType,
|
||||
OutCommand,
|
||||
Passage,
|
||||
PassageWithSourceTarget,
|
||||
SolarSystemConnection,
|
||||
} from '@/hooks/Mapper/types';
|
||||
import clsx from 'clsx';
|
||||
@@ -19,7 +20,7 @@ import { PassageCard } from './PassageCard';
|
||||
|
||||
const sortByDate = (a: string, b: string) => new Date(a).getTime() - new Date(b).getTime();
|
||||
|
||||
const itemTemplate = (item: Passage, options: VirtualScrollerTemplateOptions) => {
|
||||
const itemTemplate = (item: PassageWithSourceTarget, options: VirtualScrollerTemplateOptions) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.CharacterRow, 'w-full box-border', {
|
||||
@@ -35,7 +36,7 @@ const itemTemplate = (item: Passage, options: VirtualScrollerTemplateOptions) =>
|
||||
};
|
||||
|
||||
export interface ConnectionPassagesContentProps {
|
||||
passages: Passage[];
|
||||
passages: PassageWithSourceTarget[];
|
||||
}
|
||||
|
||||
export const ConnectionPassages = ({ passages = [] }: ConnectionPassagesContentProps) => {
|
||||
@@ -113,6 +114,20 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
[outCommand],
|
||||
);
|
||||
|
||||
const preparedPassages = useMemo(() => {
|
||||
if (!cnInfo) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return passages
|
||||
.sort((a, b) => sortByDate(b.inserted_at, a.inserted_at))
|
||||
.map<PassageWithSourceTarget>(x => ({
|
||||
...x,
|
||||
source: x.from ? cnInfo.target : cnInfo.source,
|
||||
target: x.from ? cnInfo.source : cnInfo.target,
|
||||
}));
|
||||
}, [cnInfo, passages]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedConnection) {
|
||||
return;
|
||||
@@ -145,12 +160,14 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
<InfoDrawer title="Connection" rightSide>
|
||||
<div className="flex justify-end gap-2 items-center">
|
||||
<SystemView
|
||||
showCustomName
|
||||
systemId={cnInfo.source}
|
||||
className={clsx(classes.InfoTextSize, 'select-none text-center')}
|
||||
hideRegion
|
||||
/>
|
||||
<span className="pi pi-angle-double-right text-stone-500 text-[15px]"></span>
|
||||
<SystemView
|
||||
showCustomName
|
||||
systemId={cnInfo.target}
|
||||
className={clsx(classes.InfoTextSize, 'select-none text-center')}
|
||||
hideRegion
|
||||
@@ -184,7 +201,7 @@ export const Connections = ({ selectedConnection, onHide }: OnTheMapProps) => {
|
||||
{/* separator */}
|
||||
<div className="w-full h-px bg-neutral-800 px-0.5"></div>
|
||||
|
||||
<ConnectionPassages passages={passages} />
|
||||
<ConnectionPassages passages={preparedPassages} />
|
||||
</div>
|
||||
</Sidebar>
|
||||
);
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
&.ThreeColumns {
|
||||
grid-template-columns: auto 1fr auto;
|
||||
}
|
||||
|
||||
&.FourColumns {
|
||||
grid-template-columns: auto auto 1fr auto;
|
||||
}
|
||||
}
|
||||
|
||||
.CardBorderLeftIsOwn {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import clsx from 'clsx';
|
||||
import classes from './PassageCard.module.scss';
|
||||
import { Passage } from '@/hooks/Mapper/types';
|
||||
import { TimeAgo } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { PassageWithSourceTarget } from '@/hooks/Mapper/types';
|
||||
import { SystemView, TimeAgo, TooltipPosition } from '@/hooks/Mapper/components/ui-kit';
|
||||
import { WdTooltipWrapper } from '@/hooks/Mapper/components/ui-kit/WdTooltipWrapper';
|
||||
import { kgToTons } from '@/hooks/Mapper/utils/kgToTons.ts';
|
||||
import { useMemo } from 'react';
|
||||
@@ -11,7 +11,7 @@ type PassageCardType = {
|
||||
showShipName?: boolean;
|
||||
// showSystem?: boolean;
|
||||
// useSystemsCache?: boolean;
|
||||
} & Passage;
|
||||
} & PassageWithSourceTarget;
|
||||
|
||||
const SHIP_NAME_RX = /u'|'/g;
|
||||
export const getShipName = (name: string) => {
|
||||
@@ -25,7 +25,7 @@ export const getShipName = (name: string) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardType) => {
|
||||
export const PassageCard = ({ inserted_at, character: char, ship, source, target, from }: PassageCardType) => {
|
||||
const isOwn = false;
|
||||
|
||||
const insertedAt = useMemo(() => {
|
||||
@@ -37,7 +37,39 @@ export const PassageCard = ({ inserted_at, character: char, ship }: PassageCardT
|
||||
<div className={clsx(classes.CharacterCard, 'w-full text-xs', 'flex flex-col box-border')}>
|
||||
<div className="flex flex-col justify-between px-2 py-1 gap-1">
|
||||
{/*here icon and other*/}
|
||||
<div className={clsx(classes.CharRow, classes.ThreeColumns)}>
|
||||
<div className={clsx(classes.CharRow, classes.FourColumns)}>
|
||||
<WdTooltipWrapper
|
||||
position={TooltipPosition.top}
|
||||
content={
|
||||
<div className="flex justify-between gap-2 items-center">
|
||||
<SystemView
|
||||
showCustomName
|
||||
systemId={source}
|
||||
className="select-none text-center !text-[12px]"
|
||||
hideRegion
|
||||
/>
|
||||
<span className="pi pi-angle-double-right text-stone-500 text-[15px]"></span>
|
||||
<SystemView
|
||||
showCustomName
|
||||
systemId={target}
|
||||
className="select-none text-center !text-[12px]"
|
||||
hideRegion
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'transition-all transform ease-in duration-200',
|
||||
'pi text-stone-500 text-[15px] w-[35px] h-[33px] !flex items-center justify-center border rounded-[6px]',
|
||||
{
|
||||
['pi-angle-double-right !text-orange-400 border-orange-400 hover:bg-orange-400/30']: from,
|
||||
['pi-angle-double-left !text-stone-500/70 border-stone-500/70 hover:bg-stone-500/30']: !from,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</WdTooltipWrapper>
|
||||
|
||||
{/*portrait*/}
|
||||
<span
|
||||
className={clsx(classes.EveIcon, classes.CharIcon, 'wd-bg-default')}
|
||||
|
||||
@@ -23,17 +23,17 @@ export const useCharacterActivityHandlers = () => {
|
||||
/**
|
||||
* Handle showing the character activity dialog
|
||||
*/
|
||||
const handleShowActivity = useCallback(() => {
|
||||
const handleShowActivity = useCallback((days?: number | null) => {
|
||||
// Update local state to show the dialog
|
||||
update(state => ({
|
||||
...state,
|
||||
showCharacterActivity: true,
|
||||
}));
|
||||
|
||||
// Send the command to the server
|
||||
// Send the command to the server with optional days parameter
|
||||
outCommand({
|
||||
type: OutCommand.showActivity,
|
||||
data: {},
|
||||
data: days !== undefined ? { days } : {},
|
||||
});
|
||||
}, [outCommand, update]);
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
.MarkdownCommentRoot {
|
||||
border-left-width: 3px;
|
||||
|
||||
.MarkdownTextViewer {
|
||||
@apply text-[12px] leading-[1.2] text-stone-300 break-words;
|
||||
@apply bg-gradient-to-r from-stone-600/40 via-stone-600/10 to-stone-600/0;
|
||||
|
||||
.h1 {
|
||||
@apply text-[12px] font-normal m-0 p-0 border-none break-words whitespace-normal;
|
||||
@@ -56,6 +53,10 @@
|
||||
@apply font-bold text-green-400 break-words whitespace-normal;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
i, em {
|
||||
@apply italic text-pink-400 break-words whitespace-normal;
|
||||
}
|
||||
@@ -2,10 +2,16 @@ import Markdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkBreaks from 'remark-breaks';
|
||||
|
||||
import classes from './MarkdownTextViewer.module.scss';
|
||||
|
||||
const REMARK_PLUGINS = [remarkGfm, remarkBreaks];
|
||||
|
||||
type MarkdownTextViewerProps = { children: string };
|
||||
|
||||
export const MarkdownTextViewer = ({ children }: MarkdownTextViewerProps) => {
|
||||
return <Markdown remarkPlugins={REMARK_PLUGINS}>{children}</Markdown>;
|
||||
return (
|
||||
<div className={classes.MarkdownTextViewer}>
|
||||
<Markdown remarkPlugins={REMARK_PLUGINS}>{children}</Markdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -68,4 +68,5 @@ export interface ActivitySummary {
|
||||
passages: number;
|
||||
connections: number;
|
||||
signatures: number;
|
||||
timestamp?: string;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,17 @@ export type PassageLimitedCharacterType = Pick<
|
||||
>;
|
||||
|
||||
export type Passage = {
|
||||
from: boolean;
|
||||
inserted_at: string; // Date
|
||||
ship: ShipTypeRaw;
|
||||
character: PassageLimitedCharacterType;
|
||||
};
|
||||
|
||||
export type PassageWithSourceTarget = {
|
||||
source: string;
|
||||
target: string;
|
||||
} & Passage;
|
||||
|
||||
export type ConnectionInfoOutput = {
|
||||
marl_eol_time: string;
|
||||
};
|
||||
|
||||
@@ -43,13 +43,14 @@ defmodule WandererApp.Character.Activity do
|
||||
## Parameters
|
||||
- `map_id`: ID of the map
|
||||
- `current_user`: Current user struct (used only to get user settings)
|
||||
- `days`: Optional number of days to filter activity (nil for all time)
|
||||
|
||||
## Returns
|
||||
- List of processed activity data
|
||||
"""
|
||||
def process_character_activity(map_id, current_user) do
|
||||
def process_character_activity(map_id, current_user, days \\ nil) do
|
||||
with {:ok, map_user_settings} <- get_map_user_settings(map_id, current_user.id),
|
||||
{:ok, raw_activity} <- WandererApp.Map.get_character_activity(map_id),
|
||||
{:ok, raw_activity} <- WandererApp.Map.get_character_activity(map_id, days),
|
||||
{:ok, user_characters} <-
|
||||
WandererApp.Api.Character.active_by_user(%{user_id: current_user.id}) do
|
||||
process_activity_data(raw_activity, map_user_settings, user_characters)
|
||||
|
||||
@@ -30,6 +30,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
defp location_concurrency do
|
||||
Application.get_env(:wanderer_app, :location_concurrency, System.schedulers_online() * 12)
|
||||
end
|
||||
|
||||
# Other operations can use lower concurrency
|
||||
@standard_concurrency System.schedulers_online() * 2
|
||||
|
||||
@@ -297,7 +298,7 @@ defmodule WandererApp.Character.TrackerPool do
|
||||
)
|
||||
|
||||
# Warn if location updates are falling behind (taking > 800ms for 100 chars)
|
||||
if duration > 800 do
|
||||
if duration > 2000 do
|
||||
Logger.warning(
|
||||
"[Tracker Pool] Location updates falling behind: #{duration}ms for #{length(characters)} chars (pool: #{state.uuid})"
|
||||
)
|
||||
|
||||
@@ -463,7 +463,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
{:error, reason} ->
|
||||
# Check if this is a Finch pool error
|
||||
if is_exception(reason) and Exception.message(reason) =~ "unable to provide a connection" do
|
||||
if is_exception(reason) and
|
||||
Exception.message(reason) =~ "unable to provide a connection" do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :finch, :pool_exhausted],
|
||||
%{count: 1},
|
||||
@@ -677,7 +678,8 @@ defmodule WandererApp.Esi.ApiClient do
|
||||
|
||||
{:error, reason} ->
|
||||
# Check if this is a Finch pool error
|
||||
if is_exception(reason) and Exception.message(reason) =~ "unable to provide a connection" do
|
||||
if is_exception(reason) and
|
||||
Exception.message(reason) =~ "unable to provide a connection" do
|
||||
:telemetry.execute(
|
||||
[:wanderer_app, :finch, :pool_exhausted],
|
||||
%{count: 1},
|
||||
|
||||
@@ -240,8 +240,10 @@ defmodule WandererApp.Map.Routes do
|
||||
{:ok, result}
|
||||
|
||||
{:error, _error} ->
|
||||
error_file_path = save_error_params(origin, hubs, params)
|
||||
|
||||
@logger.error(
|
||||
"Error getting custom routes for #{inspect(origin)}: #{inspect(params)}"
|
||||
"Error getting custom routes for #{inspect(origin)}: #{inspect(params)}. Params saved to: #{error_file_path}"
|
||||
)
|
||||
|
||||
WandererApp.Esi.get_routes_eve(hubs, origin, params, opts)
|
||||
@@ -249,6 +251,35 @@ defmodule WandererApp.Map.Routes do
|
||||
end
|
||||
end
|
||||
|
||||
defp save_error_params(origin, hubs, params) do
|
||||
timestamp = DateTime.utc_now() |> DateTime.to_unix(:millisecond)
|
||||
filename = "#{timestamp}_route_error_params.json"
|
||||
filepath = Path.join([System.tmp_dir!(), filename])
|
||||
|
||||
error_data = %{
|
||||
timestamp: DateTime.utc_now() |> DateTime.to_iso8601(),
|
||||
origin: origin,
|
||||
hubs: hubs,
|
||||
params: params
|
||||
}
|
||||
|
||||
case Jason.encode(error_data, pretty: true) do
|
||||
{:ok, json_string} ->
|
||||
File.write!(filepath, json_string)
|
||||
filepath
|
||||
|
||||
{:error, _reason} ->
|
||||
# Fallback: save as Elixir term if JSON encoding fails
|
||||
filepath_term = Path.join([System.tmp_dir!(), "#{timestamp}_route_error_params.term"])
|
||||
File.write!(filepath_term, inspect(error_data, pretty: true))
|
||||
filepath_term
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
@logger.error("Failed to save error params: #{inspect(e)}")
|
||||
"error_saving_params"
|
||||
end
|
||||
|
||||
defp remove_intersection(pairs_arr) do
|
||||
tuples = pairs_arr |> Enum.map(fn x -> {x.first, x.second} end)
|
||||
|
||||
|
||||
@@ -34,28 +34,14 @@ defmodule WandererApp.Map.Server.CharactersImpl do
|
||||
track_characters(map_id, rest)
|
||||
end
|
||||
|
||||
def update_tracked_characters(map_id) do
|
||||
def invalidate_characters(map_id) do
|
||||
Task.start_link(fn ->
|
||||
{:ok, all_map_tracked_character_ids} =
|
||||
character_ids =
|
||||
map_id
|
||||
|> WandererApp.MapCharacterSettingsRepo.get_tracked_by_map_all()
|
||||
|> case do
|
||||
{:ok, settings} -> {:ok, settings |> Enum.map(&Map.get(&1, :character_id))}
|
||||
_ -> {:ok, []}
|
||||
end
|
||||
|> WandererApp.Map.get_map!()
|
||||
|> Map.get(:characters, [])
|
||||
|
||||
{:ok, actual_map_tracked_characters} =
|
||||
WandererApp.Cache.lookup("maps:#{map_id}:tracked_characters", [])
|
||||
|
||||
characters_to_remove = actual_map_tracked_characters -- all_map_tracked_character_ids
|
||||
|
||||
WandererApp.Cache.insert_or_update(
|
||||
"map_#{map_id}:invalidate_character_ids",
|
||||
characters_to_remove,
|
||||
fn ids ->
|
||||
(ids ++ characters_to_remove) |> Enum.uniq()
|
||||
end
|
||||
)
|
||||
WandererApp.Cache.insert("map_#{map_id}:invalidate_character_ids", character_ids)
|
||||
|
||||
:ok
|
||||
end)
|
||||
|
||||
@@ -410,7 +410,7 @@ defmodule WandererApp.Map.Server.ConnectionsImpl do
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_source}
|
||||
),
|
||||
target_system when not is_nil(source_system) <-
|
||||
target_system when not is_nil(target_system) <-
|
||||
WandererApp.Map.find_system_by_location(
|
||||
map_id,
|
||||
%{solar_system_id: solar_system_target}
|
||||
|
||||
@@ -29,7 +29,7 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
|
||||
@update_presence_timeout :timer.seconds(5)
|
||||
@update_characters_timeout :timer.seconds(1)
|
||||
@update_tracked_characters_timeout :timer.minutes(1)
|
||||
@invalidate_characters_timeout :timer.hours(1)
|
||||
|
||||
def new(), do: __struct__()
|
||||
def new(args), do: __struct__(args)
|
||||
@@ -149,8 +149,8 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
|
||||
Process.send_after(
|
||||
self(),
|
||||
{:update_tracked_characters, map_id},
|
||||
@update_tracked_characters_timeout
|
||||
{:invalidate_characters, map_id},
|
||||
@invalidate_characters_timeout
|
||||
)
|
||||
|
||||
Process.send_after(self(), {:update_presence, map_id}, @update_presence_timeout)
|
||||
@@ -302,14 +302,14 @@ defmodule WandererApp.Map.Server.Impl do
|
||||
CharactersImpl.update_characters(map_id)
|
||||
end
|
||||
|
||||
def handle_event({:update_tracked_characters, map_id} = event) do
|
||||
def handle_event({:invalidate_characters, map_id} = event) do
|
||||
Process.send_after(
|
||||
self(),
|
||||
event,
|
||||
@update_tracked_characters_timeout
|
||||
@invalidate_characters_timeout
|
||||
)
|
||||
|
||||
CharactersImpl.update_tracked_characters(map_id)
|
||||
CharactersImpl.invalidate_characters(map_id)
|
||||
end
|
||||
|
||||
def handle_event({:update_presence, map_id} = event) do
|
||||
|
||||
@@ -72,39 +72,53 @@ defmodule WandererApp.Map.Server.PingsImpl do
|
||||
type: type
|
||||
} = _ping_info
|
||||
) do
|
||||
with {:ok, character} <- WandererApp.Character.get_character(character_id),
|
||||
{:ok,
|
||||
%{system: %{id: system_id, name: system_name, solar_system_id: solar_system_id}} = ping} <-
|
||||
WandererApp.MapPingsRepo.get_by_id(ping_id),
|
||||
:ok <- WandererApp.MapPingsRepo.destroy(ping) do
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: solar_system_id,
|
||||
type: type
|
||||
})
|
||||
case WandererApp.MapPingsRepo.get_by_id(ping_id) do
|
||||
{:ok,
|
||||
%{system: %{id: system_id, name: system_name, solar_system_id: solar_system_id}} = ping} ->
|
||||
with {:ok, character} <- WandererApp.Character.get_character(character_id),
|
||||
:ok <- WandererApp.MapPingsRepo.destroy(ping) do
|
||||
Impl.broadcast!(map_id, :ping_cancelled, %{
|
||||
id: ping_id,
|
||||
solar_system_id: solar_system_id,
|
||||
type: type
|
||||
})
|
||||
|
||||
# Broadcast rally point removal events to external clients (webhooks/SSE)
|
||||
if type == 1 do
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_removed, %{
|
||||
id: ping_id,
|
||||
solar_system_id: solar_system_id,
|
||||
system_id: system_id,
|
||||
character_id: character_id,
|
||||
character_name: character.name,
|
||||
character_eve_id: character.eve_id,
|
||||
system_name: system_name
|
||||
})
|
||||
end
|
||||
# Broadcast rally point removal events to external clients (webhooks/SSE)
|
||||
if type == 1 do
|
||||
WandererApp.ExternalEvents.broadcast(map_id, :rally_point_removed, %{
|
||||
id: ping_id,
|
||||
solar_system_id: solar_system_id,
|
||||
system_id: system_id,
|
||||
character_id: character_id,
|
||||
character_name: character.name,
|
||||
character_eve_id: character.eve_id,
|
||||
system_name: system_name
|
||||
})
|
||||
end
|
||||
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_rally_cancelled, %{
|
||||
character_id: character_id,
|
||||
user_id: user_id,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
else
|
||||
error ->
|
||||
Logger.error("Failed to destroy ping: #{inspect(error, pretty: true)}")
|
||||
end
|
||||
|
||||
{:error, %Ash.Error.Query.NotFound{}} ->
|
||||
# Ping already deleted (possibly by cascade deletion from map/system/character removal,
|
||||
# auto-expiry, or concurrent cancellation). This is not an error - the desired state
|
||||
# (ping is gone) is already achieved. Just broadcast the cancellation event.
|
||||
Logger.debug(
|
||||
"Ping #{ping_id} not found during cancellation - already deleted, skipping broadcast"
|
||||
)
|
||||
|
||||
:ok
|
||||
|
||||
WandererApp.User.ActivityTracker.track_map_event(:map_rally_cancelled, %{
|
||||
character_id: character_id,
|
||||
user_id: user_id,
|
||||
map_id: map_id,
|
||||
solar_system_id: solar_system_id
|
||||
})
|
||||
else
|
||||
error ->
|
||||
Logger.error("Failed to cancel_ping: #{inspect(error, pretty: true)}")
|
||||
Logger.error("Failed to fetch ping for cancellation: #{inspect(error, pretty: true)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,14 +30,17 @@ defmodule WandererAppWeb.MapActivityEventHandler do
|
||||
|
||||
def handle_ui_event(
|
||||
"show_activity",
|
||||
_,
|
||||
params,
|
||||
%{assigns: %{map_id: map_id, current_user: current_user}} = socket
|
||||
) do
|
||||
Task.async(fn ->
|
||||
try do
|
||||
# Extract days parameter (nil if not provided)
|
||||
days = Map.get(params, "days")
|
||||
|
||||
# Get raw activity data from the domain logic
|
||||
result =
|
||||
WandererApp.Character.Activity.process_character_activity(map_id, current_user)
|
||||
WandererApp.Character.Activity.process_character_activity(map_id, current_user, days)
|
||||
|
||||
# Group activities by user_id and summarize
|
||||
summarized_result =
|
||||
|
||||
@@ -98,7 +98,7 @@ defmodule WandererAppWeb.Telemetry do
|
||||
),
|
||||
counter("wanderer_app.tracker_pool.location_lag.count",
|
||||
tags: [:pool_uuid],
|
||||
description: "Count of location updates falling behind (>800ms)"
|
||||
description: "Count of location updates falling behind (>2s)"
|
||||
),
|
||||
counter("wanderer_app.tracker_pool.ship_skipped.count",
|
||||
tags: [:pool_uuid, :reason],
|
||||
|
||||
Reference in New Issue
Block a user