Compare commits

...

8 Commits

Author SHA1 Message Date
calli f49cc61da4 up deps and improve login 2026-04-14 22:26:17 +03:00
calli c37578c4e5 add drag for characters inside an account card. also minimize for easier reorder 2026-04-14 22:14:39 +03:00
calli 48da721980 fix launchpad storage calculations 2026-04-14 22:08:26 +03:00
calli bf31a7e2cb add matrix space link 2026-02-24 19:47:18 +02:00
calli 6b47b34ddf add buy me a beer button 2026-01-07 09:02:16 +02:00
calli e8f69b15a4 fix per cycle calculation 2026-01-07 08:59:34 +02:00
calli ebd39243b2 fix hourly averages 2026-01-02 21:10:58 +02:00
calli 0ee129b3ca remove planet cache logging 2026-01-02 20:53:09 +02:00
14 changed files with 1374 additions and 1098 deletions
+3 -1
View File
@@ -2,7 +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/bCdXzU8PHK) Any questions, feedback or suggestions are welcome at
[EVE PI Matrix](https://matrix.to/#/#eve-pi:calli.fi)
[EVE PI Discord](https://discord.gg/bCdXzU8PHK)
## Partner code ## Partner code
+1104 -1033
View File
File diff suppressed because it is too large Load Diff
+167 -27
View File
@@ -7,11 +7,14 @@ import { useContext, useState, useEffect } from "react";
import { PlanRow } from "./PlanRow"; import { PlanRow } from "./PlanRow";
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
import { DragDropContext, Droppable, Draggable, DropResult } from "@hello-pangea/dnd";
import { planetCalculations } from "@/planets"; import { planetCalculations } from "@/planets";
import { EvePraisalResult } from "@/eve-praisal"; import { EvePraisalResult } from "@/eve-praisal";
import { STORAGE_IDS, PI_SCHEMATICS, PI_PRODUCT_VOLUMES, STORAGE_CAPACITIES } from "@/const"; import { STORAGE_IDS, PI_SCHEMATICS, PI_PRODUCT_VOLUMES, STORAGE_CAPACITIES } from "@/const";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { PlanetCalculations, AlertState, StorageContent, StorageInfo } from "@/types/planet"; import { PlanetCalculations, AlertState, StorageContent, StorageInfo } from "@/types/planet";
import { getProgramOutputPrediction } from "../PlanetaryInteraction/ExtractionSimulation";
interface AccountTotals { interface AccountTotals {
monthlyEstimate: number; monthlyEstimate: number;
@@ -49,18 +52,27 @@ const calculatePlanetDetails = (planet: PlanetWithInfo, piPrices: EvePraisalResu
])); ]));
// Calculate extractor averages and check for large differences // Calculate extractor averages and check for large differences
const CYCLE_TIME = 30 * 60; // 30 minutes in seconds
const extractorAverages = extractors const extractorAverages = extractors
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle) .filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
.map(e => { .map(e => {
const cycleTime = e.extractor_details?.cycle_time || 3600; const installDate = new Date(e.install_time ?? "");
const expiryDate = new Date(e.expiry_time ?? "");
const programDuration = (expiryDate.getTime() - installDate.getTime()) / 1000;
const cycles = Math.floor(programDuration / CYCLE_TIME);
const qtyPerCycle = e.extractor_details?.qty_per_cycle || 0; const qtyPerCycle = e.extractor_details?.qty_per_cycle || 0;
const prediction = getProgramOutputPrediction(qtyPerCycle, CYCLE_TIME, cycles);
const totalOutput = prediction.reduce((sum, val) => sum + val, 0);
const averagePerHour = totalOutput / cycles * 2;
return { return {
typeId: e.extractor_details!.product_type_id!, typeId: e.extractor_details!.product_type_id!,
averagePerHour: (qtyPerCycle * 3600) / cycleTime averagePerHour
}; };
}); });
const hasLargeExtractorDifference = extractorAverages.length === 2 && const hasLargeExtractorDifference = extractorAverages.length === 2 &&
Math.abs(extractorAverages[0].averagePerHour - extractorAverages[1].averagePerHour) > balanceThreshold; Math.abs(extractorAverages[0].averagePerHour - extractorAverages[1].averagePerHour) > balanceThreshold;
// Calculate storage info // Calculate storage info
@@ -89,6 +101,7 @@ const calculatePlanetDetails = (planet: PlanetWithInfo, piPrices: EvePraisalResu
const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0; const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0;
return { return {
pin_id: storage.pin_id,
type: storageType, type: storageType,
type_id: storage.type_id, type_id: storage.type_id,
capacity: storageCapacity, capacity: storageCapacity,
@@ -231,6 +244,69 @@ 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, alertMode, balanceThreshold, minExtractionRate } = useContext(SessionContext); const { planMode, piPrices, alertMode, balanceThreshold, minExtractionRate } = useContext(SessionContext);
const accountName = characters.length > 0 ? (characters[0].account ?? "-") : "-";
const [characterOrder, setCharacterOrder] = useState<number[]>(() => {
try {
const saved = localStorage.getItem(`characterOrder-${accountName}`);
if (saved) {
const parsed: number[] = JSON.parse(saved);
const ids = characters.map(c => c.character.characterId);
const valid = parsed.filter(id => ids.includes(id));
const newIds = ids.filter(id => !valid.includes(id));
return [...valid, ...newIds];
}
} catch (_) { /* ignore corrupt localStorage */ }
return characters.map(c => c.character.characterId);
});
useEffect(() => {
const ids = characters.map(c => c.character.characterId);
setCharacterOrder(prev => {
const valid = prev.filter(id => ids.includes(id));
const newIds = ids.filter(id => !valid.includes(id));
return [...valid, ...newIds];
});
}, [characters]);
useEffect(() => {
if (characterOrder.length > 0) {
localStorage.setItem(`characterOrder-${accountName}`, JSON.stringify(characterOrder));
}
}, [characterOrder, accountName]);
const [collapsedCharacters, setCollapsedCharacters] = useState<Set<number>>(() => {
try {
const saved = localStorage.getItem(`collapsedCharacters-${accountName}`);
if (saved) return new Set<number>(JSON.parse(saved));
} catch (_) { /* ignore corrupt localStorage */ }
return new Set<number>();
});
const toggleCharacterCollapsed = (characterId: number) => {
setCollapsedCharacters(prev => {
const next = new Set(prev);
if (next.has(characterId)) next.delete(characterId);
else next.add(characterId);
localStorage.setItem(`collapsedCharacters-${accountName}`, JSON.stringify(Array.from(next)));
return next;
});
};
const orderedCharacters = characterOrder
.map(id => characters.find(c => c.character.characterId === id))
.filter((c): c is AccessToken => c !== undefined);
const handleCharacterDragEnd = (result: DropResult) => {
if (!result.destination) return;
const items = Array.from(characterOrder);
const [moved] = items.splice(result.source.index, 1);
items.splice(result.destination.index, 0, moved);
setCharacterOrder(items);
};
const DragDropContextComponent = DragDropContext as any;
const DroppableComponent = Droppable as any;
const DraggableComponent = Draggable as any;
const { monthlyEstimate, storageValue, planetCount, characterCount, runningExtractors, totalExtractors } = calculateAccountTotals(characters, piPrices); const { monthlyEstimate, storageValue, planetCount, characterCount, runningExtractors, totalExtractors } = calculateAccountTotals(characters, piPrices);
// Calculate planet details and alert states for each planet // Calculate planet details and alert states for each planet
@@ -410,30 +486,94 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
{localIsCollapsed ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />} {localIsCollapsed ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton> </IconButton>
</Box> </Box>
{!localIsCollapsed && characters.map((c) => ( {!localIsCollapsed && (
<Stack <DragDropContextComponent onDragEnd={handleCharacterDragEnd}>
key={c.character.characterId} <DroppableComponent droppableId={`characters-${accountName}`} direction="vertical">
direction="row" {(provided: any) => (
alignItems="flex-start" <Box ref={provided.innerRef} {...provided.droppableProps}>
> {orderedCharacters.map((c, index) => (
<CharacterRow character={c} /> <DraggableComponent
{planMode ? ( key={c.character.characterId}
<PlanRow character={c} /> draggableId={`char-${c.character.characterId}`}
) : ( index={index}
<PlanetaryInteractionRow >
character={c} {(provided: any, snapshot: any) => (
planetDetails={c.planets.reduce((acc, planet) => { <Stack
const details = planetDetails[`${c.character.characterId}-${planet.planet_id}`]; ref={provided.innerRef}
acc[planet.planet_id] = { {...provided.draggableProps}
...details, direction="row"
visibility: getAlertVisibility(details.alertState) alignItems="flex-start"
}; sx={{
return acc; opacity: snapshot.isDragging ? 0.8 : 1,
}, {} as Record<number, PlanetCalculations & { visibility: string }>)} backgroundColor: snapshot.isDragging ? theme.palette.action.hover : 'transparent',
/> borderRadius: 1,
)} }}
</Stack> >
))} <Box
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
pt: 1,
gap: 0.5,
}}
>
<Box
{...provided.dragHandleProps}
sx={{
display: 'flex',
alignItems: 'center',
cursor: 'grab',
px: 0.5,
color: theme.palette.text.disabled,
'&:hover': { color: theme.palette.text.secondary },
'&:active': { cursor: 'grabbing' },
}}
>
</Box>
<IconButton
size="small"
onClick={() => toggleCharacterCollapsed(c.character.characterId)}
sx={{
p: 0.25,
color: theme.palette.text.disabled,
'&:hover': { color: theme.palette.text.secondary },
transform: collapsedCharacters.has(c.character.characterId) ? 'rotate(0deg)' : 'rotate(90deg)',
transition: 'transform 0.2s ease-in-out',
}}
>
<ChevronRightIcon fontSize="small" />
</IconButton>
</Box>
<CharacterRow character={c} />
{!collapsedCharacters.has(c.character.characterId) && (
planMode ? (
<PlanRow character={c} />
) : (
<PlanetaryInteractionRow
character={c}
planetDetails={c.planets.reduce((acc, planet) => {
const details = planetDetails[`${c.character.characterId}-${planet.planet_id}`];
acc[planet.planet_id] = {
...details,
visibility: getAlertVisibility(details.alertState)
};
return acc;
}, {} as Record<number, PlanetCalculations & { visibility: string }>)}
/>
)
)}
</Stack>
)}
</DraggableComponent>
))}
{provided.placeholder}
</Box>
)}
</DroppableComponent>
</DragDropContextComponent>
)}
</Box> </Box>
</Paper> </Paper>
); );
+5
View File
@@ -17,6 +17,7 @@ import { GitHubButton } from "../Github/GitHubButton";
import { LoginButton } from "../Login/LoginButton"; import { LoginButton } from "../Login/LoginButton";
import { PartnerCodeButton } from "../PartnerCode/PartnerCodeButton"; import { PartnerCodeButton } from "../PartnerCode/PartnerCodeButton";
import { SettingsButton } from "../Settings/SettingsButtons"; import { SettingsButton } from "../Settings/SettingsButtons";
import { BuyMeCoffeeButton } from "../BuyMeCoffee/BuyMeCoffeeButton";
import { import {
Button, Button,
Dialog, Dialog,
@@ -132,6 +133,9 @@ function ResponsiveAppBar() {
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<PartnerCodeButton /> <PartnerCodeButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<BuyMeCoffeeButton />
</MenuItem>
</Menu> </Menu>
</Box> </Box>
<PublicIcon sx={{ display: { xs: "flex", md: "none" }, mr: 1 }} /> <PublicIcon sx={{ display: { xs: "flex", md: "none" }, mr: 1 }} />
@@ -173,6 +177,7 @@ function ResponsiveAppBar() {
</Button> </Button>
<CCPButton /> <CCPButton />
<PartnerCodeButton /> <PartnerCodeButton />
<BuyMeCoffeeButton />
</Box> </Box>
</Toolbar> </Toolbar>
</Container> </Container>
@@ -0,0 +1,17 @@
import { Box, Button, Tooltip } from "@mui/material";
export const BuyMeCoffeeButton = () => {
return (
<Box>
<Tooltip title="Support the development of this tool">
<Button
href="https://buymeacoffee.com/evepi"
target="_blank"
style={{ width: "100%" }}
sx={{ color: "white", display: "block" }}
>
By me a beer
</Button>
</Tooltip>
</Box>
);
};
+4 -11
View File
@@ -19,7 +19,6 @@ export const LoginDialog = ({
DEFAULT_SCOPES_TO_SELECT DEFAULT_SCOPES_TO_SELECT
); );
const [ssoUrl, setSsoUrl] = useState<string | undefined>(undefined); const [ssoUrl, setSsoUrl] = useState<string | undefined>(undefined);
const [loginUrl, setLoginUrl] = useState<string | undefined>(undefined);
const { EVE_SSO_CLIENT_ID, EVE_SSO_CALLBACK_URL } = const { EVE_SSO_CLIENT_ID, EVE_SSO_CALLBACK_URL } =
useContext(SessionContext); useContext(SessionContext);
@@ -30,15 +29,6 @@ export const LoginDialog = ({
}); });
}, []); }, []);
useEffect(() => {
if (!ssoUrl || selectedScopes.length === 0) return;
loginParameters(
selectedScopes,
EVE_SSO_CLIENT_ID,
EVE_SSO_CALLBACK_URL
).then((res) => setLoginUrl(ssoUrl + "?" + res));
}, [selectedScopes, ssoUrl, EVE_SSO_CLIENT_ID, EVE_SSO_CALLBACK_URL]);
return ( return (
<Dialog open={open} onClose={closeDialog}> <Dialog open={open} onClose={closeDialog}>
<DialogTitle>Select scopes to login with</DialogTitle> <DialogTitle>Select scopes to login with</DialogTitle>
@@ -59,8 +49,11 @@ export const LoginDialog = ({
<DialogActions> <DialogActions>
<Button <Button
variant="contained" variant="contained"
disabled={!ssoUrl || selectedScopes.length === 0}
onClick={() => { onClick={() => {
window.open(loginUrl, "_self"); if (!ssoUrl) return;
const params = loginParameters(selectedScopes, EVE_SSO_CLIENT_ID, EVE_SSO_CALLBACK_URL);
window.open(ssoUrl + "?" + params, "_self");
}} }}
> >
Login Login
@@ -135,7 +135,7 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
</Box> </Box>
<Box sx={{ flex: 1, minWidth: 0 }}> <Box sx={{ flex: 1, minWidth: 0 }}>
<Stack spacing={1}> <Stack spacing={1}>
{extractorPrograms.map(({ typeId, cycleTime, cycles, installTime, expiryTime }, idx) => { {extractorPrograms.map(({ typeId, cycleTime, cycles, expiryTime }, idx) => {
const prediction = getProgramOutputPrediction( const prediction = getProgramOutputPrediction(
extractors[idx].baseValue, extractors[idx].baseValue,
CYCLE_TIME, CYCLE_TIME,
@@ -169,7 +169,7 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
: 'inherit' : 'inherit'
} }
> >
Average per hour: {((extractors[idx].baseValue * 3600) / extractors[idx].cycleTime).toFixed(1)} units Average per hour: {(totalOutput / cycles * 2).toFixed(1)} units
</Typography> </Typography>
<Typography variant="body2"> <Typography variant="body2">
Expires in: <Countdown overtime={true} date={DateTime.fromISO(expiryTime).toMillis()} /> Expires in: <Countdown overtime={true} date={DateTime.fromISO(expiryTime).toMillis()} />
@@ -185,17 +185,24 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
</Typography> </Typography>
<Stack spacing={0.5}> <Stack spacing={0.5}>
{extractors.map((extractor, index) => { {extractors.map((extractor, index) => {
const averagePerHour = (extractor.baseValue * 3600) / extractor.cycleTime; const prediction = getProgramOutputPrediction(
extractor.baseValue,
CYCLE_TIME,
extractorPrograms[index].cycles
);
const totalOutput = prediction.reduce((sum, val) => sum + val, 0);
const cycles = extractorPrograms[index].cycles;
const averagePerHour = totalOutput / cycles * 2;
return ( return (
<Typography key={index} variant="body2"> <Typography key={index} variant="body2">
{PI_TYPES_MAP[extractor.typeId]?.name}: {averagePerHour.toFixed(1)} u/h {PI_TYPES_MAP[extractor.typeId]?.name}: {averagePerHour.toFixed(1)} u/h
</Typography> </Typography>
); );
})} })}
<Typography <Typography
variant="body2" variant="body2"
color="error" color="error"
sx={{ sx={{
mt: 1, mt: 1,
fontWeight: 'bold', fontWeight: 'bold',
borderTop: '1px solid', borderTop: '1px solid',
@@ -203,10 +210,27 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
pt: 1 pt: 1
}} }}
> >
Difference: {Math.abs( Difference: {(() => {
(extractors[0].baseValue * 3600 / extractors[0].cycleTime) - const prediction0 = getProgramOutputPrediction(
(extractors[1].baseValue * 3600 / extractors[1].cycleTime) extractors[0].baseValue,
).toFixed(1)} u/h CYCLE_TIME,
extractorPrograms[0].cycles
);
const totalOutput0 = prediction0.reduce((sum, val) => sum + val, 0);
const cycles0 = extractorPrograms[0].cycles;
const avg0 = totalOutput0 / cycles0 * 2;
const prediction1 = getProgramOutputPrediction(
extractors[1].baseValue,
CYCLE_TIME,
extractorPrograms[1].cycles
);
const totalOutput1 = prediction1.reduce((sum, val) => sum + val, 0);
const cycles1 = extractorPrograms[1].cycles;
const avg1 = totalOutput1 / cycles1 * 2;
return Math.abs(avg0 - avg1).toFixed(1);
})()} u/h
</Typography> </Typography>
</Stack> </Stack>
</Paper> </Paper>
@@ -438,7 +438,7 @@ export const PlanetTableRow = ({
.map((storage, idx) => { .map((storage, idx) => {
const fillRate = storage.fillRate; const fillRate = storage.fillRate;
const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit'; const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit';
const contents = planet.info.pins.find(p => p.type_id === storage.type_id)?.contents || []; const contents = planet.info.pins.find(p => p.pin_id === storage.pin_id)?.contents || [];
return ( return (
<React.Fragment key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`}> <React.Fragment key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`}>
+12 -2
View File
@@ -3,7 +3,7 @@ import "@fontsource/roboto/300.css";
import "@fontsource/roboto/400.css"; import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css"; import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css"; import "@fontsource/roboto/700.css";
import { memo, useCallback, useEffect, useState, Suspense } from "react"; import { memo, useCallback, useEffect, useRef, useState, Suspense } from "react";
import { AccessToken, CharacterUpdate, Env, PlanetWithInfo } from "../types"; import { AccessToken, CharacterUpdate, Env, PlanetWithInfo } from "../types";
import { MainGrid } from "./components/MainGrid"; import { MainGrid } from "./components/MainGrid";
import { refreshToken } from "@/esi-sso"; import { refreshToken } from "@/esi-sso";
@@ -37,6 +37,7 @@ const processInBatches = async <T, R>(
const Home = () => { const Home = () => {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const callbackHandled = useRef(false);
const [characters, setCharacters] = useState<AccessToken[]>([]); const [characters, setCharacters] = useState<AccessToken[]>([]);
const [sessionReady, setSessionReady] = useState(false); const [sessionReady, setSessionReady] = useState(false);
const [environment, setEnvironment] = useState<Env | undefined>(undefined); const [environment, setEnvironment] = useState<Env | undefined>(undefined);
@@ -93,7 +94,16 @@ const Home = () => {
characters: AccessToken[], characters: AccessToken[],
): Promise<AccessToken[]> => { ): Promise<AccessToken[]> => {
const code = searchParams?.get("code"); const code = searchParams?.get("code");
if (code) { const returnedState = searchParams?.get("state");
if (code && !callbackHandled.current) {
callbackHandled.current = true;
const expectedState = localStorage.getItem("oauth_state");
localStorage.removeItem("oauth_state");
if (!expectedState || returnedState !== expectedState) {
console.error("OAuth state mismatch — possible CSRF attack");
window.history.replaceState(null, "", "/");
return Promise.resolve(characters);
}
window.history.replaceState(null, "", "/"); window.history.replaceState(null, "", "/");
const res = await fetch(`api/token?code=${code}`); const res = await fetch(`api/token?code=${code}`);
const newCharacter: AccessToken = await res.json(); const newCharacter: AccessToken = await res.json();
+4 -2
View File
@@ -37,17 +37,19 @@ export const revokeToken = async (
}); });
}; };
export const loginParameters = async ( export const loginParameters = (
selectedScopes: string[], selectedScopes: string[],
EVE_SSO_CLIENT_ID: string, EVE_SSO_CLIENT_ID: string,
EVE_SSO_CALLBACK_URL: string, EVE_SSO_CALLBACK_URL: string,
) => { ) => {
const state = crypto.randomUUID();
localStorage.setItem("oauth_state", state);
return new URLSearchParams({ return new URLSearchParams({
response_type: "code", response_type: "code",
redirect_uri: EVE_SSO_CALLBACK_URL, redirect_uri: EVE_SSO_CALLBACK_URL,
client_id: EVE_SSO_CLIENT_ID, client_id: EVE_SSO_CLIENT_ID,
scope: selectedScopes.join(" "), scope: selectedScopes.join(" "),
state: "asfe", state,
}).toString(); }).toString();
}; };
+18 -5
View File
@@ -9,11 +9,25 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}); });
try { try {
const praisalRequest: { quantity: number; type_id: number }[] = JSON.parse( const parsed = JSON.parse(req.body);
req.body
if (!Array.isArray(parsed)) {
return res.status(400).json({ error: 'Invalid input' });
}
const praisalRequest: { quantity: number; type_id: number }[] = parsed.filter(
(item): item is { quantity: number; type_id: number } =>
item !== null &&
typeof item === 'object' &&
typeof item.quantity === 'number' &&
Number.isFinite(item.quantity) &&
item.quantity >= 0 &&
typeof item.type_id === 'number' &&
Number.isInteger(item.type_id) &&
item.type_id > 0
); );
logger.info({ logger.info({
event: 'praisal_request_parsed', event: 'praisal_request_parsed',
items: praisalRequest.length items: praisalRequest.length
}); });
@@ -27,10 +41,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return res.json(praisal); return res.json(praisal);
} catch (e) { } catch (e) {
logger.error({ logger.error({
event: 'praisal_request_failed', event: 'praisal_request_failed',
error: e, error: e,
body: req.body
}); });
return res.status(500).json({ error: 'Failed to get praisal' }); return res.status(500).json({ error: 'Failed to get praisal' });
} }
+2 -4
View File
@@ -20,9 +20,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return res.status(404).end(); return res.status(404).end();
} }
logger.info({ logger.info({
event: 'token_request_start', event: 'token_request_start',
code: code
}); });
const params = new URLSearchParams({ const params = new URLSearchParams({
@@ -88,11 +87,10 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}; };
return res.json(token); return res.json(token);
} catch (e) { } catch (e) {
logger.error({ logger.error({
event: 'token_request_failed', event: 'token_request_failed',
reason: 'api_error', reason: 'api_error',
error: e, error: e,
code: code
}); });
return res.status(500).end(); return res.status(500).end();
} }
+2 -2
View File
@@ -31,6 +31,7 @@ const CACHE_DURATION_MS = 60_000; // 1 minute
const CACHE_STORAGE_KEY = "planet_cache"; const CACHE_STORAGE_KEY = "planet_cache";
const loadCacheFromStorage = (): Map<string, CachedPlanetData> => { const loadCacheFromStorage = (): Map<string, CachedPlanetData> => {
if (typeof window === "undefined") return new Map();
try { try {
const stored = localStorage.getItem(CACHE_STORAGE_KEY); const stored = localStorage.getItem(CACHE_STORAGE_KEY);
if (stored) { if (stored) {
@@ -44,6 +45,7 @@ const loadCacheFromStorage = (): Map<string, CachedPlanetData> => {
}; };
const saveCacheToStorage = (cache: Map<string, CachedPlanetData>) => { const saveCacheToStorage = (cache: Map<string, CachedPlanetData>) => {
if (typeof window === "undefined") return;
try { try {
const obj = Object.fromEntries(cache); const obj = Object.fromEntries(cache);
localStorage.setItem(CACHE_STORAGE_KEY, JSON.stringify(obj)); localStorage.setItem(CACHE_STORAGE_KEY, JSON.stringify(obj));
@@ -62,11 +64,9 @@ export const getPlanet = async (
const cached = planetCache.get(cacheKey); const cached = planetCache.get(cacheKey);
if (cached && Date.now() - cached.timestamp < CACHE_DURATION_MS) { 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; 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(
+1
View File
@@ -6,6 +6,7 @@ export interface StorageContent {
} }
export interface StorageInfo { export interface StorageInfo {
pin_id: number;
type: string; type: string;
type_id: number; type_id: number;
capacity: number; capacity: number;