9 Commits

Author SHA1 Message Date
calli
67acea9be4 increase automatic refresh to 5 minutes 2025-04-23 18:59:34 +03:00
calli
98b450fcc7 rearrange character layout and align tables to character image 2025-04-23 18:52:05 +03:00
calli
eb15696241 make account card clickable for easier collapse action 2025-04-23 17:59:50 +03:00
calli
741b2480b9 add account level statistics 2025-04-23 17:41:09 +03:00
calli
b185e5d044 hide extraction simulation for planets without extractors 2025-04-23 15:23:57 +03:00
calli
f0d4708b43 show extraction tooltip only if we have extractors 2025-04-23 15:23:32 +03:00
calli
0f33a7ff0c Move activity level local storage read to useState 2025-04-23 15:18:32 +03:00
calli
bbdcece163 Show imports per planet 2025-04-23 15:15:26 +03:00
calli
2a1e74ca79 Remove sentry tunnel routing 2025-04-23 15:14:59 +03:00
10 changed files with 169 additions and 74 deletions

View File

@@ -43,11 +43,6 @@ module.exports = withSentryConfig(
// Transpiles SDK to be compatible with IE11 (increases bundle size) // Transpiles SDK to be compatible with IE11 (increases bundle size)
transpileClientSDK: true, transpileClientSDK: true,
// Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
// This can increase your server load as well as your hosting bill.
// Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
// side errors will fail.
tunnelRoute: "/monitoring",
// Hides source maps from generated client bundles // Hides source maps from generated client bundles
hideSourceMaps: true, hideSourceMaps: true,

View File

@@ -1,5 +1,5 @@
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { Box, Stack, Typography, useTheme, Paper, IconButton } from "@mui/material"; import { Box, Stack, Typography, useTheme, Paper, IconButton, Divider } from "@mui/material";
import { CharacterRow } from "../Characters/CharacterRow"; import { CharacterRow } from "../Characters/CharacterRow";
import { PlanetaryInteractionRow } from "../PlanetaryInteraction/PlanetaryInteractionRow"; import { PlanetaryInteractionRow } from "../PlanetaryInteraction/PlanetaryInteractionRow";
import { SessionContext } from "@/app/context/Context"; import { SessionContext } from "@/app/context/Context";
@@ -14,17 +14,36 @@ import { STORAGE_IDS } from "@/const";
interface AccountTotals { interface AccountTotals {
monthlyEstimate: number; monthlyEstimate: number;
storageValue: number; storageValue: number;
planetCount: number;
characterCount: number;
runningExtractors: number;
totalExtractors: number;
} }
const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalResult | undefined): AccountTotals => { const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalResult | undefined): AccountTotals => {
let totalMonthlyEstimate = 0; let totalMonthlyEstimate = 0;
let totalStorageValue = 0; let totalStorageValue = 0;
let totalPlanetCount = 0;
let totalCharacterCount = characters.length;
let runningExtractors = 0;
let totalExtractors = 0;
characters.forEach((character) => { characters.forEach((character) => {
totalPlanetCount += character.planets.length;
character.planets.forEach((planet) => { character.planets.forEach((planet) => {
const { localExports } = planetCalculations(planet); const { localExports, extractors } = planetCalculations(planet);
const planetConfig = character.planetConfig.find(p => p.planetId === planet.planet_id); const planetConfig = character.planetConfig.find(p => p.planetId === planet.planet_id);
// Count running and total extractors
if (!planetConfig?.excludeFromTotals) {
extractors.forEach(extractor => {
totalExtractors++;
if (extractor.expiry_time && new Date(extractor.expiry_time) > new Date()) {
runningExtractors++;
}
});
}
// Calculate monthly estimate // Calculate monthly estimate
if (!planetConfig?.excludeFromTotals) { if (!planetConfig?.excludeFromTotals) {
localExports.forEach((exportItem) => { localExports.forEach((exportItem) => {
@@ -56,7 +75,11 @@ const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalR
return { return {
monthlyEstimate: totalMonthlyEstimate, monthlyEstimate: totalMonthlyEstimate,
storageValue: totalStorageValue storageValue: totalStorageValue,
planetCount: totalPlanetCount,
characterCount: totalCharacterCount,
runningExtractors,
totalExtractors
}; };
}; };
@@ -64,7 +87,7 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
const theme = useTheme(); const theme = useTheme();
const [localIsCollapsed, setLocalIsCollapsed] = useState(false); const [localIsCollapsed, setLocalIsCollapsed] = useState(false);
const { planMode, piPrices } = useContext(SessionContext); const { planMode, piPrices } = useContext(SessionContext);
const { monthlyEstimate, storageValue } = calculateAccountTotals(characters, piPrices); const { monthlyEstimate, storageValue, planetCount, characterCount, runningExtractors, totalExtractors } = calculateAccountTotals(characters, piPrices);
// Update local collapse state when prop changes // Update local collapse state when prop changes
useEffect(() => { useEffect(() => {
@@ -102,7 +125,12 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}} }}
onClick={() => setLocalIsCollapsed(!localIsCollapsed)}
> >
<Box> <Box>
<Typography <Typography
@@ -116,30 +144,65 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
? `Account: ${characters[0].account}` ? `Account: ${characters[0].account}`
: "No account name"} : "No account name"}
</Typography> </Typography>
<Typography <Box sx={{
sx={{ display: 'flex',
fontSize: "0.8rem", gap: 2,
color: theme.palette.text.secondary, flexWrap: 'wrap',
}} mt: 1,
> alignItems: 'center'
Monthly Estimate: {monthlyEstimate >= 1000 }}>
? `${(monthlyEstimate / 1000).toFixed(2)} B` <Typography
: `${monthlyEstimate.toFixed(2)} M`} ISK sx={{
</Typography> fontSize: "0.8rem",
<Typography color: theme.palette.text.secondary,
sx={{ }}
fontSize: "0.8rem", >
color: theme.palette.text.secondary, Monthly: {monthlyEstimate >= 1000
}} ? `${(monthlyEstimate / 1000).toFixed(2)} B`
> : `${monthlyEstimate.toFixed(2)} M`} ISK
Storage Value: {storageValue >= 1000 </Typography>
? `${(storageValue / 1000).toFixed(2)} B` <Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
: `${storageValue.toFixed(2)} M`} ISK <Typography
</Typography> sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Storage: {storageValue >= 1000
? `${(storageValue / 1000).toFixed(2)} B`
: `${storageValue.toFixed(2)} M`} ISK
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Planets: {planetCount}
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Characters: {characterCount}
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: runningExtractors < totalExtractors ? theme.palette.error.main : theme.palette.text.secondary,
}}
>
Extractors: {runningExtractors}/{totalExtractors}
</Typography>
</Box>
</Box> </Box>
<IconButton <IconButton
size="small" size="small"
onClick={() => setLocalIsCollapsed(!localIsCollapsed)}
sx={{ sx={{
transform: localIsCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)', transform: localIsCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease-in-out' transition: 'transform 0.2s ease-in-out'

View File

@@ -7,16 +7,18 @@ import { styled, useTheme } from "@mui/material/styles";
import React from "react"; import React from "react";
import { CharacterDialog } from "./CharacterDialog"; import { CharacterDialog } from "./CharacterDialog";
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { Box, Button, Tooltip } from "@mui/material"; import { Box, Tooltip, IconButton, Typography } from "@mui/material";
import SettingsIcon from "@mui/icons-material/Settings";
import { EVE_IMAGE_URL } from "@/const"; import { EVE_IMAGE_URL } from "@/const";
import { CharacterContext } from "@/app/context/Context"; import { CharacterContext } from "@/app/context/Context";
const StackItem = styled(Stack)(({ theme }) => ({ const StackItem = styled(Stack)(({ theme }) => ({
...theme.typography.body2, ...theme.typography.body2,
padding: theme.custom.compactMode ? theme.spacing(1) : theme.spacing(2), padding: theme.custom.compactMode ? theme.spacing(1) : theme.spacing(2),
display: "flex",
textAlign: "left", textAlign: "left",
justifyContent: "center", justifyContent: "flex-start",
alignItems: "center", alignItems: "flex-start",
})); }));
export const CharacterRow = ({ character }: { character: AccessToken }) => { export const CharacterRow = ({ character }: { character: AccessToken }) => {
@@ -29,8 +31,6 @@ export const CharacterRow = ({ character }: { character: AccessToken }) => {
return ( return (
<StackItem <StackItem
key={character.character.characterId} key={character.character.characterId}
alignItems="flex-start"
justifyContent="flex-start"
> >
<CharacterDialog <CharacterDialog
character={selectedCharacter} character={selectedCharacter}
@@ -38,13 +38,49 @@ export const CharacterRow = ({ character }: { character: AccessToken }) => {
updateCharacter={updateCharacter} updateCharacter={updateCharacter}
closeDialog={() => setSelectedCharacter(undefined)} closeDialog={() => setSelectedCharacter(undefined)}
/> />
<Typography
sx={{
whiteSpace: "nowrap",
fontSize: theme.custom.smallText,
textAlign: "left",
lineHeight: 1.2,
marginBottom: "0.4rem",
marginLeft: "0.2rem",
overflow: "visible",
textOverflow: "clip",
width: "1rem"
}}
>
{character.character.name}
</Typography>
<Tooltip title={character.comment}> <Tooltip title={character.comment}>
<Box <Box
display="flex" display="flex"
flexDirection="column" flexDirection="column"
maxWidth={120} maxWidth={120}
onClick={() => setSelectedCharacter(character)} onClick={() => setSelectedCharacter(character)}
position="relative"
sx={{ cursor: "pointer" }}
> >
<IconButton
size="small"
onClick={(e) => {
e.stopPropagation();
setSelectedCharacter(character);
}}
sx={{
p: 0,
position: "absolute",
top: 4,
left: 4,
backgroundColor: "rgba(0, 0, 0, 0.5)",
"&:hover": {
backgroundColor: "rgba(0, 0, 0, 0.7)",
},
}}
>
<SettingsIcon fontSize="small" sx={{ color: "white" }} />
</IconButton>
<Image <Image
unoptimized unoptimized
src={`${EVE_IMAGE_URL}/characters/${character.character.characterId}/portrait?size=128`} src={`${EVE_IMAGE_URL}/characters/${character.character.characterId}/portrait?size=128`}
@@ -53,16 +89,6 @@ export const CharacterRow = ({ character }: { character: AccessToken }) => {
height={theme.custom.cardImageSize} height={theme.custom.cardImageSize}
style={{ marginBottom: "0.2rem", borderRadius: 8 }} style={{ marginBottom: "0.2rem", borderRadius: 8 }}
/> />
<Button
style={{
padding: 6,
fontSize: theme.custom.smallText,
lineHeight: 1,
}}
variant="outlined"
>
{character.character.name}
</Button>
</Box> </Box>
</Tooltip> </Tooltip>
</StackItem> </StackItem>

View File

@@ -170,7 +170,7 @@ export const ExtractionSimulationDisplay: React.FC<ExtractionSimulationDisplayPr
return ( return (
<Box> <Box>
<Paper sx={{ p: 2 }}> {extractors.length > 0 ? <Paper sx={{ p: 2 }}>
<Typography variant="h6" gutterBottom> <Typography variant="h6" gutterBottom>
Extraction Simulation Extraction Simulation
</Typography> </Typography>
@@ -210,7 +210,7 @@ export const ExtractionSimulationDisplay: React.FC<ExtractionSimulationDisplayPr
<div style={{ height: '300px' }}> <div style={{ height: '300px' }}>
<Line data={chartData} options={chartOptions} /> <Line data={chartData} options={chartOptions} />
</div> </div>
</Paper> </Paper> : null}
<ProductionChainVisualization <ProductionChainVisualization
extractedTypeIds={extractedTypeIds} extractedTypeIds={extractedTypeIds}

View File

@@ -102,9 +102,11 @@ export const PlanetCard = ({
return ( return (
<Tooltip <Tooltip
title={ title={
<ExtractionSimulationTooltip extractors.length > 0 ? (
extractors={extractors} <ExtractionSimulationTooltip
/> extractors={extractors}
/>
) : null
} }
componentsProps={{ componentsProps={{
tooltip: { tooltip: {

View File

@@ -165,11 +165,8 @@ export const PlanetTableRow = ({
} }
}} }}
onClick={(e) => { onClick={(e) => {
// Only trigger if clicking a cell with the clickable-cell class
if (!(e.target as HTMLElement).closest('.clickable-cell')) return; if (!(e.target as HTMLElement).closest('.clickable-cell')) return;
if (extractors.length > 0) { setSimulationOpen(!simulationOpen);
setSimulationOpen(!simulationOpen);
}
}} }}
> >
<TableCell component="th" scope="row" className="clickable-cell"> <TableCell component="th" scope="row" className="clickable-cell">
@@ -190,17 +187,19 @@ export const PlanetTableRow = ({
<Tooltip <Tooltip
placement="right" placement="right"
title={ title={
<ExtractionSimulationTooltip extractors.length > 0 ? (
extractors={extractors <ExtractionSimulationTooltip
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle) extractors={extractors
.map(e => ({ .filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
typeId: e.extractor_details!.product_type_id!, .map(e => ({
baseValue: e.extractor_details!.qty_per_cycle!, typeId: e.extractor_details!.product_type_id!,
cycleTime: e.extractor_details!.cycle_time || 3600, baseValue: e.extractor_details!.qty_per_cycle!,
installTime: e.install_time ?? "", cycleTime: e.extractor_details!.cycle_time || 3600,
expiryTime: e.expiry_time ?? "" installTime: e.install_time ?? "",
}))} expiryTime: e.expiry_time ?? ""
/> }))}
/>
) : null
} }
componentsProps={{ componentsProps={{
tooltip: { tooltip: {
@@ -291,7 +290,7 @@ export const PlanetTableRow = ({
key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`} key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`}
fontSize={theme.custom.smallText} fontSize={theme.custom.smallText}
> >
{PI_TYPES_MAP[i.type_id].name} ({i.quantity}/h) {PI_TYPES_MAP[i.type_id].name} ({i.quantity * i.factoryCount}/h)
</Typography> </Typography>
))} ))}
</div> </div>

View File

@@ -159,8 +159,8 @@ export const PlanetaryInteractionRow = ({
const theme = useTheme(); const theme = useTheme();
return theme.custom.compactMode ? ( return theme.custom.compactMode ? (
<PlanetaryInteractionIconsRow character={character} /> <div style={{ marginTop: "1.2rem" }}><PlanetaryInteractionIconsRow character={character} /></div>
) : ( ) : (
<PlanetaryIteractionTable character={character} /> <div style={{ marginTop: "1.4rem" }}><PlanetaryIteractionTable character={character} /></div>
); );
}; };

View File

@@ -51,18 +51,22 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
const [sortDirection, setSortDirection] = useState<SortDirection>("asc"); const [sortDirection, setSortDirection] = useState<SortDirection>("asc");
const [sortBy, setSortBy] = useState<SortBy>("name"); const [sortBy, setSortBy] = useState<SortBy>("name");
const [startDate, setStartDate] = useState<string>(DateTime.now().startOf('day').toISO()); const [startDate, setStartDate] = useState<string>(DateTime.now().startOf('day').toISO());
const [activityPercentage, setActivityPercentage] = useState<number>(() => { const [activityPercentage, setActivityPercentage] = useState<number>(100);
const saved = localStorage.getItem('activityPercentage');
return saved ? parseFloat(saved) : 100;
});
// Load saved values from localStorage on mount
useEffect(() => { useEffect(() => {
const savedDate = localStorage.getItem('productionStartDate'); const savedDate = localStorage.getItem('productionStartDate');
if (savedDate) { if (savedDate) {
setStartDate(savedDate); setStartDate(savedDate);
} }
const savedActivity = localStorage.getItem('activityPercentage');
if (savedActivity) {
setActivityPercentage(parseFloat(savedActivity));
}
}, []); }, []);
// Save values to localStorage when they change
useEffect(() => { useEffect(() => {
localStorage.setItem('productionStartDate', startDate); localStorage.setItem('productionStartDate', startDate);
}, [startDate]); }, [startDate]);

View File

@@ -248,7 +248,7 @@ const Home = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
const ESI_CACHE_TIME_MS = 600000; const ESI_CACHE_TIME_MS = 3000000;
const interval = setInterval(() => { const interval = setInterval(() => {
const characters = initializeCharacters(); const characters = initializeCharacters();
refreshSession(characters) refreshSession(characters)

View File

@@ -94,7 +94,13 @@ export const planetCalculations = (planet: PlanetWithInfo) => {
![...locallyProduced, ...locallyExcavated].some( ![...locallyProduced, ...locallyExcavated].some(
(lp) => lp === p.type_id, (lp) => lp === p.type_id,
), ),
); ).map((p) => ({
...p,
factoryCount: planetInfo.pins
.filter((f) => f.schematic_id === p.schematic_id)
.length,
}));
const localExports = locallyProduced const localExports = locallyProduced
.filter((p) => !locallyConsumed.some((lp) => lp === p)) .filter((p) => !locallyConsumed.some((lp) => lp === p))