6 Commits

Author SHA1 Message Date
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
24 changed files with 1047 additions and 222 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

@@ -10,6 +10,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

365
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",
@@ -25,6 +26,8 @@
"luxon": "^3.6.1", "luxon": "^3.6.1",
"next": "^14.2.23", "next": "^14.2.23",
"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",
@@ -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",
@@ -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",
@@ -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",
@@ -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",
@@ -27,6 +28,8 @@
"luxon": "^3.6.1", "luxon": "^3.6.1",
"next": "^14.2.23", "next": "^14.2.23",
"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,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,25 @@ 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 { SettingsButton } from "../Settings/SettingsButtons"; import { SettingsButton } from "../Settings/SettingsButtons";
import { AlertModeButton } from "../AlertModeButton/AlertModeButton"; import {
import { SupportButton } from "../SupportButton/SupportButton"; 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 +107,26 @@ function ResponsiveAppBar() {
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<GitHubButton /> <GitHubButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<CCPButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<SupportButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<SettingsButton /> <SettingsButton />
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}> <MenuItem
<CompactModeButton /> onClick={() => {
handleCloseNavMenu();
setFaqOpen(true);
}}
>
<Button
href=""
style={{ width: "100%" }}
sx={{ color: "white", display: "flex", justifyContent: "flex-start" }}
>
FAQ
</Button>
</MenuItem> </MenuItem>
<MenuItem onClick={handleCloseNavMenu}> <MenuItem onClick={handleCloseNavMenu}>
<PlanModeButton /> <CCPButton />
</MenuItem>
<MenuItem onClick={handleCloseNavMenu}>
<AlertModeButton />
</MenuItem> </MenuItem>
</Menu> </Menu>
</Box> </Box>
@@ -146,7 +154,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 +162,132 @@ function ResponsiveAppBar() {
<UploadButton /> <UploadButton />
<DiscordButton /> <DiscordButton />
<GitHubButton /> <GitHubButton />
<CCPButton />
<SupportButton />
<SettingsButton /> <SettingsButton />
<CompactModeButton /> <Button onClick={() => setFaqOpen(true)} color="inherit">
<PlanModeButton /> FAQ
<AlertModeButton /> </Button>
<CCPButton />
</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

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

@@ -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[];
@@ -43,6 +49,7 @@ declare module "@mui/material/styles" {
export const MainGrid = () => { export const MainGrid = () => {
const { characters, updateCharacter } = useContext(CharacterContext); const { characters, updateCharacter } = 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.08)"
: "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.08)"
: "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.08)"
: "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.08)"
: "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

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

@@ -1,5 +1,5 @@
import { ColorContext, SessionContext } from "@/app/context/Context"; import { ColorContext, SessionContext } from "@/app/context/Context";
import { PI_TYPES_MAP, STORAGE_IDS, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES } from "@/const"; import { PI_TYPES_MAP, STORAGE_IDS, STORAGE_CAPACITIES, PI_PRODUCT_VOLUMES, EVE_IMAGE_URL, PI_SCHEMATICS } from "@/const";
import { planetCalculations } from "@/planets"; import { planetCalculations } from "@/planets";
import { AccessToken, PlanetWithInfo } from "@/types"; import { AccessToken, PlanetWithInfo } from "@/types";
import CloseIcon from "@mui/icons-material/Close"; import CloseIcon from "@mui/icons-material/Close";
@@ -41,6 +41,7 @@ export const PlanetTableRow = ({
character: AccessToken; character: AccessToken;
}) => { }) => {
const theme = useTheme(); const theme = useTheme();
const { showProductIcons, extractionTimeMode } = useContext(SessionContext);
const [planetRenderOpen, setPlanetRenderOpen] = useState(false); const [planetRenderOpen, setPlanetRenderOpen] = useState(false);
const [planetConfigOpen, setPlanetConfigOpen] = useState(false); const [planetConfigOpen, setPlanetConfigOpen] = useState(false);
@@ -153,6 +154,54 @@ export const PlanetTableRow = ({
}); });
}; };
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
@@ -238,11 +287,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.length === 0 &&<Typography fontSize={theme.custom.smallText}>No extractors</Typography>}
{extractors.map((e, idx) => { {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)}
@@ -250,20 +300,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>
); );
})} })}
@@ -273,37 +322,74 @@ export const PlanetTableRow = ({
<div style={{ display: "flex", flexDirection: "column" }}> <div style={{ display: "flex", flexDirection: "column" }}>
{Array.from(localProduction).map((schematic, idx) => { {Array.from(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) => ( {localImports.map((i) => {
<Typography // Find all storage facilities (including launchpads) containing this import
key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`} const storagesWithImport = storageFacilities.filter(storage =>
fontSize={theme.custom.smallText} storage.contents?.some(content => content.type_id === i.type_id)
> );
{PI_TYPES_MAP[i.type_id].name} ({i.quantity * i.factoryCount}/h)
</Typography> // Get the total amount in all storage facilities
))} const totalAmount = storagesWithImport.reduce((sum, storage) => {
const content = storage.contents?.find(content => 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
const hoursUntilDepletion = consumptionPerHour > 0 ? totalAmount / consumptionPerHour : 0;
return (
<div
key={`import-${character.character.characterId}-${planet.planet_id}-${i.type_id}`}
style={{ display: "flex", alignItems: "center" }}
>
{renderProductDisplay(i.type_id, i.quantity * i.factoryCount)}
{totalAmount > 0 && (
<Tooltip title={
<>
<div>Total: {totalAmount.toFixed(1)} units</div>
<div>Will be depleted in {hoursUntilDepletion.toFixed(1)} hours</div>
</>
}>
<Typography
fontSize={theme.custom.smallText}
color={hoursUntilDepletion < 24 ? 'error' : hoursUntilDepletion < 48 ? 'warning' : 'success'}
sx={{ ml: 1 }}
>
({hoursUntilDepletion.toFixed(1)}h)
</Typography>
</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) => ( {localExports.map((exports) => (
<Typography <div
key={`export-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`} key={`export-${character.character.characterId}-${planet.planet_id}-${exports.typeId}`}
fontSize={theme.custom.smallText} style={{ display: "flex", alignItems: "center" }}
> >
{PI_TYPES_MAP[exports.typeId].name} {renderProductDisplay(exports.typeId, exports.amount)}
</Typography> </div>
))} ))}
</div> </div>
</TableCell> </TableCell>
@@ -371,6 +457,7 @@ 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" }}>
{storageFacilities.length === 0 &&<Typography fontSize={theme.custom.smallText}>No storage</Typography>}
{storageFacilities {storageFacilities
.sort((a, b) => { .sort((a, b) => {
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; 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;

View File

@@ -36,8 +36,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 +84,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 +98,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>

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, showProductIcons, setShowProductIcons } = useContext(SessionContext);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const handleClickOpen = () => { const handleClickOpen = () => {
@@ -45,6 +47,10 @@ export const SettingsButton = () => {
} }
}; };
const handleShowProductIconsChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setShowProductIcons(event.target.checked);
};
return ( return (
<Tooltip title="Toggle settings dialog"> <Tooltip title="Toggle settings dialog">
<> <>
@@ -57,13 +63,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"
@@ -87,7 +104,6 @@ export const SettingsButton = () => {
</div> </div>
); );
})} })}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button onClick={handleClose}>Close</Button> <Button onClick={handleClose}>Close</Button>

View File

@@ -199,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,8 @@ export const SessionContext = createContext<{
}) => PlanetConfig; }) => PlanetConfig;
balanceThreshold: number; balanceThreshold: number;
setBalanceThreshold: Dispatch<SetStateAction<number>>; setBalanceThreshold: Dispatch<SetStateAction<number>>;
showProductIcons: boolean;
setShowProductIcons: (show: boolean) => void;
}>({ }>({
sessionReady: false, sessionReady: false,
refreshSession: () => {}, refreshSession: () => {},
@@ -49,6 +53,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 +68,10 @@ export const SessionContext = createContext<{
}, },
balanceThreshold: 1000, balanceThreshold: 1000,
setBalanceThreshold: () => {}, setBalanceThreshold: () => {},
showProductIcons: false,
setShowProductIcons: () => {},
}); });
export type ColorSelectionType = { export type ColorSelectionType = {
defaultColor: string; defaultColor: string;
expiredColor: string; expiredColor: string;
@@ -84,6 +93,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

@@ -30,6 +30,8 @@ const Home = () => {
undefined, undefined,
); );
const [balanceThreshold, setBalanceThreshold] = useState(1000); const [balanceThreshold, setBalanceThreshold] = useState(1000);
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);
@@ -148,6 +150,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 +229,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())
@@ -274,10 +291,14 @@ const Home = () => {
piPrices, piPrices,
alertMode, alertMode,
toggleAlertMode, toggleAlertMode,
extractionTimeMode,
toggleExtractionTimeMode,
updatePlanetConfig, updatePlanetConfig,
readPlanetConfig, readPlanetConfig,
balanceThreshold, balanceThreshold,
setBalanceThreshold, setBalanceThreshold,
showProductIcons,
setShowProductIcons,
}} }}
> >
<CharacterContext.Provider <CharacterContext.Provider

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

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

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

@@ -0,0 +1,18 @@
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
transport: {
target: 'pino-pretty',
options: {
colorize: true,
translateTime: 'SYS:standard',
ignore: 'pid,hostname',
},
},
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;
}
};