16 Commits

Author SHA1 Message Date
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
calli
7a09503ffa add a setting to alert if extraction is under desired level 2025-12-30 17:49:25 +02:00
calli
9de91f9982 Add partner code info 2025-12-27 22:44:59 +02:00
calli
14c2732fa0 some users have so many characters that we cant keep them in localSotrage. use db 2025-12-27 21:49:57 +02:00
calli
e47c572423 fix nextjs CVE 2025-12-12 12:22:33 +02:00
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
18 changed files with 594 additions and 228 deletions

View File

@@ -2,9 +2,17 @@
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) ## Partner code
Consider using EVE partner code to support the project:
```
CALLIEVE
```
## [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)

174
package-lock.json generated
View File

@@ -24,7 +24,7 @@
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"eslint": "8.42.0", "eslint": "8.42.0",
"luxon": "^3.6.1", "luxon": "^3.6.1",
"next": "^14.2.23", "next": "14.2.35",
"next-plausible": "^3.12.0", "next-plausible": "^3.12.0",
"pino": "^9.6.0", "pino": "^9.6.0",
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",
@@ -997,9 +997,9 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w=="
}, },
"node_modules/@next/env": { "node_modules/@next/env": {
"version": "14.2.23", "version": "14.2.35",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
"integrity": "sha512-CysUC9IO+2Bh0omJ3qrb47S8DtsTKbFidGm6ow4gXIG6reZybqxbkH2nhdEm1tC8SmgzDdpq3BIML0PWsmyUYA==", "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@next/eslint-plugin-next": { "node_modules/@next/eslint-plugin-next": {
@@ -1072,9 +1072,9 @@
} }
}, },
"node_modules/@next/swc-darwin-arm64": { "node_modules/@next/swc-darwin-arm64": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
"integrity": "sha512-WhtEntt6NcbABA8ypEoFd3uzq5iAnrl9AnZt9dXdO+PZLACE32z3a3qA5OoV20JrbJfSJ6Sd6EqGZTrlRnGxQQ==", "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1088,9 +1088,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
"integrity": "sha512-vwLw0HN2gVclT/ikO6EcE+LcIN+0mddJ53yG4eZd0rXkuEr/RnOaMH8wg/sYl5iz5AYYRo/l6XX7FIo6kwbw1Q==", "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1104,9 +1104,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
"integrity": "sha512-uuAYwD3At2fu5CH1wD7FpP87mnjAv4+DNvLaR9kiIi8DLStWSW304kF09p1EQfhcbUI1Py2vZlBO2VaVqMRtpg==", "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1120,9 +1120,9 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
"integrity": "sha512-Mm5KHd7nGgeJ4EETvVgFuqKOyDh+UMXHXxye6wRRFDr4FdVRI6YTxajoV2aHE8jqC14xeAMVZvLqYqS7isHL+g==", "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1136,9 +1136,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
"integrity": "sha512-Ybfqlyzm4sMSEQO6lDksggAIxnvWSG2cDWnG2jgd+MLbHYn2pvFA8DQ4pT2Vjk3Cwrv+HIg7vXJ8lCiLz79qoQ==", "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1152,9 +1152,9 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
"integrity": "sha512-OSQX94sxd1gOUz3jhhdocnKsy4/peG8zV1HVaW6DLEbEmRRtUCUQZcKxUD9atLYa3RZA+YJx+WZdOnTkDuNDNA==", "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1168,9 +1168,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
"integrity": "sha512-ezmbgZy++XpIMTcTNd0L4k7+cNI4ET5vMv/oqNfTuSXkZtSA9BURElPFyarjjGtRgZ9/zuKDHoMdZwDZIY3ehQ==", "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1184,9 +1184,9 @@
} }
}, },
"node_modules/@next/swc-win32-ia32-msvc": { "node_modules/@next/swc-win32-ia32-msvc": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA==", "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1200,9 +1200,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
"integrity": "sha512-xCtq5BD553SzOgSZ7UH5LH+OATQihydObTrCTvVzOro8QiWYKdBVwcB2Mn2MLMo6DGW9yH1LSPw7jS7HhgJgjw==", "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -6719,12 +6719,12 @@
"peer": true "peer": true
}, },
"node_modules/next": { "node_modules/next": {
"version": "14.2.23", "version": "14.2.35",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.23.tgz", "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
"integrity": "sha512-mjN3fE6u/tynneLiEg56XnthzuYw+kD7mCujgVqioxyPqbmiotUCGJpIZGS/VaPg3ZDT1tvWxiVyRzeqJFm/kw==", "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "14.2.23", "@next/env": "14.2.35",
"@swc/helpers": "0.5.5", "@swc/helpers": "0.5.5",
"busboy": "1.6.0", "busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579", "caniuse-lite": "^1.0.30001579",
@@ -6739,15 +6739,15 @@
"node": ">=18.17.0" "node": ">=18.17.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "14.2.23", "@next/swc-darwin-arm64": "14.2.33",
"@next/swc-darwin-x64": "14.2.23", "@next/swc-darwin-x64": "14.2.33",
"@next/swc-linux-arm64-gnu": "14.2.23", "@next/swc-linux-arm64-gnu": "14.2.33",
"@next/swc-linux-arm64-musl": "14.2.23", "@next/swc-linux-arm64-musl": "14.2.33",
"@next/swc-linux-x64-gnu": "14.2.23", "@next/swc-linux-x64-gnu": "14.2.33",
"@next/swc-linux-x64-musl": "14.2.23", "@next/swc-linux-x64-musl": "14.2.33",
"@next/swc-win32-arm64-msvc": "14.2.23", "@next/swc-win32-arm64-msvc": "14.2.33",
"@next/swc-win32-ia32-msvc": "14.2.23", "@next/swc-win32-ia32-msvc": "14.2.33",
"@next/swc-win32-x64-msvc": "14.2.23" "@next/swc-win32-x64-msvc": "14.2.33"
}, },
"peerDependencies": { "peerDependencies": {
"@opentelemetry/api": "^1.1.0", "@opentelemetry/api": "^1.1.0",
@@ -10258,9 +10258,9 @@
} }
}, },
"@next/env": { "@next/env": {
"version": "14.2.23", "version": "14.2.35",
"resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz",
"integrity": "sha512-CysUC9IO+2Bh0omJ3qrb47S8DtsTKbFidGm6ow4gXIG6reZybqxbkH2nhdEm1tC8SmgzDdpq3BIML0PWsmyUYA==" "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ=="
}, },
"@next/eslint-plugin-next": { "@next/eslint-plugin-next": {
"version": "14.2.23", "version": "14.2.23",
@@ -10311,57 +10311,57 @@
} }
}, },
"@next/swc-darwin-arm64": { "@next/swc-darwin-arm64": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz",
"integrity": "sha512-WhtEntt6NcbABA8ypEoFd3uzq5iAnrl9AnZt9dXdO+PZLACE32z3a3qA5OoV20JrbJfSJ6Sd6EqGZTrlRnGxQQ==", "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==",
"optional": true "optional": true
}, },
"@next/swc-darwin-x64": { "@next/swc-darwin-x64": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz",
"integrity": "sha512-vwLw0HN2gVclT/ikO6EcE+LcIN+0mddJ53yG4eZd0rXkuEr/RnOaMH8wg/sYl5iz5AYYRo/l6XX7FIo6kwbw1Q==", "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==",
"optional": true "optional": true
}, },
"@next/swc-linux-arm64-gnu": { "@next/swc-linux-arm64-gnu": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz",
"integrity": "sha512-uuAYwD3At2fu5CH1wD7FpP87mnjAv4+DNvLaR9kiIi8DLStWSW304kF09p1EQfhcbUI1Py2vZlBO2VaVqMRtpg==", "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==",
"optional": true "optional": true
}, },
"@next/swc-linux-arm64-musl": { "@next/swc-linux-arm64-musl": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz",
"integrity": "sha512-Mm5KHd7nGgeJ4EETvVgFuqKOyDh+UMXHXxye6wRRFDr4FdVRI6YTxajoV2aHE8jqC14xeAMVZvLqYqS7isHL+g==", "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==",
"optional": true "optional": true
}, },
"@next/swc-linux-x64-gnu": { "@next/swc-linux-x64-gnu": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz",
"integrity": "sha512-Ybfqlyzm4sMSEQO6lDksggAIxnvWSG2cDWnG2jgd+MLbHYn2pvFA8DQ4pT2Vjk3Cwrv+HIg7vXJ8lCiLz79qoQ==", "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==",
"optional": true "optional": true
}, },
"@next/swc-linux-x64-musl": { "@next/swc-linux-x64-musl": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz",
"integrity": "sha512-OSQX94sxd1gOUz3jhhdocnKsy4/peG8zV1HVaW6DLEbEmRRtUCUQZcKxUD9atLYa3RZA+YJx+WZdOnTkDuNDNA==", "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==",
"optional": true "optional": true
}, },
"@next/swc-win32-arm64-msvc": { "@next/swc-win32-arm64-msvc": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz",
"integrity": "sha512-ezmbgZy++XpIMTcTNd0L4k7+cNI4ET5vMv/oqNfTuSXkZtSA9BURElPFyarjjGtRgZ9/zuKDHoMdZwDZIY3ehQ==", "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==",
"optional": true "optional": true
}, },
"@next/swc-win32-ia32-msvc": { "@next/swc-win32-ia32-msvc": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz",
"integrity": "sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA==", "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==",
"optional": true "optional": true
}, },
"@next/swc-win32-x64-msvc": { "@next/swc-win32-x64-msvc": {
"version": "14.2.23", "version": "14.2.33",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.23.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz",
"integrity": "sha512-xCtq5BD553SzOgSZ7UH5LH+OATQihydObTrCTvVzOro8QiWYKdBVwcB2Mn2MLMo6DGW9yH1LSPw7jS7HhgJgjw==", "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==",
"optional": true "optional": true
}, },
"@nodelib/fs.scandir": { "@nodelib/fs.scandir": {
@@ -14078,20 +14078,20 @@
"peer": true "peer": true
}, },
"next": { "next": {
"version": "14.2.23", "version": "14.2.35",
"resolved": "https://registry.npmjs.org/next/-/next-14.2.23.tgz", "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz",
"integrity": "sha512-mjN3fE6u/tynneLiEg56XnthzuYw+kD7mCujgVqioxyPqbmiotUCGJpIZGS/VaPg3ZDT1tvWxiVyRzeqJFm/kw==", "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==",
"requires": { "requires": {
"@next/env": "14.2.23", "@next/env": "14.2.35",
"@next/swc-darwin-arm64": "14.2.23", "@next/swc-darwin-arm64": "14.2.33",
"@next/swc-darwin-x64": "14.2.23", "@next/swc-darwin-x64": "14.2.33",
"@next/swc-linux-arm64-gnu": "14.2.23", "@next/swc-linux-arm64-gnu": "14.2.33",
"@next/swc-linux-arm64-musl": "14.2.23", "@next/swc-linux-arm64-musl": "14.2.33",
"@next/swc-linux-x64-gnu": "14.2.23", "@next/swc-linux-x64-gnu": "14.2.33",
"@next/swc-linux-x64-musl": "14.2.23", "@next/swc-linux-x64-musl": "14.2.33",
"@next/swc-win32-arm64-msvc": "14.2.23", "@next/swc-win32-arm64-msvc": "14.2.33",
"@next/swc-win32-ia32-msvc": "14.2.23", "@next/swc-win32-ia32-msvc": "14.2.33",
"@next/swc-win32-x64-msvc": "14.2.23", "@next/swc-win32-x64-msvc": "14.2.33",
"@swc/helpers": "0.5.5", "@swc/helpers": "0.5.5",
"busboy": "1.6.0", "busboy": "1.6.0",
"caniuse-lite": "^1.0.30001579", "caniuse-lite": "^1.0.30001579",

View File

@@ -26,7 +26,7 @@
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"eslint": "8.42.0", "eslint": "8.42.0",
"luxon": "^3.6.1", "luxon": "^3.6.1",
"next": "^14.2.23", "next": "14.2.35",
"next-plausible": "^3.12.0", "next-plausible": "^3.12.0",
"pino": "^9.6.0", "pino": "^9.6.0",
"pino-pretty": "^13.0.0", "pino-pretty": "^13.0.0",

View File

@@ -12,6 +12,7 @@ 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;
@@ -22,15 +23,17 @@ interface AccountTotals {
totalExtractors: number; totalExtractors: number;
} }
const calculateAlertState = (planetDetails: PlanetCalculations): AlertState => { const calculateAlertState = (planetDetails: PlanetCalculations, minExtractionRate: number): AlertState => {
const hasLowStorage = planetDetails.storageInfo.some(storage => storage.fillRate > 60); const hasLowStorage = planetDetails.storageInfo.some(storage => storage.fillRate > 60);
const hasLowImports = planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24); const hasLowImports = planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24);
const hasLowExtractionRate = planetDetails.extractorAverages.length > 0 && minExtractionRate > 0 && planetDetails.extractorAverages.some(avg => avg.averagePerHour < minExtractionRate);
return { return {
expired: planetDetails.expired, expired: planetDetails.expired,
hasLowStorage, hasLowStorage,
hasLowImports, hasLowImports,
hasLargeExtractorDifference: planetDetails.hasLargeExtractorDifference hasLargeExtractorDifference: planetDetails.hasLargeExtractorDifference,
hasLowExtractionRate
}; };
}; };
@@ -47,14 +50,23 @@ 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
}; };
}); });
@@ -228,7 +240,7 @@ const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalR
export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { characters: AccessToken[], isCollapsed?: boolean }) => { export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { characters: AccessToken[], isCollapsed?: boolean }) => {
const theme = useTheme(); const theme = useTheme();
const [localIsCollapsed, setLocalIsCollapsed] = useState(false); const [localIsCollapsed, setLocalIsCollapsed] = useState(false);
const { planMode, piPrices, alertMode, balanceThreshold } = useContext(SessionContext); const { planMode, piPrices, alertMode, balanceThreshold, minExtractionRate } = useContext(SessionContext);
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
@@ -237,7 +249,7 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
const details = calculatePlanetDetails(planet, piPrices, balanceThreshold); const details = calculatePlanetDetails(planet, piPrices, balanceThreshold);
acc[`${character.character.characterId}-${planet.planet_id}`] = { acc[`${character.character.characterId}-${planet.planet_id}`] = {
...details, ...details,
alertState: calculateAlertState(details) alertState: calculateAlertState(details, minExtractionRate)
}; };
}); });
return acc; return acc;
@@ -254,16 +266,18 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
if (alertState.hasLowStorage) return 'visible'; if (alertState.hasLowStorage) return 'visible';
if (alertState.hasLowImports) return 'visible'; if (alertState.hasLowImports) return 'visible';
if (alertState.hasLargeExtractorDifference) return 'visible'; if (alertState.hasLargeExtractorDifference) return 'visible';
if (alertState.hasLowExtractionRate) return 'visible';
return 'hidden'; return 'hidden';
}; };
// Check if any planet in the account has alerts // Check if any planet in the account has alerts
const hasAnyAlerts = Object.values(planetDetails).some(details => { const hasAnyAlerts = Object.values(planetDetails).some(details => {
const alertState = calculateAlertState(details); const alertState = calculateAlertState(details, minExtractionRate);
return alertState.expired || return alertState.expired ||
alertState.hasLowStorage || alertState.hasLowStorage ||
alertState.hasLowImports || alertState.hasLowImports ||
alertState.hasLargeExtractorDifference; alertState.hasLargeExtractorDifference ||
alertState.hasLowExtractionRate;
}); });
// If in alert mode and no alerts, hide the entire card // If in alert mode and no alerts, hide the entire card
@@ -376,6 +390,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

@@ -15,7 +15,9 @@ import { CCPButton } from "../CCP/CCPButton";
import { DiscordButton } from "../Discord/DiscordButton"; import { DiscordButton } from "../Discord/DiscordButton";
import { GitHubButton } from "../Github/GitHubButton"; import { GitHubButton } from "../Github/GitHubButton";
import { LoginButton } from "../Login/LoginButton"; import { LoginButton } from "../Login/LoginButton";
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,
@@ -128,6 +130,12 @@ function ResponsiveAppBar() {
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<CCPButton /> <CCPButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<PartnerCodeButton />
</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 }} />
@@ -168,6 +176,8 @@ function ResponsiveAppBar() {
FAQ FAQ
</Button> </Button>
<CCPButton /> <CCPButton />
<PartnerCodeButton />
<BuyMeCoffeeButton />
</Box> </Box>
</Toolbar> </Toolbar>
</Container> </Container>

View File

@@ -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>
);
};

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

@@ -0,0 +1,32 @@
import { Box, Button, Tooltip } from "@mui/material";
import { useState } from "react";
export const PartnerCodeButton = () => {
const [copied, setCopied] = useState(false);
const handleClick = () => {
navigator.clipboard.writeText("CALLIEVE");
setCopied(true);
setTimeout(() => setCopied(false), 2000);
};
return (
<Box>
<Tooltip
title={
copied
? "Copied to clipboard!"
: "Click to copy partner code - Use for CCP purchases to support this project"
}
>
<Button
onClick={handleClick}
style={{ width: "100%" }}
sx={{ color: "white", display: "block" }}
>
Partner Code: CALLIEVE
</Button>
</Tooltip>
</Box>
);
};

View File

@@ -1,8 +1,9 @@
import React from 'react'; import React, { useContext } from 'react';
import { Box, Paper, Typography, Stack } from '@mui/material'; import { Box, Paper, Typography, Stack } from '@mui/material';
import { Line } from 'react-chartjs-2'; import { Line } from 'react-chartjs-2';
import { getProgramOutputPrediction } from './ExtractionSimulation'; import { getProgramOutputPrediction } from './ExtractionSimulation';
import { PI_TYPES_MAP } from '@/const'; import { PI_TYPES_MAP } from '@/const';
import { SessionContext } from '@/app/context/Context';
import { import {
Chart as ChartJS, Chart as ChartJS,
CategoryScale, CategoryScale,
@@ -41,6 +42,7 @@ interface ExtractionSimulationTooltipProps {
export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipProps> = ({ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipProps> = ({
extractors extractors
}) => { }) => {
const { minExtractionRate } = useContext(SessionContext);
const CYCLE_TIME = 30 * 60; // 30 minutes in seconds const CYCLE_TIME = 30 * 60; // 30 minutes in seconds
// Calculate program duration and cycles for each extractor // Calculate program duration and cycles for each extractor
@@ -133,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,
@@ -159,8 +161,15 @@ export const ExtractionSimulationTooltip: React.FC<ExtractionSimulationTooltipPr
<Typography variant="body2"> <Typography variant="body2">
Average per Cycle: {(totalOutput / cycles).toFixed(1)} units Average per Cycle: {(totalOutput / cycles).toFixed(1)} units
</Typography> </Typography>
<Typography variant="body2"> <Typography
Average per hour: {(totalOutput / cycles * 2).toFixed(1)} units variant="body2"
color={
minExtractionRate > 0 && (extractors[idx].baseValue * 3600) / extractors[idx].cycleTime < minExtractionRate
? 'error'
: 'inherit'
}
>
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()} />
@@ -176,7 +185,14 @@ 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
@@ -194,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>

View File

@@ -8,7 +8,7 @@ import { PlanetCalculations } from "@/types/planet";
import React, { useContext } from "react"; import React, { useContext } from "react";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import Countdown from "react-countdown"; import Countdown from "react-countdown";
import { ColorContext } from "@/app/context/Context"; import { ColorContext, SessionContext } from "@/app/context/Context";
import { ExtractionSimulationTooltip } from "./ExtractionSimulationTooltip"; import { ExtractionSimulationTooltip } from "./ExtractionSimulationTooltip";
import { timeColor } from "./alerts"; import { timeColor } from "./alerts";
@@ -40,6 +40,7 @@ export const PlanetCard = ({
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { colors } = useContext(ColorContext); const { colors } = useContext(ColorContext);
const { minExtractionRate } = useContext(SessionContext);
const extractorConfigs: ExtractorConfig[] = planetDetails.extractors const extractorConfigs: ExtractorConfig[] = planetDetails.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)
@@ -51,6 +52,8 @@ export const PlanetCard = ({
expiryTime: e.expiry_time ?? "" expiryTime: e.expiry_time ?? ""
})); }));
const hasLowExtractionRate = planetDetails.extractorAverages.length > 0 && minExtractionRate > 0 && planetDetails.extractorAverages.some(avg => avg.averagePerHour < minExtractionRate);
return ( return (
<Tooltip <Tooltip
title={ title={
@@ -114,9 +117,30 @@ export const PlanetCard = ({
/> />
)} )}
<div style={{ position: "absolute", top: 5, left: 10 }}> <div style={{ position: "absolute", top: 5, left: 10 }}>
<Typography fontSize={theme.custom.smallText}> <Typography
fontSize={theme.custom.smallText}
color={(planetDetails.hasLargeExtractorDifference || hasLowExtractionRate) ? 'error' : 'inherit'}
>
{planet.infoUniverse?.name} {planet.infoUniverse?.name}
</Typography> </Typography>
{planetDetails.hasLargeExtractorDifference && (
<Typography
fontSize={theme.custom.smallText}
color="error"
sx={{ opacity: 0.7 }}
>
off-balance
</Typography>
)}
{hasLowExtractionRate && (
<Typography
fontSize={theme.custom.smallText}
color="error"
sx={{ opacity: 0.7 }}
>
low-extraction
</Typography>
)}
{planetDetails.extractors.map((e, idx) => { {planetDetails.extractors.map((e, idx) => {
const average = planetDetails.extractorAverages[idx]; const average = planetDetails.extractorAverages[idx];
return ( return (

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";
@@ -56,7 +55,7 @@ export const PlanetTableRow = ({
planetDetails: PlanetCalculations; planetDetails: PlanetCalculations;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { showProductIcons, extractionTimeMode, alertMode } = useContext(SessionContext); const { showProductIcons, extractionTimeMode, alertMode, minExtractionRate } = useContext(SessionContext);
const { colors } = useContext(ColorContext); const { colors } = useContext(ColorContext);
const [planetRenderOpen, setPlanetRenderOpen] = useState(false); const [planetRenderOpen, setPlanetRenderOpen] = useState(false);
@@ -104,11 +103,13 @@ export const PlanetTableRow = ({
}; };
// Check if there are any alerts // Check if there are any alerts
const hasLowExtractionRate = planetDetails.extractorAverages.length > 0 && minExtractionRate > 0 && planetDetails.extractorAverages.some(avg => avg.averagePerHour < minExtractionRate);
const hasAlerts = alertMode && ( const hasAlerts = alertMode && (
planetDetails.expired || planetDetails.expired ||
planetDetails.storageInfo.some(storage => storage.fillRate > 60) || planetDetails.storageInfo.some(storage => storage.fillRate > 60) ||
planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24) || planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24) ||
planetDetails.hasLargeExtractorDifference planetDetails.hasLargeExtractorDifference ||
hasLowExtractionRate
); );
// If in alert mode and no alerts, hide the row // If in alert mode and no alerts, hide the row
@@ -228,7 +229,7 @@ export const PlanetTableRow = ({
<Stack spacing={0}> <Stack spacing={0}>
<Typography <Typography
fontSize={theme.custom.smallText} fontSize={theme.custom.smallText}
color={planetDetails.hasLargeExtractorDifference ? 'error' : 'inherit'} color={(planetDetails.hasLargeExtractorDifference || hasLowExtractionRate) ? 'error' : 'inherit'}
> >
{planetInfoUniverse?.name} {planetInfoUniverse?.name}
</Typography> </Typography>
@@ -241,6 +242,15 @@ export const PlanetTableRow = ({
off-balance off-balance
</Typography> </Typography>
)} )}
{hasLowExtractionRate && (
<Typography
fontSize={theme.custom.smallText}
color="error"
sx={{ opacity: 0.7 }}
>
low-extraction
</Typography>
)}
</Stack> </Stack>
</Tooltip> </Tooltip>
</div> </div>
@@ -419,50 +429,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))
.map((storage, idx) => {
const fillRate = storage.fillRate;
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 || [];
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}`}>
<TableRow>
<TableCell>{isLaunchpad ? 'Launchpad' : 'Storage'}</TableCell>
<TableCell align="right">{storage.capacity.toFixed(1)} m³</TableCell>
<TableCell align="right">{storage.used.toFixed(1)} m³</TableCell>
<TableCell align="right" sx={{ color }}>{fillRate.toFixed(1)}%</TableCell>
<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 +497,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)
}))
.sort((a, b) => (b.isLaunchpad ? 1 : 0) - (a.isLaunchpad ? 1 : 0))
.map((storage, idx) => {
const fillRate = storage.fillRate;
const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit';
return ( return (
<div key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`} style={{ display: "flex", alignItems: "center" }}> <div key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`} style={{ display: "flex", alignItems: "center" }}>
<Typography fontSize={theme.custom.smallText} style={{ marginRight: "5px" }}> <Typography fontSize={theme.custom.smallText} style={{ marginRight: "5px" }}>
{isLaunchpad ? 'L' : 'S'} {storage.isLaunchpad ? 'L' : 'S'}
</Typography>
<Typography fontSize={theme.custom.smallText} style={{ color }}>
{fillRate.toFixed(1)}%
</Typography>
{storage.value > 0 && (
<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

@@ -21,7 +21,7 @@ import React, { useState, useContext } from "react";
export const SettingsButton = () => { export const SettingsButton = () => {
const { colors, setColors } = useContext(ColorContext); const { colors, setColors } = useContext(ColorContext);
const { balanceThreshold, setBalanceThreshold, showProductIcons, setShowProductIcons } = useContext(SessionContext); const { balanceThreshold, setBalanceThreshold, minExtractionRate, setMinExtractionRate, showProductIcons, setShowProductIcons } = useContext(SessionContext);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleClickOpen = () => { const handleClickOpen = () => {
@@ -51,6 +51,13 @@ export const SettingsButton = () => {
setShowProductIcons(event.target.checked); setShowProductIcons(event.target.checked);
}; };
const handleMinExtractionRateChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(event.target.value);
if (!isNaN(value) && value >= 0 && value <= 100000) {
setMinExtractionRate(value);
}
};
return ( return (
<Tooltip title="Toggle settings dialog"> <Tooltip title="Toggle settings dialog">
<> <>
@@ -93,6 +100,19 @@ export const SettingsButton = () => {
error={balanceThreshold < 0 || balanceThreshold > 100000} error={balanceThreshold < 0 || balanceThreshold > 100000}
/> />
</Box> </Box>
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle1">Minimum Extraction Rate</Typography>
<TextField
type="number"
value={minExtractionRate}
onChange={handleMinExtractionRateChange}
fullWidth
margin="normal"
inputProps={{ min: 0, max: 100000 }}
helperText="Alert if extraction per hour is below this value (0-100,000, 0 = disabled)"
error={minExtractionRate < 0 || minExtractionRate > 100000}
/>
</Box>
{Object.keys(colors).map((key) => { {Object.keys(colors).map((key) => {
return ( return (
<div key={`color-row-${key}`}> <div key={`color-row-${key}`}>

View File

@@ -39,6 +39,8 @@ export const SessionContext = createContext<{
}) => PlanetConfig; }) => PlanetConfig;
balanceThreshold: number; balanceThreshold: number;
setBalanceThreshold: Dispatch<SetStateAction<number>>; setBalanceThreshold: Dispatch<SetStateAction<number>>;
minExtractionRate: number;
setMinExtractionRate: Dispatch<SetStateAction<number>>;
showProductIcons: boolean; showProductIcons: boolean;
setShowProductIcons: (show: boolean) => void; setShowProductIcons: (show: boolean) => void;
}>({ }>({
@@ -68,6 +70,8 @@ export const SessionContext = createContext<{
}, },
balanceThreshold: 1000, balanceThreshold: 1000,
setBalanceThreshold: () => {}, setBalanceThreshold: () => {},
minExtractionRate: 0,
setMinExtractionRate: () => {},
showProductIcons: false, showProductIcons: false,
setShowProductIcons: () => {}, setShowProductIcons: () => {},
}); });

View File

@@ -18,6 +18,22 @@ import { useSearchParams } from "next/navigation";
import { EvePraisalResult, fetchAllPrices } from "@/eve-praisal"; 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";
import { saveCharacters as saveCharactersDB, loadCharacters } from "@/storage";
// 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();
@@ -30,6 +46,7 @@ const Home = () => {
undefined, undefined,
); );
const [balanceThreshold, setBalanceThreshold] = useState(1000); const [balanceThreshold, setBalanceThreshold] = useState(1000);
const [minExtractionRate, setMinExtractionRate] = useState(0);
const [showProductIcons, setShowProductIcons] = useState(false); const [showProductIcons, setShowProductIcons] = useState(false);
const [extractionTimeMode, setExtractionTimeMode] = useState(false); const [extractionTimeMode, setExtractionTimeMode] = useState(false);
@@ -63,15 +80,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 (
@@ -95,40 +110,34 @@ const Home = () => {
return Promise.resolve(characters); return Promise.resolve(characters);
}; };
const initializeCharacters = useCallback((): AccessToken[] => { const initializeCharacters = useCallback(async (): Promise<AccessToken[]> => {
const localStorageCharacters = localStorage.getItem("characters"); return await loadCharacters();
if (localStorageCharacters) {
const characterArray: AccessToken[] = JSON.parse(localStorageCharacters);
return characterArray.filter((c) => c.access_token && c.character);
}
return [];
}, []); }, []);
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 = async (characters: AccessToken[]): Promise<AccessToken[]> => {
localStorage.setItem("characters", JSON.stringify(characters)); return await saveCharactersDB(characters);
return characters;
}; };
const restoreCharacters = (characters: AccessToken[]) => { const restoreCharacters = (characters: AccessToken[]) => {
@@ -266,8 +275,8 @@ const Home = () => {
useEffect(() => { useEffect(() => {
const ESI_CACHE_TIME_MS = 3000000; const ESI_CACHE_TIME_MS = 3000000;
const interval = setInterval(() => { const interval = setInterval(async () => {
const characters = initializeCharacters(); const characters = await initializeCharacters();
refreshSession(characters) refreshSession(characters)
.then(saveCharacters) .then(saveCharacters)
.then(initializeCharacterPlanets) .then(initializeCharacterPlanets)
@@ -297,6 +306,8 @@ const Home = () => {
readPlanetConfig, readPlanetConfig,
balanceThreshold, balanceThreshold,
setBalanceThreshold, setBalanceThreshold,
minExtractionRate,
setMinExtractionRate,
showProductIcons, showProductIcons,
setShowProductIcons, setShowProductIcons,
}} }}

View File

@@ -22,10 +22,49 @@ 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) {
return cached.data;
}
const api = new Api(); const api = new Api();
const planetInfo = ( const planetInfo = (
await api.v3.getCharactersCharacterIdPlanetsPlanetId( await api.v3.getCharactersCharacterIdPlanetsPlanetId(
@@ -36,6 +75,14 @@ export const getPlanet = async (
}, },
) )
).data; ).data;
planetCache.set(cacheKey, {
data: planetInfo,
timestamp: Date.now(),
});
saveCacheToStorage(planetCache);
return planetInfo; return planetInfo;
}; };

107
src/storage.ts Normal file
View File

@@ -0,0 +1,107 @@
import { AccessToken } from "./types";
const DB_NAME = "eve-pi-db";
const DB_VERSION = 1;
const STORE_NAME = "characters";
// Initialize IndexedDB
const initDB = (): Promise<IDBDatabase> => {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (event) => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
};
});
};
// Save characters to IndexedDB
export const saveCharacters = async (
characters: AccessToken[]
): Promise<AccessToken[]> => {
try {
const db = await initDB();
const transaction = db.transaction([STORE_NAME], "readwrite");
const store = transaction.objectStore(STORE_NAME);
store.put(characters, "characters");
return new Promise((resolve, reject) => {
transaction.oncomplete = () => {
db.close();
resolve(characters);
};
transaction.onerror = () => {
db.close();
reject(transaction.error);
};
});
} catch (error) {
console.error("Failed to save to IndexedDB:", error);
// Fallback: save minimal data to localStorage
try {
const minimalCharacters = characters.map((c) => ({
...c,
planets: [], // Strip planet data to reduce size
}));
localStorage.setItem("characters", JSON.stringify(minimalCharacters));
console.warn("Saved minimal character data to localStorage fallback");
} catch (storageError) {
console.error("Failed to save to localStorage fallback:", storageError);
}
return characters;
}
};
// Load characters from IndexedDB
export const loadCharacters = async (): Promise<AccessToken[]> => {
try {
const db = await initDB();
const transaction = db.transaction([STORE_NAME], "readonly");
const store = transaction.objectStore(STORE_NAME);
const request = store.get("characters");
return new Promise((resolve, reject) => {
request.onsuccess = () => {
db.close();
const characters = request.result as AccessToken[] | undefined;
if (characters && characters.length > 0) {
resolve(characters);
} else {
// Try localStorage migration
resolve(migrateFromLocalStorage());
}
};
request.onerror = () => {
db.close();
reject(request.error);
};
});
} catch (error) {
console.error("Failed to load from IndexedDB:", error);
// Fallback to localStorage
return migrateFromLocalStorage();
}
};
// Migrate data from localStorage to IndexedDB
const migrateFromLocalStorage = (): AccessToken[] => {
try {
const localStorageCharacters = localStorage.getItem("characters");
if (localStorageCharacters) {
const characterArray: AccessToken[] = JSON.parse(localStorageCharacters);
const filtered = characterArray.filter((c) => c.access_token && c.character);
// Don't delete from localStorage yet - keep as backup
return filtered;
}
} catch (error) {
console.error("Failed to migrate from localStorage:", error);
}
return [];
};

View File

@@ -32,6 +32,7 @@ export interface AlertState {
hasLowStorage: boolean; hasLowStorage: boolean;
hasLowImports: boolean; hasLowImports: boolean;
hasLargeExtractorDifference: boolean; hasLargeExtractorDifference: boolean;
hasLowExtractionRate: boolean;
} }
export interface ExtractorAverage { export interface ExtractorAverage {