41 Commits
v1.0.4 ... main

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
calli
3a0843e54c make keys unique for the new tooltip 2025-05-02 22:00:40 +03:00
calli
e43bd91bef make active filters more visible 2025-05-02 21:54:22 +03:00
calli
cc76765278 add a storage tooltip 2025-05-02 21:54:09 +03:00
calli
73b54f6bf5 hoist calculations and alerts to accountCard level 2025-05-02 21:41:48 +03:00
calli
cbef0fd39b rename timeColors to alerts to better describe the alert logic file 2025-04-30 17:57:52 +03:00
calli
e085fcd59b add import tooltip to show import spesifics 2025-04-30 17:30:36 +03:00
calli
370400ce99 extract launchpad ids to const and recolor storage fill rates 2025-04-28 18:32:26 +03:00
calli
93507ea98e add pino logger configuration correctly 2025-04-28 18:27:35 +03:00
calli
7915d2bd29 update docker compose file 2025-04-28 18:27:18 +03:00
calli
294720f776 use planets last_update to calculate the imports depletion 2025-04-28 18:10:32 +03:00
calli
f60003accf add log level to env 2025-04-28 17:57:51 +03:00
calli
6f28cc0093 Add import exhaustion estimate 2025-04-28 17:55:44 +03:00
calli
c986884ed5 Improve logging and handle errors beter 2025-04-28 17:55:31 +03:00
calli
4fc97d473e add toggle to show exact date time when extractors need a restart 2025-04-28 17:21:46 +03:00
calli
de49595f55 ui tweaks. add FAQ section. move filters to submenu 2025-04-25 09:38:10 +03:00
calli
a1f682e9fc Align tables a bit better and add setting to show icons instead of product texts 2025-04-25 09:01:51 +03:00
calli
67acea9be4 increase automatic refresh to 5 minutes 2025-04-23 18:59:34 +03:00
calli
98b450fcc7 rearrange character layout and align tables to character image 2025-04-23 18:52:05 +03:00
calli
eb15696241 make account card clickable for easier collapse action 2025-04-23 17:59:50 +03:00
calli
741b2480b9 add account level statistics 2025-04-23 17:41:09 +03:00
calli
b185e5d044 hide extraction simulation for planets without extractors 2025-04-23 15:23:57 +03:00
calli
f0d4708b43 show extraction tooltip only if we have extractors 2025-04-23 15:23:32 +03:00
calli
0f33a7ff0c Move activity level local storage read to useState 2025-04-23 15:18:32 +03:00
calli
bbdcece163 Show imports per planet 2025-04-23 15:15:26 +03:00
calli
2a1e74ca79 Remove sentry tunnel routing 2025-04-23 15:14:59 +03:00
40 changed files with 2236 additions and 702 deletions

View File

@@ -4,3 +4,4 @@ EVE_SSO_SECRET=Secret Key
EVE_SSO_CALLBACK_URL=Callback URL (This should be the domain you are hosting at or if run locally it should be http://localhost:3000) EVE_SSO_CALLBACK_URL=Callback URL (This should be the domain you are hosting at or if run locally it should be http://localhost:3000)
NEXT_PUBLIC_PRAISAL_URL=https://praisal.avanto.tk/appraisal/structured.json?persist=no NEXT_PUBLIC_PRAISAL_URL=https://praisal.avanto.tk/appraisal/structured.json?persist=no
SENTRY_AUTH_TOKEN=Sentry token for error reporting. SENTRY_AUTH_TOKEN=Sentry token for error reporting.
LOG_LEVEL=warn

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)

View File

@@ -1,5 +1,3 @@
---
version: "2.1"
services: services:
eve-pi: eve-pi:
image: ghcr.io/calli-eve/eve-pi:latest image: ghcr.io/calli-eve/eve-pi:latest
@@ -10,6 +8,7 @@ services:
- EVE_SSO_SECRET=${EVE_SSO_SECRET} - EVE_SSO_SECRET=${EVE_SSO_SECRET}
- NEXT_PUBLIC_PRAISAL_URL=${NEXT_PUBLIC_PRAISAL_URL} - NEXT_PUBLIC_PRAISAL_URL=${NEXT_PUBLIC_PRAISAL_URL}
- SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} - SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN}
- LOG_LEVEL=warn
ports: ports:
- 3000:3000 - 3000:3000
restart: unless-stopped restart: unless-stopped

View File

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

539
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"@mui/material": "^5.13.5", "@mui/material": "^5.13.5",
"@sentry/nextjs": "^8.2.1", "@sentry/nextjs": "^8.2.1",
"@types/node": "20.3.1", "@types/node": "20.3.1",
"@types/pino": "^7.0.4",
"@types/react": "18.2.12", "@types/react": "18.2.12",
"@types/react-dom": "18.2.5", "@types/react-dom": "18.2.5",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
@@ -23,8 +24,10 @@
"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-pretty": "^13.0.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
@@ -994,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": {
@@ -1069,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"
], ],
@@ -1085,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"
], ],
@@ -1101,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"
], ],
@@ -1117,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"
], ],
@@ -1133,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"
], ],
@@ -1149,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"
], ],
@@ -1165,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"
], ],
@@ -1181,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"
], ],
@@ -1197,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"
], ],
@@ -2561,6 +2564,15 @@
"@types/pg": "*" "@types/pg": "*"
} }
}, },
"node_modules/@types/pino": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/@types/pino/-/pino-7.0.4.tgz",
"integrity": "sha512-yKw1UbZOTe7vP1xMQT+oz3FexwgIpBTrM+AC62vWgAkNRULgLTJWfYX+H5/sKPm8VXFbIcXkC3VZPyuaNioZFg==",
"license": "MIT",
"dependencies": {
"pino": "*"
}
},
"node_modules/@types/prop-types": { "node_modules/@types/prop-types": {
"version": "15.7.5", "version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
@@ -3175,6 +3187,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/autoprefixer": { "node_modules/autoprefixer": {
"version": "10.4.14", "version": "10.4.14",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
@@ -3713,6 +3734,12 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
"license": "MIT"
},
"node_modules/commander": { "node_modules/commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -3845,6 +3872,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -5046,6 +5082,12 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
"license": "MIT"
},
"node_modules/fast-deep-equal": { "node_modules/fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -5102,6 +5144,21 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
}, },
"node_modules/fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
"license": "MIT"
},
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@@ -5607,6 +5664,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
"license": "MIT"
},
"node_modules/hoist-non-react-statics": { "node_modules/hoist-non-react-statics": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -6302,6 +6365,15 @@
"url": "https://github.com/chalk/supports-color?sponsor=1" "url": "https://github.com/chalk/supports-color?sponsor=1"
} }
}, },
"node_modules/joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -6647,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",
@@ -6667,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",
@@ -6942,6 +7014,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -7189,6 +7270,67 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/pino": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz",
"integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^4.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
},
"bin": {
"pino": "bin.js"
}
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
}
},
"node_modules/pino-pretty": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz",
"integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==",
"license": "MIT",
"dependencies": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^3.0.2",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pump": "^3.0.0",
"secure-json-parse": "^2.4.0",
"sonic-boom": "^4.0.1",
"strip-json-comments": "^3.1.1"
},
"bin": {
"pino-pretty": "bin.js"
}
},
"node_modules/pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
"license": "MIT"
},
"node_modules/possible-typed-array-names": { "node_modules/possible-typed-array-names": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@@ -7332,6 +7474,22 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/process-warning": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
"integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/progress": { "node_modules/progress": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -7393,6 +7551,12 @@
} }
] ]
}, },
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/raf-schd": { "node_modules/raf-schd": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
@@ -7548,6 +7712,15 @@
"node": ">=8.10.0" "node": ">=8.10.0"
} }
}, },
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
}
},
"node_modules/redux": { "node_modules/redux": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -7900,6 +8073,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.23.2", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -7928,6 +8110,12 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==",
"license": "BSD-3-Clause"
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.0", "version": "7.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
@@ -8210,6 +8398,15 @@
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
}, },
"node_modules/sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map": { "node_modules/source-map": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -8248,6 +8445,15 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
},
"node_modules/stacktrace-parser": { "node_modules/stacktrace-parser": {
"version": "0.1.10", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz",
@@ -8746,6 +8952,15 @@
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
}, },
"node_modules/thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
}
},
"node_modules/three": { "node_modules/three": {
"version": "0.154.0", "version": "0.154.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz", "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz",
@@ -10043,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",
@@ -10096,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": {
@@ -11006,6 +11221,14 @@
"@types/pg": "*" "@types/pg": "*"
} }
}, },
"@types/pino": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/@types/pino/-/pino-7.0.4.tgz",
"integrity": "sha512-yKw1UbZOTe7vP1xMQT+oz3FexwgIpBTrM+AC62vWgAkNRULgLTJWfYX+H5/sKPm8VXFbIcXkC3VZPyuaNioZFg==",
"requires": {
"pino": "*"
}
},
"@types/prop-types": { "@types/prop-types": {
"version": "15.7.5", "version": "15.7.5",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
@@ -11494,6 +11717,11 @@
"integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==",
"dev": true "dev": true
}, },
"atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="
},
"autoprefixer": { "autoprefixer": {
"version": "10.4.14", "version": "10.4.14",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
@@ -11828,6 +12056,11 @@
"simple-swizzle": "^0.2.2" "simple-swizzle": "^0.2.2"
} }
}, },
"colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
"integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="
},
"commander": { "commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
@@ -11928,6 +12161,11 @@
"is-data-view": "^1.0.1" "is-data-view": "^1.0.1"
} }
}, },
"dateformat": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="
},
"debug": { "debug": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -12757,6 +12995,11 @@
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="
}, },
"fast-copy": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
"integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="
},
"fast-deep-equal": { "fast-deep-equal": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -12807,6 +13050,16 @@
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
}, },
"fast-redact": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
"integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="
},
"fast-safe-stringify": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="
},
"fast-uri": { "fast-uri": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@@ -13135,6 +13388,11 @@
"function-bind": "^1.1.2" "function-bind": "^1.1.2"
} }
}, },
"help-me": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
"integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="
},
"hoist-non-react-statics": { "hoist-non-react-statics": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@@ -13565,6 +13823,11 @@
} }
} }
}, },
"joycon": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
"integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="
},
"js-tokens": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -13815,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",
@@ -13985,6 +14248,11 @@
"es-object-atoms": "^1.0.0" "es-object-atoms": "^1.0.0"
} }
}, },
"on-exit-leak-free": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
"integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="
},
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@@ -14151,6 +14419,57 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
}, },
"pino": {
"version": "9.6.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.6.0.tgz",
"integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==",
"requires": {
"atomic-sleep": "^1.0.0",
"fast-redact": "^3.1.1",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pino-std-serializers": "^7.0.0",
"process-warning": "^4.0.0",
"quick-format-unescaped": "^4.0.3",
"real-require": "^0.2.0",
"safe-stable-stringify": "^2.3.1",
"sonic-boom": "^4.0.1",
"thread-stream": "^3.0.0"
}
},
"pino-abstract-transport": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
"integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"requires": {
"split2": "^4.0.0"
}
},
"pino-pretty": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.0.0.tgz",
"integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==",
"requires": {
"colorette": "^2.0.7",
"dateformat": "^4.6.3",
"fast-copy": "^3.0.2",
"fast-safe-stringify": "^2.1.1",
"help-me": "^5.0.0",
"joycon": "^3.1.1",
"minimist": "^1.2.6",
"on-exit-leak-free": "^2.1.0",
"pino-abstract-transport": "^2.0.0",
"pump": "^3.0.0",
"secure-json-parse": "^2.4.0",
"sonic-boom": "^4.0.1",
"strip-json-comments": "^3.1.1"
}
},
"pino-std-serializers": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
"integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="
},
"possible-typed-array-names": { "possible-typed-array-names": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@@ -14235,6 +14554,11 @@
"fast-diff": "^1.1.2" "fast-diff": "^1.1.2"
} }
}, },
"process-warning": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
"integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="
},
"progress": { "progress": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz",
@@ -14274,6 +14598,11 @@
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
"integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==" "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="
}, },
"quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="
},
"raf-schd": { "raf-schd": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
@@ -14393,6 +14722,11 @@
"picomatch": "^2.2.1" "picomatch": "^2.2.1"
} }
}, },
"real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
"integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="
},
"redux": { "redux": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
@@ -14608,6 +14942,11 @@
"is-regex": "^1.2.1" "is-regex": "^1.2.1"
} }
}, },
"safe-stable-stringify": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
"integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="
},
"scheduler": { "scheduler": {
"version": "0.23.2", "version": "0.23.2",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
@@ -14627,6 +14966,11 @@
"ajv-keywords": "^3.5.2" "ajv-keywords": "^3.5.2"
} }
}, },
"secure-json-parse": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz",
"integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="
},
"semver": { "semver": {
"version": "7.7.0", "version": "7.7.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz",
@@ -14818,6 +15162,14 @@
} }
} }
}, },
"sonic-boom": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
"integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
"requires": {
"atomic-sleep": "^1.0.0"
}
},
"source-map": { "source-map": {
"version": "0.5.7", "version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
@@ -14846,6 +15198,11 @@
} }
} }
}, },
"split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="
},
"stacktrace-parser": { "stacktrace-parser": {
"version": "0.1.10", "version": "0.1.10",
"resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz",
@@ -15177,6 +15534,14 @@
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
"integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="
}, },
"thread-stream": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
"integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"requires": {
"real-require": "^0.2.0"
}
},
"three": { "three": {
"version": "0.154.0", "version": "0.154.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz", "resolved": "https://registry.npmjs.org/three/-/three-0.154.0.tgz",

View File

@@ -18,6 +18,7 @@
"@mui/material": "^5.13.5", "@mui/material": "^5.13.5",
"@sentry/nextjs": "^8.2.1", "@sentry/nextjs": "^8.2.1",
"@types/node": "20.3.1", "@types/node": "20.3.1",
"@types/pino": "^7.0.4",
"@types/react": "18.2.12", "@types/react": "18.2.12",
"@types/react-dom": "18.2.5", "@types/react-dom": "18.2.5",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
@@ -25,8 +26,10 @@
"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-pretty": "^13.0.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-chartjs-2": "^5.3.0", "react-chartjs-2": "^5.3.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",

View File

@@ -1,5 +1,5 @@
import { AccessToken } from "@/types"; import { AccessToken, PlanetWithInfo, Pin } from "@/types";
import { Box, Stack, Typography, useTheme, Paper, IconButton } from "@mui/material"; import { Box, Stack, Typography, useTheme, Paper, IconButton, Divider } from "@mui/material";
import { CharacterRow } from "../Characters/CharacterRow"; import { CharacterRow } from "../Characters/CharacterRow";
import { PlanetaryInteractionRow } from "../PlanetaryInteraction/PlanetaryInteractionRow"; import { PlanetaryInteractionRow } from "../PlanetaryInteraction/PlanetaryInteractionRow";
import { SessionContext } from "@/app/context/Context"; import { SessionContext } from "@/app/context/Context";
@@ -9,22 +9,195 @@ import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { planetCalculations } from "@/planets"; import { planetCalculations } from "@/planets";
import { EvePraisalResult } from "@/eve-praisal"; import { EvePraisalResult } from "@/eve-praisal";
import { STORAGE_IDS } from "@/const"; import { STORAGE_IDS, PI_SCHEMATICS, PI_PRODUCT_VOLUMES, STORAGE_CAPACITIES } from "@/const";
import { DateTime } from "luxon";
import { PlanetCalculations, AlertState, StorageContent, StorageInfo } from "@/types/planet";
import { getProgramOutputPrediction } from "../PlanetaryInteraction/ExtractionSimulation";
interface AccountTotals { interface AccountTotals {
monthlyEstimate: number; monthlyEstimate: number;
storageValue: number; storageValue: number;
planetCount: number;
characterCount: number;
runningExtractors: number;
totalExtractors: number;
} }
const calculateAlertState = (planetDetails: PlanetCalculations, minExtractionRate: number): AlertState => {
const hasLowStorage = planetDetails.storageInfo.some(storage => storage.fillRate > 60);
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 {
expired: planetDetails.expired,
hasLowStorage,
hasLowImports,
hasLargeExtractorDifference: planetDetails.hasLargeExtractorDifference,
hasLowExtractionRate
};
};
const calculatePlanetDetails = (planet: PlanetWithInfo, piPrices: EvePraisalResult | undefined, balanceThreshold: number): PlanetCalculations => {
const { expired, extractors, localProduction: rawProduction, localImports, localExports: rawExports } = planetCalculations(planet);
// Convert localProduction to include factoryCount
const localProduction = new Map(Array.from(rawProduction).map(([key, value]) => [
key,
{
...value,
factoryCount: value.count || 1
}
]));
// Calculate extractor averages and check for large differences
const CYCLE_TIME = 30 * 60; // 30 minutes in seconds
const extractorAverages = extractors
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
.map(e => {
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 prediction = getProgramOutputPrediction(qtyPerCycle, CYCLE_TIME, cycles);
const totalOutput = prediction.reduce((sum, val) => sum + val, 0);
const averagePerHour = totalOutput / cycles * 2;
return {
typeId: e.extractor_details!.product_type_id!,
averagePerHour
};
});
const hasLargeExtractorDifference = extractorAverages.length === 2 &&
Math.abs(extractorAverages[0].averagePerHour - extractorAverages[1].averagePerHour) > balanceThreshold;
// Calculate storage info
const storageFacilities = planet.info.pins.filter((pin: Pin) =>
STORAGE_IDS().some(storage => storage.type_id === pin.type_id)
);
const storageInfo = storageFacilities.map((storage: Pin) => {
if (!storage || !storage.contents) return null;
const storageType = STORAGE_IDS().find(s => s.type_id === storage.type_id)?.name || 'Unknown';
const storageCapacity = STORAGE_CAPACITIES[storage.type_id] || 0;
const totalVolume = (storage.contents || [])
.reduce((sum: number, item: StorageContent) => {
const volume = PI_PRODUCT_VOLUMES[item.type_id] || 0;
return sum + (item.amount * volume);
}, 0);
const totalValue = (storage.contents || [])
.reduce((sum: number, item: StorageContent) => {
const price = piPrices?.appraisal.items.find((a) => a.typeID === item.type_id)?.prices.sell.min ?? 0;
return sum + (item.amount * price);
}, 0);
const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0;
return {
type: storageType,
type_id: storage.type_id,
capacity: storageCapacity,
used: totalVolume,
fillRate: fillRate,
value: totalValue
};
}).filter(Boolean) as StorageInfo[];
// Calculate import depletion times
const importDepletionTimes = localImports.map(i => {
// Find all storage facilities containing this import
const storagesWithImport = storageFacilities.filter((storage: Pin) =>
storage.contents?.some((content: StorageContent) => content.type_id === i.type_id)
);
// Get the total amount in all storage facilities
const totalAmount = storagesWithImport.reduce((sum: number, storage: Pin) => {
const content = storage.contents?.find((content: StorageContent) => content.type_id === i.type_id);
return sum + (content?.amount ?? 0);
}, 0);
// Calculate consumption rate per hour
const schematic = PI_SCHEMATICS.find(s => s.schematic_id === i.schematic_id);
const cycleTime = schematic?.cycle_time ?? 3600;
const consumptionPerHour = i.quantity * i.factoryCount * (3600 / cycleTime);
// Calculate time until depletion in hours, starting from last_update
const lastUpdate = DateTime.fromISO(planet.last_update);
const now = DateTime.now();
const hoursSinceUpdate = now.diff(lastUpdate, 'hours').hours;
const remainingAmount = Math.max(0, totalAmount - (consumptionPerHour * hoursSinceUpdate));
const hoursUntilDepletion = consumptionPerHour > 0 ? remainingAmount / consumptionPerHour : 0;
// Calculate monthly cost
const price = piPrices?.appraisal.items.find((a) => a.typeID === i.type_id)?.prices.sell.min ?? 0;
const monthlyCost = (consumptionPerHour * 24 * 30 * price) / 1000000; // Cost in millions
return {
typeId: i.type_id,
hoursUntilDepletion,
monthlyCost
};
});
// Convert localExports to match the LocalExport interface
const localExports = rawExports.map(e => {
const schematic = PI_SCHEMATICS.flatMap(s => s.outputs)
.find(s => s.type_id === e.typeId)?.schematic_id ?? 0;
const factoryCount = planet.info.pins
.filter(p => p.schematic_id === schematic)
.length;
return {
type_id: e.typeId,
schematic_id: schematic,
quantity: e.amount / factoryCount, // Convert total amount back to per-factory quantity
factoryCount
};
});
return {
expired,
extractors,
localProduction,
localImports,
localExports,
storageInfo,
extractorAverages,
hasLargeExtractorDifference,
importDepletionTimes,
visibility: 'visible' as const
};
};
const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalResult | undefined): AccountTotals => { const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalResult | undefined): AccountTotals => {
let totalMonthlyEstimate = 0; let totalMonthlyEstimate = 0;
let totalStorageValue = 0; let totalStorageValue = 0;
let totalPlanetCount = 0;
let totalCharacterCount = characters.length;
let runningExtractors = 0;
let totalExtractors = 0;
characters.forEach((character) => { characters.forEach((character) => {
totalPlanetCount += character.planets.length;
character.planets.forEach((planet) => { character.planets.forEach((planet) => {
const { localExports } = planetCalculations(planet); const { localExports, extractors } = planetCalculations(planet);
const planetConfig = character.planetConfig.find(p => p.planetId === planet.planet_id); const planetConfig = character.planetConfig.find(p => p.planetId === planet.planet_id);
// Count running and total extractors
if (!planetConfig?.excludeFromTotals) {
extractors.forEach(extractor => {
totalExtractors++;
if (extractor.expiry_time && new Date(extractor.expiry_time) > new Date()) {
runningExtractors++;
}
});
}
// Calculate monthly estimate // Calculate monthly estimate
if (!planetConfig?.excludeFromTotals) { if (!planetConfig?.excludeFromTotals) {
localExports.forEach((exportItem) => { localExports.forEach((exportItem) => {
@@ -56,21 +229,62 @@ const calculateAccountTotals = (characters: AccessToken[], piPrices: EvePraisalR
return { return {
monthlyEstimate: totalMonthlyEstimate, monthlyEstimate: totalMonthlyEstimate,
storageValue: totalStorageValue storageValue: totalStorageValue,
planetCount: totalPlanetCount,
characterCount: totalCharacterCount,
runningExtractors,
totalExtractors
}; };
}; };
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 } = useContext(SessionContext); const { planMode, piPrices, alertMode, balanceThreshold, minExtractionRate } = useContext(SessionContext);
const { monthlyEstimate, storageValue } = calculateAccountTotals(characters, piPrices); const { monthlyEstimate, storageValue, planetCount, characterCount, runningExtractors, totalExtractors } = calculateAccountTotals(characters, piPrices);
// Calculate planet details and alert states for each planet
const planetDetails = characters.reduce((acc, character) => {
character.planets.forEach(planet => {
const details = calculatePlanetDetails(planet, piPrices, balanceThreshold);
acc[`${character.character.characterId}-${planet.planet_id}`] = {
...details,
alertState: calculateAlertState(details, minExtractionRate)
};
});
return acc;
}, {} as Record<string, PlanetCalculations & { alertState: AlertState }>);
// Update local collapse state when prop changes // Update local collapse state when prop changes
useEffect(() => { useEffect(() => {
setLocalIsCollapsed(propIsCollapsed ?? false); setLocalIsCollapsed(propIsCollapsed ?? false);
}, [propIsCollapsed]); }, [propIsCollapsed]);
const getAlertVisibility = (alertState: AlertState) => {
if (!alertMode) return 'visible';
if (alertState.expired) return 'visible';
if (alertState.hasLowStorage) return 'visible';
if (alertState.hasLowImports) return 'visible';
if (alertState.hasLargeExtractorDifference) return 'visible';
if (alertState.hasLowExtractionRate) return 'visible';
return 'hidden';
};
// Check if any planet in the account has alerts
const hasAnyAlerts = Object.values(planetDetails).some(details => {
const alertState = calculateAlertState(details, minExtractionRate);
return alertState.expired ||
alertState.hasLowStorage ||
alertState.hasLowImports ||
alertState.hasLargeExtractorDifference ||
alertState.hasLowExtractionRate;
});
// If in alert mode and no alerts, hide the entire card
if (alertMode && !hasAnyAlerts) {
return null;
}
return ( return (
<Paper <Paper
elevation={2} elevation={2}
@@ -102,7 +316,12 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
cursor: 'pointer',
'&:hover': {
backgroundColor: theme.palette.action.hover,
},
}} }}
onClick={() => setLocalIsCollapsed(!localIsCollapsed)}
> >
<Box> <Box>
<Typography <Typography
@@ -116,30 +335,83 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
? `Account: ${characters[0].account}` ? `Account: ${characters[0].account}`
: "No account name"} : "No account name"}
</Typography> </Typography>
<Typography <Box sx={{
sx={{ display: 'flex',
fontSize: "0.8rem", gap: 2,
color: theme.palette.text.secondary, flexWrap: 'wrap',
}} mt: 1,
> alignItems: 'center'
Monthly Estimate: {monthlyEstimate >= 1000 }}>
? `${(monthlyEstimate / 1000).toFixed(2)} B` <Typography
: `${monthlyEstimate.toFixed(2)} M`} ISK sx={{
</Typography> fontSize: "0.8rem",
<Typography color: theme.palette.text.secondary,
sx={{ }}
fontSize: "0.8rem", >
color: theme.palette.text.secondary, Monthly: {monthlyEstimate >= 1000
}} ? `${(monthlyEstimate / 1000).toFixed(2)} B`
> : `${monthlyEstimate.toFixed(2)} M`} ISK
Storage Value: {storageValue >= 1000 </Typography>
? `${(storageValue / 1000).toFixed(2)} B` <Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
: `${storageValue.toFixed(2)} M`} ISK <Typography
</Typography> sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Storage: {storageValue >= 1000
? `${(storageValue / 1000).toFixed(2)} B`
: `${storageValue.toFixed(2)} M`} ISK
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Planets: {planetCount}
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: theme.palette.text.secondary,
}}
>
Characters: {characterCount}
</Typography>
<Divider orientation="vertical" flexItem sx={{ height: 16, borderColor: theme.palette.divider }} />
<Typography
sx={{
fontSize: "0.8rem",
color: runningExtractors < totalExtractors ? theme.palette.error.main : theme.palette.text.secondary,
}}
>
Extractors: {runningExtractors}/{totalExtractors}
</Typography>
<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>
<IconButton <IconButton
size="small" size="small"
onClick={() => setLocalIsCollapsed(!localIsCollapsed)}
sx={{ sx={{
transform: localIsCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)', transform: localIsCollapsed ? 'rotate(-90deg)' : 'rotate(0deg)',
transition: 'transform 0.2s ease-in-out' transition: 'transform 0.2s ease-in-out'
@@ -158,7 +430,17 @@ export const AccountCard = ({ characters, isCollapsed: propIsCollapsed }: { char
{planMode ? ( {planMode ? (
<PlanRow character={c} /> <PlanRow character={c} />
) : ( ) : (
<PlanetaryInteractionRow 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> </Stack>
))} ))}

View File

@@ -1,21 +0,0 @@
import { SessionContext } from "@/app/context/Context";
import { Button, Tooltip } from "@mui/material";
import { useContext } from "react";
export const AlertModeButton = () => {
const { alertMode, toggleAlertMode } = useContext(SessionContext);
return (
<Tooltip title="Toggle alert mode to show only accounts and planets that need action.">
<Button
style={{
backgroundColor: alertMode
? "rgba(144, 202, 249, 0.08)"
: "inherit",
}}
onClick={toggleAlertMode}
>
Alert mode
</Button>
</Tooltip>
);
};

View File

@@ -12,20 +12,27 @@ import * as React from "react";
import { DowloadButton } from "../Backup/DowloadButton"; import { DowloadButton } from "../Backup/DowloadButton";
import { UploadButton } from "../Backup/UploadButton"; import { UploadButton } from "../Backup/UploadButton";
import { CCPButton } from "../CCP/CCPButton"; import { CCPButton } from "../CCP/CCPButton";
import { CompactModeButton } from "../CompactModeButton/CompactModeButton";
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 { PlanModeButton } from "../PlanModeButton/PlanModeButton"; import { PartnerCodeButton } from "../PartnerCode/PartnerCodeButton";
import { SettingsButton } from "../Settings/SettingsButtons"; import { SettingsButton } from "../Settings/SettingsButtons";
import { AlertModeButton } from "../AlertModeButton/AlertModeButton"; import { BuyMeCoffeeButton } from "../BuyMeCoffee/BuyMeCoffeeButton";
import { SupportButton } from "../SupportButton/SupportButton"; import {
Button,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
} from "@mui/material";
import { useState } from "react";
function ResponsiveAppBar() { function ResponsiveAppBar() {
const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>( const [anchorElNav, setAnchorElNav] = React.useState<null | HTMLElement>(
null, null,
); );
const [faqOpen, setFaqOpen] = useState(false);
const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => { const handleOpenNavMenu = (event: React.MouseEvent<HTMLElement>) => {
setAnchorElNav(event.currentTarget); setAnchorElNav(event.currentTarget);
@@ -102,23 +109,32 @@ function ResponsiveAppBar() {
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<GitHubButton /> <GitHubButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<SettingsButton />
</MenuItem>
<MenuItem
onClick={() => {
handleCloseNavMenu();
setFaqOpen(true);
}}
>
<Button
href=""
style={{ width: "100%" }}
sx={{ color: "white", display: "flex", justifyContent: "flex-start" }}
>
FAQ
</Button>
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<CCPButton /> <CCPButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<SupportButton /> <PartnerCodeButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<SettingsButton /> <BuyMeCoffeeButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<CompactModeButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<PlanModeButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<AlertModeButton />
</MenuItem> </MenuItem>
</Menu> </Menu>
</Box> </Box>
@@ -146,7 +162,7 @@ function ResponsiveAppBar() {
flexGrow: 1, flexGrow: 1,
display: { xs: "none", md: "flex" }, display: { xs: "none", md: "flex" },
alignItems: "center", alignItems: "center",
gap: "0.2rem" gap: "0.2rem",
}} }}
> >
<LoginButton /> <LoginButton />
@@ -154,15 +170,134 @@ function ResponsiveAppBar() {
<UploadButton /> <UploadButton />
<DiscordButton /> <DiscordButton />
<GitHubButton /> <GitHubButton />
<CCPButton />
<SupportButton />
<SettingsButton /> <SettingsButton />
<CompactModeButton /> <Button onClick={() => setFaqOpen(true)} color="inherit">
<PlanModeButton /> FAQ
<AlertModeButton /> </Button>
<CCPButton />
<PartnerCodeButton />
<BuyMeCoffeeButton />
</Box> </Box>
</Toolbar> </Toolbar>
</Container> </Container>
<Dialog
open={faqOpen}
onClose={() => setFaqOpen(false)}
aria-labelledby="faq-dialog-title"
>
<DialogTitle id="faq-dialog-title">
Frequently Asked Questions
</DialogTitle>
<DialogContent>
<DialogContentText>
<strong>EVE Online Partner Code</strong>
<br />
Consider using my partner code for CCP related purchases to support
this project:
<Button
href=""
style={{ width: "100%" }}
sx={{ color: "white", display: "block" }}
onClick={() => {
navigator.clipboard.writeText("CALLIEVE");
}}
>
CALLIEVE
</Button>{" "}
Click button above to copy to clipboard.
<br />
<br />
<strong>What is this application?</strong>
<br />
This EVE Online Planetary Interaction tool that helps you track and
manage your colonies and production chains. Main usecase is to see
if your extractors are running.
<br />
<br />
<strong>How do I add characters?</strong>
<br />
You can add characters by clicking the &quot;Add Character&quot;
button in the app bar and following the authentication process.
<br />
<br />
<strong>Why don&apos;t my storage numbers add up?</strong>
<br />
EVE Online API (ESI) provides planetary interaction endpoints. These
are updated when you submit changes to planet. For example after an
extractor restart.
<br />
<br />
<strong>What does exclude mean?</strong>
<br />
Exclude means that this planet will not be counted in totals. This
is useful if you have longer production chains where exports of a
planet are consumed by another planet.
<br />
<br />
<strong>How do see my planets production chains?</strong>
<br />
Click on the planet row to open the extraction simulation.
<br />
<br />
<strong>What is Compact Mode?</strong>
<br />
Compact Mode reduces the size of character cards to show more
information at once. You can toggle it in the settings.
<br />
<br />
<strong>What is Alert Mode?</strong>
<br />
Alert mode shows only the planets that have extractors offline and
need attention.
<br />
<br />
<strong>What does off-blanace mean?</strong>
<br />
Off-blanace alert shows up for planets that have two extractors that
are extracting different amount of material. Treshold can be set in
settings. Generally you want to keep this below 1000.
<br />
<br />
<strong>How do I reorder accounts?</strong>
<br />
You can drag and drop account cards to reorder them. The order will
be saved automatically.
<br />
<br />
<strong>How do I add a character to account groups?</strong>
<br />
You can add a character to an account group by clicking the cog on
the character card and typing in the account/group name.
<br />
<br />
<strong>I like icons! Why so much text?</strong>
<br />
Toggle this option in settings.
<br />
<br />
<strong>I like colors! Why your alert colors are so ugly?</strong>
<br />
Change the color scheme in settings.
<br />
<br />
<strong>What does the 3D view do?</strong>
<br />
Nothing. Just made it for fun
<br />
<br />
<strong>Why are planet settings empty?</strong>
<br />
Its a work in progress feature. Plan is to be able to configuer
planet and planet exports to belong to production chains.
<br />
<br />
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setFaqOpen(false)}>Close</Button>
</DialogActions>
</Dialog>
</AppBar> </AppBar>
); );
} }

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

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

View File

@@ -1,21 +0,0 @@
import { SessionContext } from "@/app/context/Context";
import { Button, Tooltip } from "@mui/material";
import { useContext } from "react";
export const CompactModeButton = () => {
const { compactMode, toggleCompactMode } = useContext(SessionContext);
return (
<Tooltip title="Toggle compact layout for widescreen">
<Button
style={{
backgroundColor: compactMode
? "rgba(144, 202, 249, 0.08)"
: "inherit",
}}
onClick={toggleCompactMode}
>
Compact mode
</Button>
</Tooltip>
);
};

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

@@ -6,15 +6,21 @@ import {
ThemeProvider, ThemeProvider,
createTheme, createTheme,
Button, Button,
Tooltip,
} from "@mui/material"; } from "@mui/material";
import { AccountCard } from "./Account/AccountCard"; import { AccountCard } from "./Account/AccountCard";
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { CharacterContext, SessionContext } from "../context/Context"; import { CharacterContext, SessionContext } from "../context/Context";
import ResponsiveAppBar from "./AppBar/AppBar"; import ResponsiveAppBar from "./AppBar/AppBar";
import { Summary } from "./Summary/Summary"; import { Summary } from "./Summary/Summary";
import { DragDropContext, Droppable, Draggable, DropResult } from "@hello-pangea/dnd"; import {
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown'; DragDropContext,
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp'; Droppable,
Draggable,
DropResult,
} from "@hello-pangea/dnd";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
interface Grouped { interface Grouped {
[key: string]: AccessToken[]; [key: string]: AccessToken[];
@@ -42,7 +48,8 @@ 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 [accountOrder, setAccountOrder] = useState<string[]>([]); const [accountOrder, setAccountOrder] = useState<string[]>([]);
const [allCollapsed, setAllCollapsed] = useState(false); const [allCollapsed, setAllCollapsed] = useState(false);
@@ -54,15 +61,19 @@ export const MainGrid = () => {
group[account ?? ""] = group[account ?? ""] ?? []; group[account ?? ""] = group[account ?? ""] ?? [];
group[account ?? ""].push(character); group[account ?? ""].push(character);
return group; return group;
}, {}) }, {}),
); );
const savedOrder = localStorage.getItem('accountOrder'); const savedOrder = localStorage.getItem("accountOrder");
if (savedOrder) { if (savedOrder) {
try { try {
const parsedOrder = JSON.parse(savedOrder); const parsedOrder = JSON.parse(savedOrder);
const validOrder = parsedOrder.filter((account: string) => currentAccounts.includes(account)); const validOrder = parsedOrder.filter((account: string) =>
const newAccounts = currentAccounts.filter(account => !validOrder.includes(account)); currentAccounts.includes(account),
);
const newAccounts = currentAccounts.filter(
(account) => !validOrder.includes(account),
);
setAccountOrder([...validOrder, ...newAccounts]); setAccountOrder([...validOrder, ...newAccounts]);
} catch (e) { } catch (e) {
setAccountOrder(currentAccounts); setAccountOrder(currentAccounts);
@@ -74,7 +85,7 @@ export const MainGrid = () => {
useEffect(() => { useEffect(() => {
if (accountOrder.length > 0) { if (accountOrder.length > 0) {
localStorage.setItem('accountOrder', JSON.stringify(accountOrder)); localStorage.setItem("accountOrder", JSON.stringify(accountOrder));
} }
}, [accountOrder]); }, [accountOrder]);
@@ -85,7 +96,6 @@ export const MainGrid = () => {
return group; return group;
}, {}); }, {});
const { compactMode } = useContext(SessionContext);
const [darkTheme, setDarkTheme] = useState( const [darkTheme, setDarkTheme] = useState(
createTheme({ createTheme({
palette: { palette: {
@@ -138,14 +148,75 @@ export const MainGrid = () => {
<Box sx={{ flexGrow: 1 }}> <Box sx={{ flexGrow: 1 }}>
<ResponsiveAppBar /> <ResponsiveAppBar />
{compactMode ? <></> : <Summary characters={characters} />} {compactMode ? <></> : <Summary characters={characters} />}
<Box sx={{ display: 'flex', justifyContent: 'flex-start', padding: 1 }}> <Box
sx={{
display: "flex",
justifyContent: "flex-start",
padding: 1,
gap: 1,
}}
>
<Button <Button
startIcon={allCollapsed ? <KeyboardArrowDownIcon /> : <KeyboardArrowUpIcon />} startIcon={
allCollapsed ? <KeyboardArrowDownIcon /> : <KeyboardArrowUpIcon />
}
onClick={() => setAllCollapsed(!allCollapsed)} onClick={() => setAllCollapsed(!allCollapsed)}
size="small" size="small"
> >
{allCollapsed ? 'Expand All' : 'Collapse All'} {allCollapsed ? "Expand All" : "Collapse All"}
</Button> </Button>
<Tooltip title="Toggle compact layout for widescreen">
<Button
size="small"
style={{
backgroundColor: compactMode
? "rgba(144, 202, 249, 0.16)"
: "inherit",
}}
onClick={toggleCompactMode}
>
Compact mode
</Button>
</Tooltip>
<Tooltip title="Toggle alert mode to show only accounts and planets that need action.">
<Button
size="small"
style={{
backgroundColor: alertMode
? "rgba(144, 202, 249, 0.16)"
: "inherit",
}}
onClick={toggleAlertMode}
>
Alert mode
</Button>
</Tooltip>
<Tooltip title="Toggle plan mode that show layout for widescreen">
<Button
size="small"
style={{
backgroundColor: planMode
? "rgba(144, 202, 249, 0.16)"
: "inherit",
}}
onClick={togglePlanMode}
>
Plan mode
</Button>
</Tooltip>
<Tooltip title="Toggle extraction time display mode">
<Button
size="small"
style={{
backgroundColor: extractionTimeMode
? "rgba(144, 202, 249, 0.16)"
: "inherit",
}}
onClick={toggleExtractionTimeMode}
>
Extraction datetime
</Button>
</Tooltip>
</Box> </Box>
<DragDropContextComponent onDragEnd={handleDragEnd}> <DragDropContextComponent onDragEnd={handleDragEnd}>
<DroppableComponent droppableId="accounts"> <DroppableComponent droppableId="accounts">
@@ -153,7 +224,7 @@ export const MainGrid = () => {
<Grid <Grid
container container
spacing={1} spacing={1}
sx={{ padding: 1 }} sx={{ padding: 1, width: "100%" }}
{...provided.droppableProps} {...provided.droppableProps}
ref={provided.innerRef} ref={provided.innerRef}
> >
@@ -172,17 +243,18 @@ export const MainGrid = () => {
{...provided.draggableProps} {...provided.draggableProps}
{...provided.dragHandleProps} {...provided.dragHandleProps}
sx={{ sx={{
'& > *': { "& > *": {
width: '100%', width: "100%",
}, },
}} }}
> >
{groupByAccount[account] && groupByAccount[account].length > 0 && ( {groupByAccount[account] &&
<AccountCard groupByAccount[account].length > 0 && (
characters={groupByAccount[account]} <AccountCard
isCollapsed={allCollapsed} characters={groupByAccount[account]}
/> isCollapsed={allCollapsed}
)} />
)}
</Grid> </Grid>
)} )}
</DraggableComponent> </DraggableComponent>

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,15 +0,0 @@
import { SessionContext } from "@/app/context/Context";
import { Button, Tooltip } from "@mui/material";
import { useContext } from "react";
export const PlanModeButton = () => {
const { planMode, togglePlanMode } = useContext(SessionContext);
return (
<Tooltip title="Toggle plan mode that show layout for widescreen">
<Button onClick={togglePlanMode} style={{backgroundColor: planMode ? 'rgba(144, 202, 249, 0.08)' : 'inherit'}}>
Plan mode
</Button>
</Tooltip>
);
};

View File

@@ -13,7 +13,7 @@ import {
} from "@mui/material"; } from "@mui/material";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import Countdown from "react-countdown"; import Countdown from "react-countdown";
import { timeColor } from "../PlanetaryInteraction/timeColors"; import { timeColor } from "../PlanetaryInteraction/alerts";
import Image from "next/image"; import Image from "next/image";
import { ColorContext, SessionContext } from "@/app/context/Context"; import { ColorContext, SessionContext } from "@/app/context/Context";
import { useContext } from "react"; import { useContext } from "react";

View File

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

View File

@@ -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,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',
@@ -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

@@ -4,18 +4,21 @@ import {
AccessToken, AccessToken,
PlanetWithInfo, PlanetWithInfo,
} from "@/types"; } from "@/types";
import { PlanetCalculations } from "@/types/planet";
import React, { useContext } from "react"; import React, { useContext } from "react";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { EXTRACTOR_TYPE_IDS } from "@/const";
import Countdown from "react-countdown"; import Countdown from "react-countdown";
import { getProgramOutputPrediction } from "./ExtractionSimulation";
import {
alertModeVisibility,
extractorsHaveExpired,
timeColor,
} from "./timeColors";
import { ColorContext, SessionContext } from "@/app/context/Context"; import { ColorContext, SessionContext } from "@/app/context/Context";
import { ExtractionSimulationTooltip } from "./ExtractionSimulationTooltip"; import { ExtractionSimulationTooltip } from "./ExtractionSimulationTooltip";
import { timeColor } from "./alerts";
interface ExtractorConfig {
typeId: number;
baseValue: number;
cycleTime: number;
installTime: string;
expiryTime: string;
}
const StackItem = styled(Stack)(({ theme }) => ({ const StackItem = styled(Stack)(({ theme }) => ({
...theme.typography.body2, ...theme.typography.body2,
@@ -29,82 +32,36 @@ const StackItem = styled(Stack)(({ theme }) => ({
export const PlanetCard = ({ export const PlanetCard = ({
character, character,
planet, planet,
planetDetails,
}: { }: {
character: AccessToken; character: AccessToken;
planet: PlanetWithInfo; planet: PlanetWithInfo;
planetDetails: PlanetCalculations;
}) => { }) => {
const { alertMode } = useContext(SessionContext);
const planetInfo = planet.info;
const planetInfoUniverse = planet.infoUniverse;
const theme = useTheme(); const theme = useTheme();
const extractorsExpiryTime =
(planetInfo &&
planetInfo.pins
.filter((p) => EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id))
.map((p) => p.expiry_time)) ??
[];
const { colors } = useContext(ColorContext); const { colors } = useContext(ColorContext);
const expired = extractorsHaveExpired(extractorsExpiryTime); const { minExtractionRate } = useContext(SessionContext);
const CYCLE_TIME = 30 * 60; // 30 minutes in seconds const extractorConfigs: ExtractorConfig[] = planetDetails.extractors
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
.map(e => ({
typeId: e.extractor_details!.product_type_id!,
baseValue: e.extractor_details!.qty_per_cycle!,
cycleTime: e.extractor_details?.cycle_time || 3600,
installTime: e.install_time ?? "",
expiryTime: e.expiry_time ?? ""
}));
const extractors = planetInfo?.pins const hasLowExtractionRate = planetDetails.extractorAverages.length > 0 && minExtractionRate > 0 && planetDetails.extractorAverages.some(avg => avg.averagePerHour < minExtractionRate);
.filter((p) => EXTRACTOR_TYPE_IDS.some((e) => e === p.type_id))
.map((p) => ({
typeId: p.type_id,
baseValue: p.extractor_details?.qty_per_cycle || 0,
cycleTime: p.extractor_details?.cycle_time || 3600,
installTime: p.install_time || "",
expiryTime: p.expiry_time || "",
installedSchematicId: p.extractor_details?.product_type_id || undefined
})) || [];
// Calculate program duration and cycles for each extractor
const extractorPrograms = extractors.map(extractor => {
const installDate = new Date(extractor.installTime);
const expiryDate = new Date(extractor.expiryTime);
const programDuration = (expiryDate.getTime() - installDate.getTime()) / 1000; // Convert to seconds
return {
...extractor,
programDuration,
cycles: Math.floor(programDuration / CYCLE_TIME)
};
});
// Get output predictions for each extractor
const extractorOutputs = extractorPrograms.map(extractor => ({
typeId: extractor.typeId,
cycleTime: CYCLE_TIME,
cycles: extractor.cycles,
prediction: getProgramOutputPrediction(
extractor.baseValue,
CYCLE_TIME,
extractor.cycles
)
}));
// Calculate average per hour for each extractor
const extractorAverages = extractorOutputs.map(extractor => {
const totalOutput = extractor.prediction.reduce((sum, val) => sum + val, 0);
const programDuration = extractor.cycles * CYCLE_TIME;
const averagePerHour = (totalOutput / programDuration) * 3600;
return {
typeId: extractor.typeId,
averagePerHour
};
});
return ( return (
<Tooltip <Tooltip
title={ title={
<ExtractionSimulationTooltip planetDetails.extractors.length > 0 ? (
extractors={extractors} <ExtractionSimulationTooltip
/> extractors={extractorConfigs}
/>
) : null
} }
componentsProps={{ componentsProps={{
tooltip: { tooltip: {
@@ -119,14 +76,13 @@ export const PlanetCard = ({
} }
}} }}
> >
<StackItem <StackItem
alignItems="flex-start" alignItems="flex-start"
height="100%" height="100%"
position="relative" position="relative"
minHeight={theme.custom.cardMinHeight} minHeight={theme.custom.cardMinHeight}
visibility={alertModeVisibility(alertMode, expired)} style={{ visibility: planetDetails.visibility }}
> >
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<Image <Image
unoptimized unoptimized
@@ -151,55 +107,74 @@ export const PlanetCard = ({
borderRadius: 8, borderRadius: 8,
}} /> }} />
</div> </div>
{planetDetails.expired && (
{expired && ( <Image
<Image width={32}
width={32} height={32}
height={32} src={`/stopped.png`}
src={`/stopped.png`} alt=""
alt="" style={{ position: "absolute", top: theme.custom.stoppedPosition }}
style={{ position: "absolute", top: theme.custom.stoppedPosition }} />
/> )}
)} <div style={{ position: "absolute", top: 5, left: 10 }}>
<div style={{ position: "absolute", top: 5, left: 10 }}> <Typography
<Typography fontSize={theme.custom.smallText}> fontSize={theme.custom.smallText}
{planetInfoUniverse?.name} color={(planetDetails.hasLargeExtractorDifference || hasLowExtractionRate) ? 'error' : 'inherit'}
</Typography> >
{extractorsExpiryTime.map((e, idx) => { {planet.infoUniverse?.name}
const extractor = extractors[idx]; </Typography>
const average = extractorAverages[idx]; {planetDetails.hasLargeExtractorDifference && (
return ( <Typography
<div key={`${e}-${idx}-${character.character.characterId}`}> fontSize={theme.custom.smallText}
<Typography color="error"
color={timeColor(e, colors)} sx={{ opacity: 0.7 }}
fontSize={theme.custom.smallText} >
> off-balance
{!expired && e && <Countdown </Typography>
overtime={true} )}
date={DateTime.fromISO(e).toMillis()} {hasLowExtractionRate && (
/> <Typography
} fontSize={theme.custom.smallText}
</Typography> color="error"
{!expired && extractor && average && ( sx={{ opacity: 0.7 }}
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}> >
<Image low-extraction
unoptimized </Typography>
src={`https://images.evetech.net/types/${extractor.installedSchematicId}/icon?size=32`} )}
alt="" {planetDetails.extractors.map((e, idx) => {
width={16} const average = planetDetails.extractorAverages[idx];
height={16} return (
style={{ borderRadius: 4 }} <div key={`${e}-${idx}-${character.character.characterId}`}>
/> <Typography
<Typography fontSize={theme.custom.smallText}> color={timeColor(e.expiry_time, colors)}
{average.averagePerHour.toFixed(1)}/h fontSize={theme.custom.smallText}
</Typography> >
</div> {!planetDetails.expired && e.expiry_time && <Countdown
)} overtime={true}
</div> date={DateTime.fromISO(e.expiry_time).toMillis()}
); />
})} }
</div> </Typography>
</StackItem> {!planetDetails.expired && e && average && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<Image
unoptimized
src={`https://images.evetech.net/types/${e.extractor_details?.product_type_id}/icon?size=32`}
alt=""
width={16}
height={16}
style={{ borderRadius: 4 }}
/>
<Typography fontSize={theme.custom.smallText}>
{average.averagePerHour.toFixed(1)}/h
</Typography>
</div>
)}
</div>
);
})}
</div>
</StackItem>
</Tooltip> </Tooltip>
); );
}; };

View File

@@ -1,7 +1,7 @@
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 } 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 CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
import MoreVertIcon from '@mui/icons-material/MoreVert'; import MoreVertIcon from '@mui/icons-material/MoreVert';
import { Button, Tooltip, Typography, useTheme, Menu, MenuItem, IconButton, Checkbox, FormControlLabel } from "@mui/material"; import { Button, Tooltip, Typography, useTheme, Menu, MenuItem, IconButton, Checkbox, FormControlLabel } from "@mui/material";
@@ -18,11 +18,13 @@ import React, { forwardRef, useContext, useState } from "react";
import Countdown from "react-countdown"; import Countdown from "react-countdown";
import { PlanetConfigDialog } from "../PlanetConfig/PlanetConfigDialog"; import { PlanetConfigDialog } from "../PlanetConfig/PlanetConfigDialog";
import PinsCanvas3D from "./PinsCanvas3D"; import PinsCanvas3D from "./PinsCanvas3D";
import { alertModeVisibility, timeColor } from "./timeColors"; import { timeColor } from "./alerts";
import { ExtractionSimulationDisplay } from './ExtractionSimulationDisplay'; import { ExtractionSimulationDisplay } from './ExtractionSimulationDisplay';
import { ExtractionSimulationTooltip } from './ExtractionSimulationTooltip'; import { ExtractionSimulationTooltip } from './ExtractionSimulationTooltip';
import { ProductionNode } from './ExtractionSimulation';
import { Collapse, Box, Stack } from "@mui/material"; import { Collapse, Box, Stack } from "@mui/material";
import Table from "@mui/material/Table";
import TableHead from "@mui/material/TableHead";
import TableBody from "@mui/material/TableBody";
const Transition = forwardRef(function Transition( const Transition = forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
@@ -33,14 +35,28 @@ const Transition = forwardRef(function Transition(
return <Slide direction="up" ref={ref} {...props} />; return <Slide direction="up" ref={ref} {...props} />;
}); });
interface SchematicInput {
type_id: number;
quantity: number;
}
interface SchematicOutput {
type_id: number;
quantity: number;
}
export const PlanetTableRow = ({ export const PlanetTableRow = ({
planet, planet,
character, character,
planetDetails,
}: { }: {
planet: PlanetWithInfo; planet: PlanetWithInfo;
character: AccessToken; character: AccessToken;
planetDetails: PlanetCalculations;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { showProductIcons, extractionTimeMode, alertMode, minExtractionRate } = useContext(SessionContext);
const { colors } = useContext(ColorContext);
const [planetRenderOpen, setPlanetRenderOpen] = useState(false); const [planetRenderOpen, setPlanetRenderOpen] = useState(false);
const [planetConfigOpen, setPlanetConfigOpen] = useState(false); const [planetConfigOpen, setPlanetConfigOpen] = useState(false);
@@ -71,80 +87,13 @@ export const PlanetTableRow = ({
setPlanetConfigOpen(false); setPlanetConfigOpen(false);
}; };
const { piPrices, alertMode, updatePlanetConfig, readPlanetConfig, balanceThreshold } = useContext(SessionContext); const { piPrices, updatePlanetConfig, readPlanetConfig } = useContext(SessionContext);
const planetInfo = planet.info; const planetInfo = planet.info;
const planetInfoUniverse = planet.infoUniverse; const planetInfoUniverse = planet.infoUniverse;
const { expired, extractors, localProduction, localImports, localExports } =
planetCalculations(planet);
const planetConfig = readPlanetConfig({ const planetConfig = readPlanetConfig({
characterId: character.character.characterId, characterId: character.character.characterId,
planetId: planet.planet_id, planetId: planet.planet_id,
}); });
const { colors } = useContext(ColorContext);
// Convert local production to ProductionNode array for simulation
const productionNodes: ProductionNode[] = Array.from(localProduction).map(([schematicId, schematic]) => ({
schematicId: schematicId,
typeId: schematic.outputs[0].type_id,
name: schematic.name,
inputs: schematic.inputs.map(input => ({
typeId: input.type_id,
quantity: input.quantity
})),
outputs: schematic.outputs.map(output => ({
typeId: output.type_id,
quantity: output.quantity
})),
cycleTime: schematic.cycle_time,
factoryCount: schematic.count || 1
}));
// Calculate extractor averages and check for large differences
const extractorAverages = extractors
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
.map(e => {
const cycleTime = e.extractor_details?.cycle_time || 3600;
const qtyPerCycle = e.extractor_details?.qty_per_cycle || 0;
return {
typeId: e.extractor_details!.product_type_id!,
averagePerHour: (qtyPerCycle * 3600) / cycleTime
};
});
const hasLargeExtractorDifference = extractorAverages.length === 2 &&
Math.abs(extractorAverages[0].averagePerHour - extractorAverages[1].averagePerHour) > balanceThreshold;
const storageFacilities = planetInfo.pins.filter(pin =>
STORAGE_IDS().some(storage => storage.type_id === pin.type_id)
);
const getStorageInfo = (pin: any) => {
if (!pin || !pin.contents) return null;
const storageType = PI_TYPES_MAP[pin.type_id].name;
const storageCapacity = STORAGE_CAPACITIES[pin.type_id] || 0;
const totalVolume = (pin.contents || [])
.reduce((sum: number, item: any) => {
const volume = PI_PRODUCT_VOLUMES[item.type_id] || 0;
return sum + (item.amount * volume);
}, 0);
const totalValue = (pin.contents || [])
.reduce((sum: number, item: any) => {
const price = piPrices?.appraisal.items.find((a) => a.typeID === item.type_id)?.prices.sell.min ?? 0;
return sum + (item.amount * price);
}, 0);
const fillRate = storageCapacity > 0 ? (totalVolume / storageCapacity) * 100 : 0;
return {
type: storageType,
capacity: storageCapacity,
used: totalVolume,
fillRate: fillRate,
value: totalValue
};
};
const handleExcludeChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleExcludeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
updatePlanetConfig({ updatePlanetConfig({
@@ -153,10 +102,73 @@ export const PlanetTableRow = ({
}); });
}; };
// Check if there are any alerts
const hasLowExtractionRate = planetDetails.extractorAverages.length > 0 && minExtractionRate > 0 && planetDetails.extractorAverages.some(avg => avg.averagePerHour < minExtractionRate);
const hasAlerts = alertMode && (
planetDetails.expired ||
planetDetails.storageInfo.some(storage => storage.fillRate > 60) ||
planetDetails.importDepletionTimes.some(depletion => depletion.hoursUntilDepletion < 24) ||
planetDetails.hasLargeExtractorDifference ||
hasLowExtractionRate
);
// If in alert mode and no alerts, hide the row
if (alertMode && !hasAlerts) {
return null;
}
const renderProductDisplay = (typeId: number, amount?: number) => {
if (!typeId || !PI_TYPES_MAP[typeId]) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
<Typography fontSize={theme.custom.smallText} color="text.secondary">
No product
</Typography>
{amount !== undefined && (
<Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px", flexShrink: 0 }}>
{amount}
</Typography>
)}
</div>
);
}
if (showProductIcons) {
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
<Image
src={`${EVE_IMAGE_URL}/types/${typeId}/icon?size=32`}
alt={PI_TYPES_MAP[typeId].name}
width={32}
height={32}
style={{ marginRight: "5px" }}
/>
{amount !== undefined && (
<Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px", flexShrink: 0 }}>
{amount}
</Typography>
)}
</div>
);
}
return (
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", width: "100%" }}>
<Typography fontSize={theme.custom.smallText}>
{PI_TYPES_MAP[typeId].name}
</Typography>
{amount !== undefined && (
<Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px", flexShrink: 0 }}>
{amount}
</Typography>
)}
</div>
);
};
return ( return (
<> <>
<TableRow <TableRow
style={{ visibility: alertModeVisibility(alertMode, expired) }} style={{ visibility: planetDetails.visibility }}
sx={{ sx={{
"&:last-child td, &:last-child th": { border: 0 }, "&:last-child td, &:last-child th": { border: 0 },
cursor: 'pointer', cursor: 'pointer',
@@ -164,12 +176,9 @@ export const PlanetTableRow = ({
backgroundColor: 'action.hover' backgroundColor: 'action.hover'
} }
}} }}
onClick={(e) => { onClick={(e: React.MouseEvent<HTMLTableRowElement>) => {
// Only trigger if clicking a cell with the clickable-cell class
if (!(e.target as HTMLElement).closest('.clickable-cell')) return; if (!(e.target as HTMLElement).closest('.clickable-cell')) return;
if (extractors.length > 0) { setSimulationOpen(!simulationOpen);
setSimulationOpen(!simulationOpen);
}
}} }}
> >
<TableCell component="th" scope="row" className="clickable-cell"> <TableCell component="th" scope="row" className="clickable-cell">
@@ -190,17 +199,19 @@ export const PlanetTableRow = ({
<Tooltip <Tooltip
placement="right" placement="right"
title={ title={
<ExtractionSimulationTooltip planetDetails.extractors.length > 0 ? (
extractors={extractors <ExtractionSimulationTooltip
.filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle) extractors={planetDetails.extractors
.map(e => ({ .filter(e => e.extractor_details?.product_type_id && e.extractor_details?.qty_per_cycle)
typeId: e.extractor_details!.product_type_id!, .map(e => ({
baseValue: e.extractor_details!.qty_per_cycle!, typeId: e.extractor_details!.product_type_id!,
cycleTime: e.extractor_details!.cycle_time || 3600, baseValue: e.extractor_details!.qty_per_cycle!,
installTime: e.install_time ?? "", cycleTime: e.extractor_details!.cycle_time || 3600,
expiryTime: e.expiry_time ?? "" installTime: e.install_time ?? "",
}))} expiryTime: e.expiry_time ?? ""
/> }))}
/>
) : null
} }
componentsProps={{ componentsProps={{
tooltip: { tooltip: {
@@ -216,14 +227,14 @@ export const PlanetTableRow = ({
}} }}
> >
<Stack spacing={0}> <Stack spacing={0}>
<Typography <Typography
fontSize={theme.custom.smallText} fontSize={theme.custom.smallText}
color={hasLargeExtractorDifference ? 'error' : 'inherit'} color={(planetDetails.hasLargeExtractorDifference || hasLowExtractionRate) ? 'error' : 'inherit'}
> >
{planetInfoUniverse?.name} {planetInfoUniverse?.name}
</Typography> </Typography>
{hasLargeExtractorDifference && ( {planetDetails.hasLargeExtractorDifference && (
<Typography <Typography
fontSize={theme.custom.smallText} fontSize={theme.custom.smallText}
color="error" color="error"
sx={{ opacity: 0.7 }} sx={{ opacity: 0.7 }}
@@ -231,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>
@@ -239,11 +259,12 @@ export const PlanetTableRow = ({
<TableCell className="clickable-cell">{planet.upgrade_level}</TableCell> <TableCell className="clickable-cell">{planet.upgrade_level}</TableCell>
<TableCell className="clickable-cell"> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{extractors.map((e, idx) => { {planetDetails.extractors.length === 0 &&<Typography fontSize={theme.custom.smallText}>No extractors</Typography>}
{planetDetails.extractors.map((e, idx) => {
return ( return (
<div <div
key={`${e}-${idx}-${character.character.characterId}`} key={`${e}-${idx}-${character.character.characterId}`}
style={{ display: "flex" }} style={{ display: "flex", alignItems: "center" }}
> >
<Typography <Typography
color={timeColor(e.expiry_time, colors)} color={timeColor(e.expiry_time, colors)}
@@ -251,20 +272,19 @@ export const PlanetTableRow = ({
paddingRight={1} paddingRight={1}
> >
{e ? ( {e ? (
<Countdown extractionTimeMode ? (
overtime={true} DateTime.fromISO(e.expiry_time ?? "").toFormat('yyyy-MM-dd HH:mm:ss')
date={DateTime.fromISO(e.expiry_time ?? "").toMillis()} ) : (
/> <Countdown
overtime={true}
date={DateTime.fromISO(e.expiry_time ?? "").toMillis()}
/>
)
) : ( ) : (
"STOPPED" "STOPPED"
)} )}
</Typography> </Typography>
<Typography fontSize={theme.custom.smallText}> {renderProductDisplay(e.extractor_details?.product_type_id ?? 0)}
{
PI_TYPES_MAP[e.extractor_details?.product_type_id ?? 0]
?.name
}
</Typography>
</div> </div>
); );
})} })}
@@ -272,47 +292,68 @@ export const PlanetTableRow = ({
</TableCell> </TableCell>
<TableCell className="clickable-cell"> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{Array.from(localProduction).map((schematic, idx) => { {Array.from(planetDetails.localProduction).map((schematic, idx) => {
return ( return (
<Typography <div
key={`prod-${character.character.characterId}-${planet.planet_id}-${idx}`} key={`prod-${character.character.characterId}-${planet.planet_id}-${idx}`}
fontSize={theme.custom.smallText} style={{ display: "flex", alignItems: "center" }}
> >
{schematic[1].name} {renderProductDisplay(schematic[1].outputs[0].type_id)}
</Typography> </div>
); );
})} })}
</div> </div>
</TableCell> </TableCell>
<TableCell className="clickable-cell"> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{localImports.map((i) => ( {planetDetails.localImports.map((i) => {
<Typography const depletionTime = planetDetails.importDepletionTimes.find(d => d.typeId === i.type_id);
key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`} return (
fontSize={theme.custom.smallText} <div
> key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`}
{PI_TYPES_MAP[i.type_id].name} ({i.quantity}/h) style={{ display: "flex", alignItems: "center" }}
</Typography> >
))} <Tooltip title={
<>
<div>Will be depleted in {depletionTime?.hoursUntilDepletion.toFixed(1)} hours</div>
<div>Monthly cost: {depletionTime?.monthlyCost.toFixed(2)}M ISK</div>
</>
}>
<div style={{ display: "flex", alignItems: "center" }}>
{renderProductDisplay(i.type_id, i.quantity * i.factoryCount)}
{depletionTime && (
<Typography
fontSize={theme.custom.smallText}
color={depletionTime.hoursUntilDepletion < 24 ? 'error' : depletionTime.hoursUntilDepletion < 48 ? 'warning' : 'success'}
sx={{ ml: 1 }}
>
({depletionTime.hoursUntilDepletion.toFixed(1)}h)
</Typography>
)}
</div>
</Tooltip>
</div>
);
})}
</div> </div>
</TableCell> </TableCell>
<TableCell className="clickable-cell"> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{localExports.map((exports) => ( {planetDetails.localExports.map((exports) => (
<Typography <div
key={`export-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`} key={`export-${character.character.characterId}-${planet.planet_id}-${exports.type_id}`}
fontSize={theme.custom.smallText} style={{ display: "flex", alignItems: "center" }}
> >
{PI_TYPES_MAP[exports.typeId].name} {renderProductDisplay(exports.type_id, exports.quantity * exports.factoryCount)}
</Typography> </div>
))} ))}
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{localExports.map((exports) => ( {planetDetails.localExports.map((exports) => (
<FormControlLabel <FormControlLabel
key={`export-excluded-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`} key={`export-excluded-${character.character.characterId}-${planet.planet_id}-${exports.type_id}`}
control={ control={
<Checkbox <Checkbox
checked={planetConfig.excludeFromTotals} checked={planetConfig.excludeFromTotals}
@@ -327,12 +368,12 @@ export const PlanetTableRow = ({
</TableCell> </TableCell>
<TableCell className="clickable-cell"> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{localExports.map((exports) => ( {planetDetails.localExports.map((exports) => (
<Typography <Typography
key={`export-uph-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`} key={`export-uph-${character.character.characterId}-${planet.planet_id}-${exports.type_id}`}
fontSize={theme.custom.smallText} fontSize={theme.custom.smallText}
> >
{exports.amount} {exports.quantity * exports.factoryCount}
</Typography> </Typography>
))} ))}
</div> </div>
@@ -346,11 +387,11 @@ export const PlanetTableRow = ({
textAlign: "end", textAlign: "end",
}} }}
> >
{localExports.map((e) => { {planetDetails.localExports.map((e) => {
const valueInMillions = const valueInMillions =
(((piPrices?.appraisal.items.find((a) => a.typeID === e.typeId) (((piPrices?.appraisal.items.find((a) => a.typeID === e.type_id)
?.prices.sell.min ?? 0) * ?.prices.sell.min ?? 0) *
e.amount) / e.quantity * e.factoryCount) /
1000000) * 1000000) *
24 * 24 *
30; 30;
@@ -361,7 +402,7 @@ export const PlanetTableRow = ({
return ( return (
<Typography <Typography
key={`export-praisal-${character.character.characterId}-${planet.planet_id}-${e.typeId}`} key={`export-praisal-${character.character.characterId}-${planet.planet_id}-${e.type_id}`}
fontSize={theme.custom.smallText} fontSize={theme.custom.smallText}
> >
{displayValue} {displayValue}
@@ -371,46 +412,119 @@ export const PlanetTableRow = ({
</div> </div>
</TableCell> </TableCell>
<TableCell className="clickable-cell"> <TableCell className="clickable-cell">
<div style={{ display: "flex", flexDirection: "column" }}> <Tooltip
{storageFacilities title={
.sort((a, b) => { <Box sx={{ p: 1 }}>
const isALaunchpad = a.type_id === 2256 || a.type_id === 2542 || a.type_id === 2543 || a.type_id === 2544 || a.type_id === 2552 || a.type_id === 2555 || a.type_id === 2556 || a.type_id === 2557; <Typography variant="subtitle2" sx={{ mb: 1 }}>
const isBLaunchpad = b.type_id === 2256 || b.type_id === 2542 || b.type_id === 2543 || b.type_id === 2544 || b.type_id === 2552 || b.type_id === 2555 || b.type_id === 2556 || b.type_id === 2557; Storage Facilities
return isALaunchpad === isBLaunchpad ? 0 : isALaunchpad ? -1 : 1; </Typography>
}) <Table size="small">
.map((storage) => { <TableHead>
const storageInfo = getStorageInfo(storage); <TableRow>
if (!storageInfo) return null; <TableCell>Type</TableCell>
<TableCell align="right">Capacity</TableCell>
const isLaunchpad = storage.type_id === 2256 || <TableCell align="right">Used</TableCell>
storage.type_id === 2542 || <TableCell align="right">Fill Rate</TableCell>
storage.type_id === 2543 || <TableCell align="right">Value</TableCell>
storage.type_id === 2544 || </TableRow>
storage.type_id === 2552 || </TableHead>
storage.type_id === 2555 || <TableBody>
storage.type_id === 2556 || {planetDetails.storageInfo
storage.type_id === 2557; .map(storage => ({
...storage,
const fillRate = storageInfo.fillRate; isLaunchpad: LAUNCHPAD_IDS.includes(storage.type_id)
const color = fillRate > 95 ? '#ff0000' : fillRate > 80 ? '#ffd700' : 'inherit'; }))
.sort((a, b) => (b.isLaunchpad ? 1 : 0) - (a.isLaunchpad ? 1 : 0))
return ( .map((storage, idx) => {
<div key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.pin_id}`} style={{ display: "flex", alignItems: "center" }}> const fillRate = storage.fillRate;
<Typography fontSize={theme.custom.smallText} style={{ marginRight: "5px" }}> const color = fillRate > 90 ? '#ff0000' : fillRate > 80 ? '#ffa500' : fillRate > 60 ? '#ffd700' : 'inherit';
{isLaunchpad ? 'L' : 'S'} const contents = planet.info.pins.find(p => p.type_id === storage.type_id)?.contents || [];
</Typography>
<Typography fontSize={theme.custom.smallText} style={{ color }}> return (
{fillRate.toFixed(1)}% <React.Fragment key={`storage-${character.character.characterId}-${planet.planet_id}-${storage.type}-${idx}`}>
</Typography> <TableRow>
{storageInfo.value > 0 && ( <TableCell>{storage.isLaunchpad ? 'Launchpad' : 'Storage'}</TableCell>
<Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px" }}> <TableCell align="right">{storage.capacity.toFixed(1)} m³</TableCell>
({Math.round(storageInfo.value / 1000000)}M) <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>
<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>
</Table>
</Box>
}
componentsProps={{
tooltip: {
sx: {
bgcolor: 'background.paper',
'& .MuiTooltip-arrow': {
color: 'background.paper',
},
maxWidth: 'none',
width: 'fit-content'
}
}
}}
>
<div style={{ display: "flex", flexDirection: "column" }}>
{planetDetails.storageInfo.length === 0 &&<Typography fontSize={theme.custom.smallText}>No storage</Typography>}
{planetDetails.storageInfo
.map(storage => ({
...storage,
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 (
<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" }}>
{storage.isLaunchpad ? 'L' : 'S'}
</Typography> </Typography>
)} <Typography fontSize={theme.custom.smallText} style={{ color }}>
</div> {fillRate.toFixed(1)}%
); </Typography>
})} {storage.value > 0 && (
</div> <Typography fontSize={theme.custom.smallText} style={{ marginLeft: "5px" }}>
({Math.round(storage.value / 1000000)}M)
</Typography>
)}
</div>
);
})}
</div>
</Tooltip>
</TableCell> </TableCell>
<TableCell className="menu-cell"> <TableCell className="menu-cell">
<IconButton <IconButton
@@ -448,7 +562,7 @@ export const PlanetTableRow = ({
<Collapse in={simulationOpen} timeout="auto" unmountOnExit> <Collapse in={simulationOpen} timeout="auto" unmountOnExit>
<Box sx={{ my: 2 }}> <Box sx={{ my: 2 }}>
<ExtractionSimulationDisplay <ExtractionSimulationDisplay
extractors={extractors extractors={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)
.map(e => ({ .map(e => ({
typeId: e.extractor_details!.product_type_id!, typeId: e.extractor_details!.product_type_id!,
@@ -457,7 +571,21 @@ export const PlanetTableRow = ({
installTime: e.install_time ?? "", installTime: e.install_time ?? "",
expiryTime: e.expiry_time ?? "" expiryTime: e.expiry_time ?? ""
}))} }))}
productionNodes={productionNodes} productionNodes={Array.from(planetDetails.localProduction).map(([schematicId, schematic]) => ({
schematicId: schematicId,
typeId: schematic.outputs[0].type_id,
name: schematic.name,
inputs: schematic.inputs.map((input: SchematicInput) => ({
typeId: input.type_id,
quantity: input.quantity
})),
outputs: schematic.outputs.map((output: SchematicOutput) => ({
typeId: output.type_id,
quantity: output.quantity
})),
cycleTime: schematic.cycle_time,
factoryCount: schematic.factoryCount || 1
}))}
/> />
</Box> </Box>
</Collapse> </Collapse>

View File

@@ -1,5 +1,5 @@
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { Icon, IconButton, Stack, Tooltip, Typography, styled, useTheme } from "@mui/material"; import { IconButton, Stack, Tooltip, Typography, styled, useTheme } from "@mui/material";
import { PlanetCard } from "./PlanetCard"; import { PlanetCard } from "./PlanetCard";
import { NoPlanetCard } from "./NoPlanetCard"; import { NoPlanetCard } from "./NoPlanetCard";
import Table from "@mui/material/Table"; import Table from "@mui/material/Table";
@@ -11,6 +11,7 @@ import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import { PlanetTableRow } from "./PlanetTableRow"; import { PlanetTableRow } from "./PlanetTableRow";
import { Settings } from "@mui/icons-material"; import { Settings } from "@mui/icons-material";
import { PlanetCalculations } from "@/types/planet";
const StackItem = styled(Stack)(({ theme }) => ({ const StackItem = styled(Stack)(({ theme }) => ({
...theme.typography.body2, ...theme.typography.body2,
@@ -22,8 +23,10 @@ const StackItem = styled(Stack)(({ theme }) => ({
const PlanetaryIteractionTable = ({ const PlanetaryIteractionTable = ({
character, character,
planetDetails,
}: { }: {
character: AccessToken; character: AccessToken;
planetDetails: Record<number, PlanetCalculations>;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
@@ -36,8 +39,8 @@ const PlanetaryIteractionTable = ({
return ( return (
<StackItem width="100%"> <StackItem width="100%">
<TableContainer component={Paper}> <TableContainer component={Paper} sx={{ width: '100%' }}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table" sx={{ width: '100%' }}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell width="8%"> <TableCell width="8%">
@@ -84,8 +87,8 @@ const PlanetaryIteractionTable = ({
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell width="2%"> <TableCell width="2%">
<Tooltip title="How many units per hour factories are producing"> <Tooltip title="How many units per hour factories are producing on this planet">
<Typography fontSize={theme.custom.smallText}>u/h</Typography> <Typography fontSize={theme.custom.smallText}>uph</Typography>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
<TableCell width="4%" align="right"> <TableCell width="4%" align="right">
@@ -98,7 +101,7 @@ const PlanetaryIteractionTable = ({
<TableCell width="10%"> <TableCell width="10%">
<Tooltip title="Storage facility fill rate"> <Tooltip title="Storage facility fill rate">
<Typography fontSize={theme.custom.smallText}> <Typography fontSize={theme.custom.smallText}>
Storage Fill rate Storage%
</Typography> </Typography>
</Tooltip> </Tooltip>
</TableCell> </TableCell>
@@ -117,6 +120,7 @@ const PlanetaryIteractionTable = ({
key={`${character.character.characterId}-${planet.planet_id}`} key={`${character.character.characterId}-${planet.planet_id}`}
planet={planet} planet={planet}
character={character} character={character}
planetDetails={planetDetails[planet.planet_id]}
/> />
))} ))}
</TableBody> </TableBody>
@@ -128,8 +132,10 @@ const PlanetaryIteractionTable = ({
const PlanetaryInteractionIconsRow = ({ const PlanetaryInteractionIconsRow = ({
character, character,
planetDetails,
}: { }: {
character: AccessToken; character: AccessToken;
planetDetails: Record<number, PlanetCalculations>;
}) => { }) => {
return ( return (
<StackItem> <StackItem>
@@ -139,6 +145,7 @@ const PlanetaryInteractionIconsRow = ({
key={`${character.character.characterId}-${planet.planet_id}`} key={`${character.character.characterId}-${planet.planet_id}`}
planet={planet} planet={planet}
character={character} character={character}
planetDetails={planetDetails[planet.planet_id]}
/> />
))} ))}
{Array.from(Array(6 - character.planets.length).keys()).map((i, id) => ( {Array.from(Array(6 - character.planets.length).keys()).map((i, id) => (
@@ -153,14 +160,16 @@ const PlanetaryInteractionIconsRow = ({
export const PlanetaryInteractionRow = ({ export const PlanetaryInteractionRow = ({
character, character,
planetDetails,
}: { }: {
character: AccessToken; character: AccessToken;
planetDetails: Record<number, PlanetCalculations>;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
return theme.custom.compactMode ? ( return theme.custom.compactMode ? (
<PlanetaryInteractionIconsRow character={character} /> <div style={{ marginTop: "1.2rem" }}><PlanetaryInteractionIconsRow character={character} planetDetails={planetDetails} /></div>
) : ( ) : (
<PlanetaryIteractionTable character={character} /> <div style={{ marginTop: "1.4rem" }}><PlanetaryIteractionTable character={character} planetDetails={planetDetails} /></div>
); );
}; };

View File

@@ -13,13 +13,15 @@ import {
Typography, Typography,
TextField, TextField,
Box, Box,
FormControlLabel,
Checkbox,
} from "@mui/material"; } from "@mui/material";
import { ColorChangeHandler, ColorResult, CompactPicker } from "react-color"; import { ColorChangeHandler, ColorResult, CompactPicker } from "react-color";
import React, { useState, useContext } from "react"; 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 } = 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 = () => {
@@ -45,6 +47,17 @@ export const SettingsButton = () => {
} }
}; };
const handleShowProductIconsChange = (event: React.ChangeEvent<HTMLInputElement>) => {
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">
<> <>
@@ -57,13 +70,24 @@ export const SettingsButton = () => {
aria-labelledby="alert-dialog-title" aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" aria-describedby="alert-dialog-description"
> >
<DialogTitle id="alert-dialog-title"> <DialogTitle id="alert-dialog-title">
{"Settings"} {"Settings"}
</DialogTitle> </DialogTitle>
<DialogContent style={{ paddingTop: "1rem" }}> <DialogContent style={{ paddingTop: "1rem" }}>
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<Typography variant="subtitle1">Display Settings</Typography>
<FormControlLabel
control={
<Checkbox
checked={showProductIcons}
onChange={handleShowProductIconsChange}
/>
}
label="Show product icons instead of names"
/>
</Box>
<Box sx={{ mt: 2 }}>
<Typography variant="subtitle1">Balance Threshold</Typography> <Typography variant="subtitle1">Balance Threshold</Typography>
<TextField <TextField
type="number" type="number"
@@ -76,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}`}>
@@ -87,7 +124,6 @@ export const SettingsButton = () => {
</div> </div>
); );
})} })}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose}>Close</Button> <Button onClick={handleClose}>Close</Button>

View File

@@ -51,18 +51,22 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
const [sortDirection, setSortDirection] = useState<SortDirection>("asc"); const [sortDirection, setSortDirection] = useState<SortDirection>("asc");
const [sortBy, setSortBy] = useState<SortBy>("name"); const [sortBy, setSortBy] = useState<SortBy>("name");
const [startDate, setStartDate] = useState<string>(DateTime.now().startOf('day').toISO()); const [startDate, setStartDate] = useState<string>(DateTime.now().startOf('day').toISO());
const [activityPercentage, setActivityPercentage] = useState<number>(() => { const [activityPercentage, setActivityPercentage] = useState<number>(100);
const saved = localStorage.getItem('activityPercentage');
return saved ? parseFloat(saved) : 100;
});
// Load saved values from localStorage on mount
useEffect(() => { useEffect(() => {
const savedDate = localStorage.getItem('productionStartDate'); const savedDate = localStorage.getItem('productionStartDate');
if (savedDate) { if (savedDate) {
setStartDate(savedDate); setStartDate(savedDate);
} }
const savedActivity = localStorage.getItem('activityPercentage');
if (savedActivity) {
setActivityPercentage(parseFloat(savedActivity));
}
}, []); }, []);
// Save values to localStorage when they change
useEffect(() => { useEffect(() => {
localStorage.setItem('productionStartDate', startDate); localStorage.setItem('productionStartDate', startDate);
}, [startDate]); }, [startDate]);
@@ -195,8 +199,8 @@ export const Summary = ({ characters }: { characters: AccessToken[] }) => {
sx={{ width: '100px' }} sx={{ width: '100px' }}
/> />
</Box> </Box>
<TableContainer component={Paper}> <TableContainer component={Paper} sx={{ width: '100%' }}>
<Table size="small" aria-label="a dense table"> <Table size="small" aria-label="a dense table" sx={{ width: '100%' }}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell width="20%"> <TableCell width="20%">

View File

@@ -1,23 +0,0 @@
import { Box, Button, Tooltip } from "@mui/material";
export const SupportButton = () => {
return (
<Box>
<Tooltip
title={`
Consider using code 'CALLIEVE' on EVE store checkout to support the project! Click to copy to clipboard ;)
`}
>
<Button
href=""
style={{ width: "100%" }}
sx={{ color: "white", display: "block" }}
onClick={() => {
navigator.clipboard.writeText("CALLIEVE");
}}
>
CALLIEVE
</Button>
</Tooltip>
</Box>
);
};

View File

@@ -26,6 +26,8 @@ export const SessionContext = createContext<{
togglePlanMode: () => void; togglePlanMode: () => void;
alertMode: boolean; alertMode: boolean;
toggleAlertMode: () => void; toggleAlertMode: () => void;
extractionTimeMode: boolean;
toggleExtractionTimeMode: () => void;
piPrices: EvePraisalResult | undefined; piPrices: EvePraisalResult | undefined;
updatePlanetConfig: (config: PlanetConfig) => void; updatePlanetConfig: (config: PlanetConfig) => void;
readPlanetConfig: ({ readPlanetConfig: ({
@@ -37,6 +39,10 @@ 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;
setShowProductIcons: (show: boolean) => void;
}>({ }>({
sessionReady: false, sessionReady: false,
refreshSession: () => {}, refreshSession: () => {},
@@ -49,6 +55,8 @@ export const SessionContext = createContext<{
togglePlanMode: () => {}, togglePlanMode: () => {},
alertMode: false, alertMode: false,
toggleAlertMode: () => {}, toggleAlertMode: () => {},
extractionTimeMode: false,
toggleExtractionTimeMode: () => {},
piPrices: undefined, piPrices: undefined,
updatePlanetConfig: () => {}, updatePlanetConfig: () => {},
readPlanetConfig: ({ readPlanetConfig: ({
@@ -62,7 +70,12 @@ export const SessionContext = createContext<{
}, },
balanceThreshold: 1000, balanceThreshold: 1000,
setBalanceThreshold: () => {}, setBalanceThreshold: () => {},
minExtractionRate: 0,
setMinExtractionRate: () => {},
showProductIcons: false,
setShowProductIcons: () => {},
}); });
export type ColorSelectionType = { export type ColorSelectionType = {
defaultColor: string; defaultColor: string;
expiredColor: string; expiredColor: string;
@@ -84,6 +97,7 @@ export const defaultColors = {
dayColor: "#2F695A", dayColor: "#2F695A",
twoDaysColor: "#2F695A", twoDaysColor: "#2F695A",
}; };
export const ColorContext = createContext<{ export const ColorContext = createContext<{
colors: ColorSelectionType; colors: ColorSelectionType;
setColors: (colors: ColorSelectionType) => void; setColors: (colors: ColorSelectionType) => void;

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,9 @@ 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 [extractionTimeMode, setExtractionTimeMode] = useState(false);
const [colors, setColors] = useState<ColorSelectionType>(defaultColors); const [colors, setColors] = useState<ColorSelectionType>(defaultColors);
const [alertMode, setAlertMode] = useState(false); const [alertMode, setAlertMode] = useState(false);
@@ -61,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 (
@@ -93,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[]) => {
@@ -148,6 +159,10 @@ const Home = () => {
setAlertMode(!alertMode); setAlertMode(!alertMode);
}; };
const toggleExtractionTimeMode = () => {
setExtractionTimeMode(!extractionTimeMode);
};
const updatePlanetConfig = (config: PlanetConfig) => { const updatePlanetConfig = (config: PlanetConfig) => {
const charactersToSave = characters.map((c) => { const charactersToSave = characters.map((c) => {
if (c.character.characterId === config.characterId) { if (c.character.characterId === config.characterId) {
@@ -223,6 +238,17 @@ const Home = () => {
localStorage.setItem("colors", JSON.stringify(colors)); localStorage.setItem("colors", JSON.stringify(colors));
}, [colors]); }, [colors]);
useEffect(() => {
const savedMode = localStorage.getItem('extractionTimeMode');
if (savedMode) {
setExtractionTimeMode(savedMode === 'true');
}
}, []);
useEffect(() => {
localStorage.setItem('extractionTimeMode', extractionTimeMode.toString());
}, [extractionTimeMode]);
useEffect(() => { useEffect(() => {
fetch("api/env") fetch("api/env")
.then((r) => r.json()) .then((r) => r.json())
@@ -248,9 +274,9 @@ const Home = () => {
}, []); }, []);
useEffect(() => { useEffect(() => {
const ESI_CACHE_TIME_MS = 600000; 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)
@@ -274,10 +300,16 @@ const Home = () => {
piPrices, piPrices,
alertMode, alertMode,
toggleAlertMode, toggleAlertMode,
extractionTimeMode,
toggleExtractionTimeMode,
updatePlanetConfig, updatePlanetConfig,
readPlanetConfig, readPlanetConfig,
balanceThreshold, balanceThreshold,
setBalanceThreshold, setBalanceThreshold,
minExtractionRate,
setMinExtractionRate,
showProductIcons,
setShowProductIcons,
}} }}
> >
<CharacterContext.Provider <CharacterContext.Provider

View File

@@ -1080,3 +1080,5 @@ export const STORAGE_CAPACITIES: Record<number, number> = {
2556: 10000, // Plasma Launchpad 2556: 10000, // Plasma Launchpad
2557: 10000, // Storm Launchpad 2557: 10000, // Storm Launchpad
}; };
export const LAUNCHPAD_IDS = [2256, 2542, 2543, 2544, 2552, 2555, 2556, 2557];

View File

@@ -1,11 +1,51 @@
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import logger from "@/utils/logger";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "GET") { if (req.method === "GET") {
const EVE_SSO_CALLBACK_URL = process.env.EVE_SSO_CALLBACK_URL; logger.info({
const EVE_SSO_CLIENT_ID = process.env.EVE_SSO_CLIENT_ID; event: 'env_request_start'
res.json({ EVE_SSO_CLIENT_ID, EVE_SSO_CALLBACK_URL }); });
try {
const EVE_SSO_CALLBACK_URL = process.env.EVE_SSO_CALLBACK_URL;
const EVE_SSO_CLIENT_ID = process.env.EVE_SSO_CLIENT_ID;
if (!EVE_SSO_CALLBACK_URL || !EVE_SSO_CLIENT_ID) {
logger.error({
event: 'env_request_failed',
reason: 'missing_env_vars',
vars: {
hasCallbackUrl: !!EVE_SSO_CALLBACK_URL,
hasClientId: !!EVE_SSO_CLIENT_ID
}
});
return res.status(500).json({ error: 'Missing required environment variables' });
}
logger.info({
event: 'env_request_success',
vars: {
hasCallbackUrl: true,
hasClientId: true
}
});
return res.json({ EVE_SSO_CLIENT_ID, EVE_SSO_CALLBACK_URL });
} catch (e) {
logger.error({
event: 'env_request_failed',
reason: 'unexpected_error',
error: e
});
return res.status(500).json({ error: 'Internal server error' });
}
} else { } else {
logger.warn({
event: 'invalid_method',
method: req.method,
path: req.url
});
res.status(404).end(); res.status(404).end();
} }
}; };

View File

@@ -1,19 +1,45 @@
import { getPraisal } from "@/eve-praisal"; import { getPraisal } from "@/eve-praisal";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import logger from "@/utils/logger";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST") { if (req.method === "POST") {
const praisalRequest: { quantity: number; type_id: number }[] = JSON.parse( logger.info({
req.body event: 'praisal_request_start'
); });
try { try {
const praisalRequest: { quantity: number; type_id: number }[] = JSON.parse(
req.body
);
logger.info({
event: 'praisal_request_parsed',
items: praisalRequest.length
});
const praisal = await getPraisal(praisalRequest); const praisal = await getPraisal(praisalRequest);
logger.info({
event: 'praisal_request_success',
items: praisalRequest.length
});
return res.json(praisal); return res.json(praisal);
} catch (e) { } catch (e) {
console.log(e); logger.error({
res.status(404).end(); event: 'praisal_request_failed',
error: e,
body: req.body
});
return res.status(500).json({ error: 'Failed to get praisal' });
} }
} else { } else {
logger.warn({
event: 'invalid_method',
method: req.method,
path: req.url
});
res.status(404).end(); res.status(404).end();
} }
}; };

View File

@@ -1,7 +1,8 @@
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { extractCharacterFromToken } from "@/utils"; import { extractCharacterFromToken } from "@/utils/utils";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import crypto from "crypto-js"; import crypto from "crypto-js";
import logger from "@/utils/logger";
const EVE_SSO_TOKEN_URL = "https://login.eveonline.com/v2/oauth/token"; const EVE_SSO_TOKEN_URL = "https://login.eveonline.com/v2/oauth/token";
const EVE_SSO_CLIENT_ID = process.env.EVE_SSO_CLIENT_ID ?? ""; const EVE_SSO_CLIENT_ID = process.env.EVE_SSO_CLIENT_ID ?? "";
@@ -10,6 +11,14 @@ const EVE_SSO_SECRET = process.env.EVE_SSO_SECRET ?? "";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST") { if (req.method === "POST") {
const accessToken: AccessToken = req.body; const accessToken: AccessToken = req.body;
logger.info({
event: 'token_refresh_start',
character: {
name: accessToken.character.name,
characterId: accessToken.character.characterId
}
});
const params = new URLSearchParams({ const params = new URLSearchParams({
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token: crypto.AES.decrypt( refresh_token: crypto.AES.decrypt(
@@ -33,7 +42,20 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
body: params, body: params,
headers, headers,
}).then((res) => res.json()); }).then((res) => res.json());
const character = extractCharacterFromToken(response); const character = extractCharacterFromToken(response);
if (!character) {
logger.error({
event: 'token_refresh_failed',
reason: 'character_extraction_failed',
character: {
name: accessToken.character.name,
characterId: accessToken.character.characterId
}
});
return res.json({ ...accessToken, needsLogin: true });
}
const token: AccessToken = { const token: AccessToken = {
access_token: response.access_token, access_token: response.access_token,
token_type: response.token_type, token_type: response.token_type,
@@ -51,12 +73,26 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
planetConfig: accessToken.planetConfig ?? [], planetConfig: accessToken.planetConfig ?? [],
}; };
console.log("Refresh", character.name, character.characterId); logger.info({
event: 'token_refresh_success',
character: {
name: character.name,
characterId: character.characterId
}
});
return res.json(token); return res.json(token);
} catch (e) { } catch (e) {
console.log(e); logger.error({
res.json({ ...accessToken, needsLogin: true }); event: 'token_refresh_failed',
reason: 'api_error',
error: e,
character: {
name: accessToken.character.name,
characterId: accessToken.character.characterId
}
});
return res.json({ ...accessToken, needsLogin: true });
} }
} else { } else {
res.status(404).end(); res.status(404).end();

View File

@@ -1,6 +1,7 @@
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import crypto from "crypto-js"; import crypto from "crypto-js";
import logger from "@/utils/logger";
const EVE_SSO_REVOKE_URL = "https://login.eveonline.com/v2/oauth/revoke"; const EVE_SSO_REVOKE_URL = "https://login.eveonline.com/v2/oauth/revoke";
const EVE_SSO_CLIENT_ID = process.env.EVE_SSO_CLIENT_ID ?? ""; const EVE_SSO_CLIENT_ID = process.env.EVE_SSO_CLIENT_ID ?? "";
@@ -9,6 +10,14 @@ const EVE_SSO_SECRET = process.env.EVE_SSO_SECRET ?? "";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "POST") { if (req.method === "POST") {
const accessToken: AccessToken = req.body; const accessToken: AccessToken = req.body;
logger.info({
event: 'token_revoke_start',
character: {
name: accessToken.character.name,
characterId: accessToken.character.characterId
}
});
const params = new URLSearchParams({ const params = new URLSearchParams({
grant_type: "refresh_token", grant_type: "refresh_token",
refresh_token: crypto.AES.decrypt( refresh_token: crypto.AES.decrypt(
@@ -27,24 +36,42 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
}; };
try { try {
await fetch(EVE_SSO_REVOKE_URL, { const response = await fetch(EVE_SSO_REVOKE_URL, {
method: "POST", method: "POST",
body: params, body: params,
headers, headers,
}).then((res) => res.json()); });
console.log( if (!response.ok) {
"Revoke", throw new Error(`HTTP error! status: ${response.status}`);
accessToken.character.name, }
accessToken.character.characterId
); logger.info({
event: 'token_revoke_success',
character: {
name: accessToken.character.name,
characterId: accessToken.character.characterId
}
});
return res.end(); return res.end();
} catch (e) { } catch (e) {
console.log(e); logger.error({
event: 'token_revoke_failed',
error: e,
character: {
name: accessToken.character.name,
characterId: accessToken.character.characterId
}
});
return res.status(500).end(); return res.status(500).end();
} }
} else { } else {
logger.warn({
event: 'invalid_method',
method: req.method,
path: req.url
});
res.status(404).end(); res.status(404).end();
} }
}; };

View File

@@ -1,7 +1,8 @@
import { AccessToken } from "@/types"; import { AccessToken } from "@/types";
import { extractCharacterFromToken } from "@/utils"; import { extractCharacterFromToken } from "@/utils/utils";
import { NextApiRequest, NextApiResponse } from "next"; import { NextApiRequest, NextApiResponse } from "next";
import crypto from "crypto-js"; import crypto from "crypto-js";
import logger from "@/utils/logger";
const EVE_SSO_TOKEN_URL = "https://login.eveonline.com/v2/oauth/token"; const EVE_SSO_TOKEN_URL = "https://login.eveonline.com/v2/oauth/token";
const EVE_SSO_CLIENT_ID = process.env.EVE_SSO_CLIENT_ID ?? ""; const EVE_SSO_CLIENT_ID = process.env.EVE_SSO_CLIENT_ID ?? "";
@@ -10,7 +11,19 @@ const EVE_SSO_SECRET = process.env.EVE_SSO_SECRET ?? "";
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "GET") { if (req.method === "GET") {
const code = req.query.code as string; const code = req.query.code as string;
if (!code || code === undefined) return res.status(404).end(); if (!code || code === undefined) {
logger.warn({
event: 'token_request_failed',
reason: 'missing_code',
query: req.query
});
return res.status(404).end();
}
logger.info({
event: 'token_request_start',
code: code
});
const params = new URLSearchParams({ const params = new URLSearchParams({
grant_type: "authorization_code", grant_type: "authorization_code",
@@ -26,34 +39,69 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
"User-Agent": "https://github.com/calli-eve/eve-pi", "User-Agent": "https://github.com/calli-eve/eve-pi",
}; };
const response = await fetch(EVE_SSO_TOKEN_URL, { try {
method: "POST", const response = await fetch(EVE_SSO_TOKEN_URL, {
body: params, method: "POST",
headers, body: params,
}).then((res) => res.json()); headers,
});
const character = extractCharacterFromToken(response); if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
console.log("Login", character.name, character.characterId); const data = await response.json();
const character = extractCharacterFromToken(data);
const token: AccessToken = { if (!character) {
access_token: response.access_token, logger.error({
token_type: response.token_type, event: 'token_request_failed',
refresh_token: crypto.AES.encrypt( reason: 'character_extraction_failed',
response.refresh_token, data
EVE_SSO_SECRET, });
).toString(), return res.status(500).end();
expires_at: Date.now() + response.expires_in * 1000, }
character,
needsLogin: false, logger.info({
account: "-", event: 'token_request_success',
comment: "", character: {
system: "", name: character.name,
planets: [], characterId: character.characterId
planetConfig: [], }
}; });
res.json(token);
const token: AccessToken = {
access_token: data.access_token,
token_type: data.token_type,
refresh_token: crypto.AES.encrypt(
data.refresh_token,
EVE_SSO_SECRET,
).toString(),
expires_at: Date.now() + data.expires_in * 1000,
character,
needsLogin: false,
account: "-",
comment: "",
system: "",
planets: [],
planetConfig: [],
};
return res.json(token);
} catch (e) {
logger.error({
event: 'token_request_failed',
reason: 'api_error',
error: e,
code: code
});
return res.status(500).end();
}
} else { } else {
logger.warn({
event: 'invalid_method',
method: req.method,
path: req.url
});
res.status(404).end(); res.status(404).end();
} }
}; };

View File

@@ -7,7 +7,7 @@ import {
} from "@/types"; } from "@/types";
import { Api } from "@/esi-api"; import { Api } from "@/esi-api";
import { EXTRACTOR_TYPE_IDS, FACTORY_IDS, PI_SCHEMATICS } from "@/const"; import { EXTRACTOR_TYPE_IDS, FACTORY_IDS, PI_SCHEMATICS } from "@/const";
import { extractorsHaveExpired } from "./app/components/PlanetaryInteraction/timeColors"; import { extractorsHaveExpired } from "./app/components/PlanetaryInteraction/alerts";
export const getPlanets = async (character: AccessToken): Promise<Planet[]> => { export const getPlanets = async (character: AccessToken): Promise<Planet[]> => {
const api = new Api(); const api = new Api();
@@ -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;
}; };
@@ -94,7 +141,13 @@ export const planetCalculations = (planet: PlanetWithInfo) => {
![...locallyProduced, ...locallyExcavated].some( ![...locallyProduced, ...locallyExcavated].some(
(lp) => lp === p.type_id, (lp) => lp === p.type_id,
), ),
); ).map((p) => ({
...p,
factoryCount: planetInfo.pins
.filter((f) => f.schematic_id === p.schematic_id)
.length,
}));
const localExports = locallyProduced const localExports = locallyProduced
.filter((p) => !locallyConsumed.some((lp) => lp === p)) .filter((p) => !locallyConsumed.some((lp) => lp === p))

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 [];
};

84
src/types/planet.ts Normal file
View File

@@ -0,0 +1,84 @@
import { Pin, PlanetWithInfo } from '../types';
export interface StorageContent {
type_id: number;
amount: number;
}
export interface StorageInfo {
type: string;
type_id: number;
capacity: number;
used: number;
fillRate: number;
value: number;
}
export interface PlanetCalculations {
expired: boolean;
extractors: Pin[];
localProduction: Map<number, LocalProductionInfo>;
localImports: LocalImport[];
localExports: LocalExport[];
storageInfo: StorageInfo[];
extractorAverages: ExtractorAverage[];
hasLargeExtractorDifference: boolean;
importDepletionTimes: ImportDepletionTime[];
visibility: 'visible' | 'hidden';
}
export interface AlertState {
expired: boolean;
hasLowStorage: boolean;
hasLowImports: boolean;
hasLargeExtractorDifference: boolean;
hasLowExtractionRate: boolean;
}
export interface ExtractorAverage {
typeId: number;
averagePerHour: number;
}
export interface ImportDepletionTime {
typeId: number;
hoursUntilDepletion: number;
monthlyCost: number;
}
export interface LocalProductionInfo {
name: string;
cycle_time: number;
schematic_id: number;
inputs: SchematicInput[];
outputs: SchematicOutput[];
factoryCount: number;
}
export interface LocalImport {
type_id: number;
schematic_id: number;
quantity: number;
factoryCount: number;
}
export interface LocalExport {
type_id: number;
schematic_id: number;
quantity: number;
factoryCount: number;
}
export interface SchematicInput {
schematic_id: number;
type_id: number;
quantity: number;
is_input: number;
}
export interface SchematicOutput {
schematic_id: number;
type_id: number;
quantity: number;
is_input: number;
}

View File

@@ -1,13 +0,0 @@
import { AccessToken, Character } from "./types";
export const extractCharacterFromToken = (token: AccessToken): Character => {
const decodedToken = parseJwt(token.access_token);
return {
name: decodedToken.name,
characterId: decodedToken.sub.split(":")[2],
};
};
const parseJwt = (token: string) => {
return JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
};

16
src/utils/logger.ts Normal file
View File

@@ -0,0 +1,16 @@
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => {
return { level: label };
},
},
timestamp: () => `,"time":"${new Date().toISOString()}"`,
base: {
env: process.env.NODE_ENV,
},
});
export default logger;

22
src/utils/utils.ts Normal file
View File

@@ -0,0 +1,22 @@
import { AccessToken, Character } from "../types";
export const extractCharacterFromToken = (token: AccessToken): Character | null => {
const decodedToken = parseJwt(token.access_token);
if (!decodedToken || !decodedToken.name || !decodedToken.sub) {
return null;
}
return {
name: decodedToken.name,
characterId: decodedToken.sub.split(":")[2],
};
};
const parseJwt = (token: string | undefined) => {
if (!token) return null;
try {
return JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
} catch (error) {
console.error('Failed to parse JWT token:', error);
return null;
}
};