8 Commits

Author SHA1 Message Date
calli
5c6f912721 cache planets for one minute so we dont spam ESI 2025-11-09 23:07:23 +02:00
calli
470935fe9d update official instance url 2025-08-01 09:45:25 +03:00
calli
7ce238c9c7 increase batch from 5 to 50 2025-05-17 20:28:01 +03:00
calli
6523000e69 lets batch the requests for users with gazillion characters 2025-05-17 19:51:21 +03:00
calli
b993b28840 Add balance and storage alert counts to account card 2025-05-17 17:21:49 +03:00
calli
c036bc10e1 Sort storages 2025-05-17 17:21:37 +03:00
calli
b743193f46 update discord link 2025-05-17 17:10:58 +03:00
calli
02ebaf6e35 remove unused imports 2025-05-02 23:00:56 +03:00
7 changed files with 183 additions and 94 deletions

View File

@@ -2,9 +2,9 @@
Simple tool to track your PI planet extractors. Login with your characters and enjoy the PI! Simple tool to track your PI planet extractors. Login with your characters and enjoy the PI!
Any questions, feedback or suggestions are welcome at [EVE PI Discord](https://discord.gg/GPtw5kfuJu) Any questions, feedback or suggestions are welcome at [EVE PI Discord](https://discord.gg/bCdXzU8PHK)
## [Avanto hosted PI tool](https://pi.avanto.tk) ## [Hosted PI tool](https://pi.calli.fi)
![Screenshot of PI tool](https://github.com/calli-eve/eve-pi/blob/main/images/eve-pi.png) ![Screenshot of PI tool](https://github.com/calli-eve/eve-pi/blob/main/images/eve-pi.png)
![3D render of a planet](https://github.com/calli-eve/eve-pi/blob/main/images/3dplanet.png) ![3D render of a planet](https://github.com/calli-eve/eve-pi/blob/main/images/3dplanet.png)

View File

@@ -376,6 +376,24 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
> >
Extractors: {runningExtractors}/{totalExtractors} Extractors: {runningExtractors}/{totalExtractors}
</Typography> </Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: Object.values(planetDetails).some(d => d.alertState.hasLowStorage) ? theme.palette.error.main : theme.palette.text.secondary,
}}
>
Storage Alerts: {Object.values(planetDetails).filter(d => d.alertState.hasLowStorage).length}
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: Object.values(planetDetails).some(d => d.alertState.hasLargeExtractorDifference) ? theme.palette.error.main : theme.palette.text.secondary,
}}
>
Balance Alerts: {Object.values(planetDetails).filter(d => d.alertState.hasLargeExtractorDifference).length}
</Typography>
</Box> </Box>
</Box> </Box>
<IconButton <IconButton

View File

@@ -4,7 +4,7 @@ export const DiscordButton = () => {
<Box> <Box>
<Tooltip title="Come nerd out in discord about PI and this tool"> <Tooltip title="Come nerd out in discord about PI and this tool">
<Button <Button
href="https://discord.gg/GPtw5kfuJu" href="https://discord.gg/bCdXzU8PHK"
target="_blank" target="_blank"
style={{ width: "100%" }} style={{ width: "100%" }}
sx={{ color: "white", display: "block" }} sx={{ color: "white", display: "block" }}

View File

@@ -48,7 +48,7 @@ declare module "@mui/material/styles" {
} }
export const MainGrid = () => { export const MainGrid = () => {
const { characters, updateCharacter } = useContext(CharacterContext); const { characters } = useContext(CharacterContext);
const { compactMode, toggleCompactMode, alertMode, toggleAlertMode, planMode, togglePlanMode, extractionTimeMode, toggleExtractionTimeMode } = useContext(SessionContext); const { compactMode, toggleCompactMode, alertMode, toggleAlertMode, planMode, togglePlanMode, extractionTimeMode, toggleExtractionTimeMode } = useContext(SessionContext);
const [accountOrder, setAccountOrder] = useState<string[]>([]); const [accountOrder, setAccountOrder] = useState<string[]>([]);
const [allCollapsed, setAllCollapsed] = useState(false); const [allCollapsed, setAllCollapsed] = useState(false);

View File

@@ -1,6 +1,5 @@
import { ColorContext, SessionContext } from "@/app/context/Context"; import { ColorContext, SessionContext } from "@/app/context/Context";
import { PI_TYPES_MAP, STORAGE_IDS, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES, EVE_IMAGE_URL, PI_SCHEMATICS, LAUNCHPAD_IDS } from "@/const"; import { PI_TYPES_MAP, EVE_IMAGE_URL, LAUNCHPAD_IDS } from "@/const";
import { planetCalculations } from "@/planets";
import { AccessToken, PlanetWithInfo } from "@/types"; import { AccessToken, PlanetWithInfo } from "@/types";
import { PlanetCalculations, StorageInfo } from "@/types/planet"; import { PlanetCalculations, StorageInfo } from "@/types/planet";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
@@ -419,50 +418,55 @@ export const PlanetTableRow = ({
</TableRow> </TableRow>
</TableHead> </TableHead>
<TableBody> <TableBody>
{planetDetails.storageInfo.map((storage: StorageInfo, idx: number) => { {planetDetails.storageInfo
const isLaunchpad = LAUNCHPAD_IDS.includes(storage.type_id); .map(storage => ({
const fillRate = storage.fillRate; ...storage,
const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit'; isLaunchpad: LAUNCHPAD_IDS.includes(storage.type_id)
const contents = planet.info.pins.find(p => p.type_id === storage.type_id)?.contents || []; }))
.sort((a, b) => (b.isLaunchpad ? 1 : 0) - (a.isLaunchpad ? 1 : 0))
return ( .map((storage, idx) => {
<React.Fragment key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`}> const fillRate = storage.fillRate;
<TableRow> const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit';
<TableCell>{isLaunchpad ? 'Launchpad' : 'Storage'}</TableCell> const contents = planet.info.pins.find(p => p.type_id === storage.type_id)?.contents || [];
<TableCell align="right">{storage.capacity.toFixed(1)} m³</TableCell>
<TableCell align="right">{storage.used.toFixed(1)} m³</TableCell> return (
<TableCell align="right" sx={{ color }}>{fillRate.toFixed(1)}%</TableCell> <React.Fragment key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`}>
<TableCell align="right">
{storage.value > 0 ? (
storage.value >= 1000000000
? `${(storage.value / 1000000000).toFixed(2)} B`
: `${(storage.value / 1000000).toFixed(0)} M`
) : '-'} ISK
</TableCell>
</TableRow>
{contents.length > 0 && (
<TableRow> <TableRow>
<TableCell colSpan={5} sx={{ pt: 0, pb: 0 }}> <TableCell>{storage.isLaunchpad ? 'Launchpad' : 'Storage'}</TableCell>
<Table size="small"> <TableCell align="right">{storage.capacity.toFixed(1)} m³</TableCell>
<TableBody> <TableCell align="right">{storage.used.toFixed(1)} m³</TableCell>
{contents.map((content, idy) => ( <TableCell align="right" sx={{ color }}>{fillRate.toFixed(1)}%</TableCell>
<TableRow key={`content-${character.character.characterId}-${planet.planet_id}-${storage.type}-${content.type_id}-${idx}-${idy}`}> <TableCell align="right">
<TableCell sx={{ pl: 2 }}> {storage.value > 0 ? (
{PI_TYPES_MAP[content.type_id]?.name} storage.value >= 1000000000
</TableCell> ? `${(storage.value / 1000000000).toFixed(2)} B`
<TableCell align="right" colSpan={4}> : `${(storage.value / 1000000).toFixed(0)} M`
{content.amount.toFixed(1)} units ) : '-'} ISK
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableCell> </TableCell>
</TableRow> </TableRow>
)} {contents.length > 0 && (
</React.Fragment> <TableRow>
); <TableCell colSpan={5} sx={{ pt: 0, pb: 0 }}>
})} <Table size="small">
<TableBody>
{contents.map((content, idy) => (
<TableRow key={`content-${character.character.characterId}-${planet.planet_id}-${storage.type}-${content.type_id}-${idx}-${idy}`}>
<TableCell sx={{ pl: 2 }}>
{PI_TYPES_MAP[content.type_id]?.name}
</TableCell>
<TableCell align="right" colSpan={4}>
{content.amount.toFixed(1)} units
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableCell>
</TableRow>
)}
</React.Fragment>
);
})}
</TableBody> </TableBody>
</Table> </Table>
</Box> </Box>
@@ -482,27 +486,32 @@ export const PlanetTableRow = ({
> >
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{planetDetails.storageInfo.length === 0 &&<Typography fontSize={theme.custom.smallText}>No storage</Typography>} {planetDetails.storageInfo.length === 0 &&<Typography fontSize={theme.custom.smallText}>No storage</Typography>}
{planetDetails.storageInfo.map((storage: StorageInfo, idx: number) => { {planetDetails.storageInfo
const isLaunchpad = LAUNCHPAD_IDS.includes(storage.type_id); .map(storage => ({
const fillRate = storage.fillRate; ...storage,
const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit'; isLaunchpad: LAUNCHPAD_IDS.includes(storage.type_id)
}))
return ( .sort((a, b) => (b.isLaunchpad ? 1 : 0) - (a.isLaunchpad ? 1 : 0))
<div key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`} style={{ display: "flex", alignItems: "center" }}> .map((storage, idx) => {
<Typography fontSize={theme.custom.smallText} style={{ marginRight: "5px" }}> const fillRate = storage.fillRate;
{isLaunchpad ? 'L' : 'S'} const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit';
</Typography>
<Typography fontSize={theme.custom.smallText} style={{ color }}> return (
{fillRate.toFixed(1)}% <div key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`} style={{ display: "flex", alignItems: "center" }}>
</Typography> <Typography fontSize={theme.custom.smallText} style={{ marginRight: "5px" }}>
{storage.value > 0 && ( {storage.isLaunchpad ? 'L' : 'S'}
<Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px" }}>
({Math.round(storage.value / 1000000)}M)
</Typography> </Typography>
)} <Typography fontSize={theme.custom.smallText} style={{ color }}>
</div> {fillRate.toFixed(1)}%
); </Typography>
})} {storage.value > 0 && (
<Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px" }}>
({Math.round(storage.value / 1000000)}M)
</Typography>
)}
</div>
);
})}
</div> </div>
</Tooltip> </Tooltip>
</TableCell> </TableCell>

View File

@@ -19,6 +19,21 @@ import { EvePraisalResult, fetchAllPrices } from "@/eve-praisal";
import { getPlanet, getPlanetUniverse, getPlanets } from "@/planets"; import { getPlanet, getPlanetUniverse, getPlanets } from "@/planets";
import { PlanetConfig } from "@/types"; import { PlanetConfig } from "@/types";
// Add batch processing utility
const processInBatches = async <T, R>(
items: T[],
batchSize: number,
processFn: (item: T) => Promise<R>
): Promise<R[]> => {
const results: R[] = [];
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
const batchResults = await Promise.all(batch.map(processFn));
results.push(...batchResults);
}
return results;
};
const Home = () => { const Home = () => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const [characters, setCharacters] = useState<AccessToken[]>([]); const [characters, setCharacters] = useState<AccessToken[]>([]);
@@ -63,15 +78,13 @@ const Home = () => {
}; };
const refreshSession = async (characters: AccessToken[]) => { const refreshSession = async (characters: AccessToken[]) => {
return Promise.all( return processInBatches(characters, 50, async (c) => {
characters.map((c) => { try {
try { return await refreshToken(c);
return refreshToken(c); } catch {
} catch { return { ...c, needsLogin: true };
return { ...c, needsLogin: true }; }
} });
}),
);
}; };
const handleCallback = async ( const handleCallback = async (
@@ -107,24 +120,24 @@ const Home = () => {
const initializeCharacterPlanets = ( const initializeCharacterPlanets = (
characters: AccessToken[], characters: AccessToken[],
): Promise<AccessToken[]> => ): Promise<AccessToken[]> =>
Promise.all( processInBatches(characters, 50, async (c) => {
characters.map(async (c) => { if (c.needsLogin || c.character === undefined)
if (c.needsLogin || c.character === undefined) return { ...c, planets: [] };
return { ...c, planets: [] }; const planets = await getPlanets(c);
const planets = await getPlanets(c); const planetsWithInfo: PlanetWithInfo[] = await processInBatches(
const planetsWithInfo: PlanetWithInfo[] = await Promise.all( planets,
planets.map(async (p) => ({ 3,
...p, async (p) => ({
info: await getPlanet(c, p), ...p,
infoUniverse: await getPlanetUniverse(p), info: await getPlanet(c, p),
})), infoUniverse: await getPlanetUniverse(p),
); })
return { );
...c, return {
planets: planetsWithInfo, ...c,
}; planets: planetsWithInfo,
}), };
); });
const saveCharacters = (characters: AccessToken[]): AccessToken[] => { const saveCharacters = (characters: AccessToken[]): AccessToken[] => {
localStorage.setItem("characters", JSON.stringify(characters)); localStorage.setItem("characters", JSON.stringify(characters));

View File

@@ -22,10 +22,51 @@ export const getPlanets = async (character: AccessToken): Promise<Planet[]> => {
return planets; return planets;
}; };
interface CachedPlanetData {
data: PlanetInfo;
timestamp: number;
}
const CACHE_DURATION_MS = 60_000; // 1 minute
const CACHE_STORAGE_KEY = "planet_cache";
const loadCacheFromStorage = (): Map<string, CachedPlanetData> => {
try {
const stored = localStorage.getItem(CACHE_STORAGE_KEY);
if (stored) {
const parsed = JSON.parse(stored);
return new Map(Object.entries(parsed));
}
} catch (error) {
console.error("Failed to load planet cache from localStorage:", error);
}
return new Map();
};
const saveCacheToStorage = (cache: Map<string, CachedPlanetData>) => {
try {
const obj = Object.fromEntries(cache);
localStorage.setItem(CACHE_STORAGE_KEY, JSON.stringify(obj));
} catch (error) {
console.error("Failed to save planet cache to localStorage:", error);
}
};
const planetCache = loadCacheFromStorage();
export const getPlanet = async ( export const getPlanet = async (
character: AccessToken, character: AccessToken,
planet: Planet, planet: Planet,
): Promise<PlanetInfo> => { ): Promise<PlanetInfo> => {
const cacheKey = `${character.character.characterId}-${planet.planet_id}`;
const cached = planetCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION_MS) {
console.log(`[Cache HIT] Planet ${planet.planet_id} for character ${character.character.characterId}`);
return cached.data;
}
console.log(`[Cache MISS] Fetching planet ${planet.planet_id} for character ${character.character.characterId}`);
const api = new Api(); const api = new Api();
const planetInfo = ( const planetInfo = (
await api.v3.getCharactersCharacterIdPlanetsPlanetId( await api.v3.getCharactersCharacterIdPlanetsPlanetId(
@@ -36,6 +77,14 @@ export const getPlanet = async (
}, },
) )
).data; ).data;
planetCache.set(cacheKey, {
data: planetInfo,
timestamp: Date.now(),
});
saveCacheToStorage(planetCache);
return planetInfo; return planetInfo;
}; };