7 Commits

8 changed files with 353 additions and 212 deletions

View File

@@ -13,29 +13,29 @@
"@fontsource/roboto": "^5.1.0",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^6.1.2",
"@mui/material": "^6.1.2",
"@mui/x-charts": "^7.19.0",
"@mui/x-date-pickers": "^7.19.0",
"@mui/icons-material": "^6.1.3",
"@mui/material": "^6.1.3",
"@mui/x-charts": "^7.20.0",
"@mui/x-date-pickers": "^7.20.0",
"@types/semver": "^7.5.8",
"date-and-time": "^3.6.0",
"dayjs": "^1.11.13",
"filesize": "^10.1.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2",
"react-router-dom": "^6.27.0",
"semver": "^7.6.3"
},
"devDependencies": {
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@vitejs/plugin-react": "^4.3.2",
"eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.12",
"typescript": "^5.6.2",
"typescript": "^5.6.3",
"vite": "^5.4.8"
}
},
@@ -1153,9 +1153,9 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.2.tgz",
"integrity": "sha512-1oE4U38/TtzLWRYWEm/m70dUbpcvBx0QvDVg6NtpOmSNQC1Mbx0X/rNvYDdZnn8DIsAiVQ+SZ3am6doSswUQ4g==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.1.3.tgz",
"integrity": "sha512-ajMUgdfhTb++rwqj134Cq9f4SRN8oXUqMRnY72YBnXiXai3olJLLqETheRlq3MM8wCKrbq7g6j7iWL1VvP44VQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
@@ -1163,9 +1163,9 @@
}
},
"node_modules/@mui/icons-material": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.2.tgz",
"integrity": "sha512-7NNcjW5JoT9jHagrVbARA1o41vQY2xezDamtke+mEKKZmsJyejfRBOacSrPDfjZQ//lyhIjNKyzAwisxYJR47w==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.1.3.tgz",
"integrity": "sha512-QBQCCIMSAv6IkArTg4Hg8q2sJRhHOci8oPAlkHWFlt2ghBdy3EqyLbIELLE/bhpqhX+E/ZkPYGIUQCd5/L0owA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.6"
@@ -1178,7 +1178,7 @@
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^6.1.2",
"@mui/material": "^6.1.3",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
@@ -1189,16 +1189,16 @@
}
},
"node_modules/@mui/material": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.2.tgz",
"integrity": "sha512-5TtHeAVX9D5d2LYfB1GAUn29BcVETVsrQ76Dwb2SpAfQGW3JVy4deJCAd0RrIkI3eEUrsl0E4xuBdreszxdTTg==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.1.3.tgz",
"integrity": "sha512-loV5MBoMKLrK80JeWINmQ1A4eWoLv51O2dBPLJ260IAhupkB3Wol8lEQTEvvR2vO3o6xRHuXe1WaQEP6N3riqg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@mui/core-downloads-tracker": "^6.1.2",
"@mui/system": "^6.1.2",
"@mui/types": "^7.2.17",
"@mui/utils": "^6.1.2",
"@mui/core-downloads-tracker": "^6.1.3",
"@mui/system": "^6.1.3",
"@mui/types": "^7.2.18",
"@mui/utils": "^6.1.3",
"@popperjs/core": "^2.11.8",
"@types/react-transition-group": "^4.4.11",
"clsx": "^2.1.1",
@@ -1217,7 +1217,7 @@
"peerDependencies": {
"@emotion/react": "^11.5.0",
"@emotion/styled": "^11.3.0",
"@mui/material-pigment-css": "^6.1.2",
"@mui/material-pigment-css": "^6.1.3",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1238,13 +1238,13 @@
}
},
"node_modules/@mui/private-theming": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.2.tgz",
"integrity": "sha512-S8WcjZdNdi++8UhrrY8Lton5h/suRiQexvdTfdcPAlbajlvgM+kx+uJstuVIEyTb3gMkxzIZep87knZ0tqcR0g==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.1.3.tgz",
"integrity": "sha512-XK5OYCM0x7gxWb/WBEySstBmn+dE3YKX7U7jeBRLm6vHU5fGUd7GiJWRirpivHjOK9mRH6E1MPIVd+ze5vguKQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@mui/utils": "^6.1.2",
"@mui/utils": "^6.1.3",
"prop-types": "^15.8.1"
},
"engines": {
@@ -1265,13 +1265,14 @@
}
},
"node_modules/@mui/styled-engine": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.2.tgz",
"integrity": "sha512-uKOfWkR23X39xj7th2nyTcCHqInTAXtUnqD3T5qRVdJcOPvu1rlgTleTwJC/FJvWZJBU6ieuTWDhbcx5SNViHQ==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.1.3.tgz",
"integrity": "sha512-i4yh9m+eMZE3cNERpDhVr6Wn73Yz6C7MH0eE2zZvw8d7EFkIJlCQNZd1xxGZqarD2DDq2qWHcjIOucWGhxACtA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@emotion/cache": "^11.13.1",
"@emotion/serialize": "^1.3.2",
"@emotion/sheet": "^1.4.0",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -1298,16 +1299,16 @@
}
},
"node_modules/@mui/system": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.2.tgz",
"integrity": "sha512-mzW7F1ZMIYS1aLON48Nrk9c65OrVEVQ+R4lUcTWs1lCSul0VGK23eo4dmY0NX5PS7Oe4xz3P5B9tQZZ7SYgxcg==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-6.1.3.tgz",
"integrity": "sha512-ILaD9UsLTBLjMcep3OumJMXh1PYr7aqnkHm/L47bH46+YmSL1zWAX6tWG8swEQROzW2GvYluEMp5FreoxOOC6w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@mui/private-theming": "^6.1.2",
"@mui/styled-engine": "^6.1.2",
"@mui/types": "^7.2.17",
"@mui/utils": "^6.1.2",
"@mui/private-theming": "^6.1.3",
"@mui/styled-engine": "^6.1.3",
"@mui/types": "^7.2.18",
"@mui/utils": "^6.1.3",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"prop-types": "^15.8.1"
@@ -1338,9 +1339,9 @@
}
},
"node_modules/@mui/types": {
"version": "7.2.17",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.17.tgz",
"integrity": "sha512-oyumoJgB6jDV8JFzRqjBo2daUuHpzDjoO/e3IrRhhHo/FxJlaVhET6mcNrKHUq2E+R+q3ql0qAtvQ4rfWHhAeQ==",
"version": "7.2.18",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.18.tgz",
"integrity": "sha512-uvK9dWeyCJl/3ocVnTOS6nlji/Knj8/tVqVX03UVTpdmTJYu/s4jtDd9Kvv0nRGE0CUSNW1UYAci7PYypjealg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1352,13 +1353,13 @@
}
},
"node_modules/@mui/utils": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.2.tgz",
"integrity": "sha512-6+B1YZ8cCBWD1fc3RjqpclF9UA0MLUiuXhyCO+XowD/Z2ku5IlxeEhHHlgglyBWFGMu4kib4YU3CDsG5/zVjJQ==",
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.1.3.tgz",
"integrity": "sha512-4JBpLkjprlKjN10DGb1aiy/ii9TKbQ601uSHtAmYFAS879QZgAD7vRnv/YBE4iBbc7NXzFgbQMCOFrupXWekIA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@mui/types": "^7.2.17",
"@mui/types": "^7.2.18",
"@types/prop-types": "^15.7.13",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -1382,15 +1383,15 @@
}
},
"node_modules/@mui/x-charts": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.19.0.tgz",
"integrity": "sha512-gOiSmb+bLIoGOGs+5uzEb7sau7KmjAKJI4+hORCjHUAOefmtHdiw39gZVne1Gcah+h6BIrWMkNJHk5djcCaGsg==",
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.20.0.tgz",
"integrity": "sha512-mm3ERanuxWWc16dYLC54jqQp1CrHFSvWYvaXvhaXhWZdNrSIWNEY4inCbrDuGvl+i7/uNJgxeINw4SOtQ/BOFA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@mui/utils": "^5.16.6",
"@mui/x-charts-vendor": "7.19.0",
"@mui/x-internals": "7.18.0",
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-charts-vendor": "7.20.0",
"@mui/x-internals": "7.20.0",
"@react-spring/rafz": "^9.7.4",
"@react-spring/web": "^9.7.4",
"clsx": "^2.1.1",
@@ -1417,12 +1418,12 @@
}
},
"node_modules/@mui/x-charts-vendor": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-7.19.0.tgz",
"integrity": "sha512-idHOi6U8mmX1ezKclv2p4QBrWxQIUSS+bd7rp2PGnf7XAPtUY/X9VTZvU1mDRawtyKjyzQD+vmSbYx9piytqAg==",
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz",
"integrity": "sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg==",
"license": "MIT AND ISC",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@babel/runtime": "^7.25.7",
"@types/d3-color": "^3.1.3",
"@types/d3-delaunay": "^6.0.4",
"@types/d3-interpolate": "^3.0.4",
@@ -1439,45 +1440,15 @@
"robust-predicates": "^3.0.2"
}
},
"node_modules/@mui/x-charts/node_modules/@mui/utils": {
"version": "5.16.6",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz",
"integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/types": "^7.2.15",
"@types/prop-types": "^15.7.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^18.3.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/x-date-pickers": {
"version": "7.19.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.19.0.tgz",
"integrity": "sha512-OIQ+IxgL2Si7DP68sw1ImcHXZtAmklHcyo/oqP4HuJZ2lVnP5sJkoXrksfumL1wjWKJkecONFz3unAqViKXzCQ==",
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.20.0.tgz",
"integrity": "sha512-LnijrF8IF3r7c7sAVXRX4pDurozJSMUGAJdd5xuTT7ZPQIOp5ry0kDKqx79WAjXA/ZgjropLNt/nk15GE+6ZNw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@mui/utils": "^5.16.6",
"@mui/x-internals": "7.18.0",
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0",
"@mui/x-internals": "7.20.0",
"@types/react-transition-group": "^4.4.11",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
@@ -1535,44 +1506,14 @@
}
}
},
"node_modules/@mui/x-date-pickers/node_modules/@mui/utils": {
"version": "5.16.6",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz",
"integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/types": "^7.2.15",
"@types/prop-types": "^15.7.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^18.3.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/x-internals": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.18.0.tgz",
"integrity": "sha512-lzCHOWIR0cAIY1bGrWSprYerahbnH5C31ql/2OWCEjcngL2NAV1M6oKI2Vp4HheqzJ822c60UyWyapvyjSzY/A==",
"version": "7.20.0",
"resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.20.0.tgz",
"integrity": "sha512-ScXdEwtnxmBEq9umeusnotfeVQnnhjOZcM2ddXyIupmzeGmgDDtEcXGyTgrS/GOc91J74g81s6eJ4UCrlYZ2sg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.25.6",
"@mui/utils": "^5.16.6"
"@babel/runtime": "^7.25.7",
"@mui/utils": "^5.16.6 || ^6.0.0"
},
"engines": {
"node": ">=14.0.0"
@@ -1585,36 +1526,6 @@
"react": "^17.0.0 || ^18.0.0"
}
},
"node_modules/@mui/x-internals/node_modules/@mui/utils": {
"version": "5.16.6",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.16.6.tgz",
"integrity": "sha512-tWiQqlhxAt3KENNiSRL+DIn9H5xNVK6Jjf70x3PnfQPz1MPBdh7yyIcAyVBT9xiw7hP3SomRhPR7hzBMBCjqEA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.9",
"@mui/types": "^7.2.15",
"@types/prop-types": "^15.7.12",
"clsx": "^2.1.1",
"prop-types": "^15.8.1",
"react-is": "^18.3.1"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@types/react": "^17.0.0 || ^18.0.0",
"react": "^17.0.0 || ^18.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -1736,9 +1647,9 @@
}
},
"node_modules/@remix-run/router": {
"version": "1.19.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.2.tgz",
"integrity": "sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.20.0.tgz",
"integrity": "sha512-mUnk8rPJBI9loFDZ+YzPGdeniYK+FTmRD1TMCz7ev2SNIozyKKpnGgsxO34u6Z4z/t0ITuu7voi/AshfsGsgFg==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -2094,9 +2005,9 @@
}
},
"node_modules/@types/react-dom": {
"version": "18.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz",
"integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==",
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2927,16 +2838,16 @@
}
},
"node_modules/eslint-plugin-react-hooks": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz",
"integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==",
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.0.0.tgz",
"integrity": "sha512-hIOwI+5hYGpJEc4uPRmz2ulCjAGD/N13Lukkh8cLV0i2IRk/bdZDYjgLVHj+U9Z704kLIdIO6iueGvxNur0sgw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"peerDependencies": {
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-react-refresh": {
@@ -4104,12 +4015,12 @@
}
},
"node_modules/react-router": {
"version": "6.26.2",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.26.2.tgz",
"integrity": "sha512-tvN1iuT03kHgOFnLPfLJ8V95eijteveqdOSk+srqfePtQvqCExB8eHOYnlilbOcyJyKnYkr1vJvf7YqotAJu1A==",
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.27.0.tgz",
"integrity": "sha512-YA+HGZXz4jaAkVoYBE98VQl+nVzI+cVI2Oj/06F5ZM+0u3TgedN9Y9kmMRo2mnkSK2nCpNQn0DVob4HCsY/WLw==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.19.2"
"@remix-run/router": "1.20.0"
},
"engines": {
"node": ">=14.0.0"
@@ -4119,13 +4030,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.26.2",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.26.2.tgz",
"integrity": "sha512-z7YkaEW0Dy35T3/QKPYB1LjMK2R1fxnHO8kWpUMTBdfVzZrWOiY9a7CtN8HqdWtDUWd5FY6Dl8HFsqVwH4uOtQ==",
"version": "6.27.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.27.0.tgz",
"integrity": "sha512-+bvtFWMC0DgAFrfKXKG9Fc+BcXWRUO1aJIihbB79xaeq0v5UzfvnM5houGUm1Y461WVRcgAQ+Clh5rdb1eCx4g==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.19.2",
"react-router": "6.26.2"
"@remix-run/router": "1.20.0",
"react-router": "6.27.0"
},
"engines": {
"node": ">=14.0.0"
@@ -4465,9 +4376,9 @@
}
},
"node_modules/typescript": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -15,29 +15,29 @@
"@fontsource/roboto": "^5.1.0",
"@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1",
"@mui/icons-material": "^6.1.2",
"@mui/material": "^6.1.2",
"@mui/x-charts": "^7.19.0",
"@mui/x-date-pickers": "^7.19.0",
"@mui/icons-material": "^6.1.3",
"@mui/material": "^6.1.3",
"@mui/x-charts": "^7.20.0",
"@mui/x-date-pickers": "^7.20.0",
"@types/semver": "^7.5.8",
"date-and-time": "^3.6.0",
"dayjs": "^1.11.13",
"filesize": "^10.1.6",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2",
"react-router-dom": "^6.27.0",
"semver": "^7.6.3"
},
"devDependencies": {
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.0",
"@types/react-dom": "^18.3.1",
"@typescript-eslint/eslint-plugin": "^8.8.0",
"@typescript-eslint/parser": "^8.8.0",
"@vitejs/plugin-react": "^4.3.2",
"eslint": "^8.57.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.12",
"typescript": "^5.6.2",
"typescript": "^5.6.3",
"vite": "^5.4.8"
}
}

View File

@@ -39,3 +39,8 @@
* Interval of time (in seconds) between two synchronisations
*/
#define SYNC_TIME_INTERVAL 5
/**
* OTA download timeout (in milliseconds)
*/
#define OTA_REC_TIMEOUT 15000

View File

@@ -29,12 +29,12 @@ static void seed_ctr_drbg_context(mbedtls_entropy_context *entropy, mbedtls_ctr_
mbedtls_entropy_init(entropy);
mbedtls_ctr_drbg_init(ctr_drbg);
ESP_LOGI(TAG, "Seed Mbedtls\n");
ESP_LOGI(TAG, "Seed Mbedtls");
if ((ret = mbedtls_ctr_drbg_seed(ctr_drbg, mbedtls_entropy_func, entropy,
(const unsigned char *)pers,
strlen(pers))) != 0)
{
ESP_LOGE(TAG, " failed\n ! mbedtls_ctr_drbg_seed returned %d\n", ret);
ESP_LOGE(TAG, " failed\n ! mbedtls_ctr_drbg_seed returned %d", ret);
reboot();
}
}
@@ -54,7 +54,7 @@ bool crypto_gen_priv_key()
mbedtls_ctr_drbg_context ctr_drbg;
seed_ctr_drbg_context(&entropy, &ctr_drbg);
ESP_LOGI(TAG, "PK info from type\n");
ESP_LOGI(TAG, "PK info from type");
if ((ret = mbedtls_pk_setup(&key, mbedtls_pk_info_from_type(MBEDTLS_PK_ECKEY))) != 0)
{
ESP_LOGE(TAG, " failed\n ! mbedtls_pk_setup returned -0x%04x", (unsigned int)-ret);
@@ -62,7 +62,7 @@ bool crypto_gen_priv_key()
}
// Generate private key
ESP_LOGI(TAG, "Generate private key\n");
ESP_LOGI(TAG, "Generate private key");
ret = mbedtls_ecp_gen_key(ECPARAMS,
mbedtls_pk_ec(key),
mbedtls_ctr_drbg_random, &ctr_drbg);
@@ -74,7 +74,7 @@ bool crypto_gen_priv_key()
}
// Export private key
ESP_LOGI(TAG, "Export private key\n");
ESP_LOGI(TAG, "Export private key");
unsigned char *key_buff = malloc(PRV_KEY_DER_MAX_BYTES);
if ((ret = mbedtls_pk_write_key_der(&key, key_buff, PRV_KEY_DER_MAX_BYTES)) < 1)
{
@@ -108,7 +108,7 @@ void crypto_print_priv_key()
mbedtls_ctr_drbg_context ctr_drbg;
seed_ctr_drbg_context(&entropy, &ctr_drbg);
ESP_LOGI(TAG, "Parse private key (len = %d)\n", key_len);
ESP_LOGI(TAG, "Parse private key (len = %d)", key_len);
if ((ret = mbedtls_pk_parse_key(&key, key_buff, key_len, NULL, 0, mbedtls_ctr_drbg_random, &ctr_drbg)) != 0)
{
ESP_LOGE(TAG, " failed\n ! mbedtls_pk_parse_key returned -0x%04x",
@@ -117,7 +117,7 @@ void crypto_print_priv_key()
}
free(key_buff);
ESP_LOGI(TAG, "Show private key\n");
ESP_LOGI(TAG, "Show private key");
unsigned char *out = malloc(16000);
memset(out, 0, 16000);
if ((ret = mbedtls_pk_write_key_pem(&key, out, 16000)) != 0)
@@ -153,7 +153,7 @@ static bool crypto_get_priv_key_mpi(mbedtls_mpi *dst)
mbedtls_ctr_drbg_context ctr_drbg;
seed_ctr_drbg_context(&entropy, &ctr_drbg);
ESP_LOGI(TAG, "Parse private key (len = %d)\n", key_len);
ESP_LOGI(TAG, "Parse private key (len = %d)", key_len);
if ((ret = mbedtls_pk_parse_key(&key, key_buff, key_len, NULL, 0, mbedtls_ctr_drbg_random, &ctr_drbg)) != 0)
{
ESP_LOGE(TAG, " failed\n ! mbedtls_pk_parse_key returned -0x%04x",
@@ -190,7 +190,7 @@ char *crypto_get_csr()
mbedtls_ctr_drbg_context ctr_drbg;
seed_ctr_drbg_context(&entropy, &ctr_drbg);
ESP_LOGI(TAG, "Parse private key (len = %d)\n", key_len);
ESP_LOGI(TAG, "Parse private key (len = %d)", key_len);
if ((ret = mbedtls_pk_parse_key(&key, key_buff, key_len, NULL, 0, mbedtls_ctr_drbg_random, &ctr_drbg)) != 0)
{
ESP_LOGE(TAG, " failed\n ! mbedtls_pk_parse_key returned -0x%04x",
@@ -214,7 +214,7 @@ char *crypto_get_csr()
reboot();
}
ESP_LOGI(TAG, "Sign CSR with private key\n");
ESP_LOGI(TAG, "Sign CSR with private key");
mbedtls_x509write_csr_set_key(&req, &key);
char *csr = malloc(4096);

View File

@@ -33,7 +33,7 @@ char *dev_name()
char *dev = malloc(len + strlen(DEV_PREFIX) + 1);
if (dev == NULL)
{
ESP_LOGE(TAG, "Failed to allocate memory to store dev name!\n");
ESP_LOGE(TAG, "Failed to allocate memory to store dev name!");
return NULL;
}

View File

@@ -18,7 +18,7 @@ static const char *TAG = "main";
void app_main(void)
{
esp_log_level_set("*", ESP_LOG_VERBOSE);
esp_log_level_set("*", ESP_LOG_INFO);
system_show_free_memory();
@@ -31,40 +31,40 @@ void app_main(void)
// Initialize storage
if (storage_init() == false)
{
ESP_LOGE(TAG, "Failed to init storage!\n");
ESP_LOGE(TAG, "Failed to init storage!");
reboot();
}
// Give a name to the device
if (dev_generate_name())
{
ESP_LOGI(TAG, "Generated a new device name\n");
ESP_LOGI(TAG, "Generated a new device name");
}
char *name = dev_name();
ESP_LOGI(TAG, "Dev name: %s\n", name);
ESP_LOGI(TAG, "Dev name: %s", name);
free(name);
// Generate private key, if needed
if (crypto_gen_priv_key())
{
ESP_LOGI(TAG, "Generated device private key!\n");
ESP_LOGI(TAG, "Generated device private key!");
}
ESP_LOGI(TAG, "Device private key:\n");
ESP_LOGI(TAG, "Device private key:");
crypto_print_priv_key();
// Show current private key
char *csr = crypto_get_csr();
ESP_LOGI(TAG, "Current CSR:\n%s\n", csr);
ESP_LOGI(TAG, "Current CSR:\n%s", csr);
free(csr);
// Initialize network stack
ESP_LOGI(TAG, "Initialize network\n");
ESP_LOGI(TAG, "Initialize network");
ethernet_init();
ethernet_wait_for_network();
// Get if secure origin endpoint is known
ESP_LOGI(TAG, "Check secure origin\n");
ESP_LOGI(TAG, "Check secure origin");
if (storage_get_secure_origin(NULL) == 0)
{
char *sec_ori = unsecure_api_get_secure_origin();
@@ -78,7 +78,7 @@ void app_main(void)
}
// Print secure origin endpoint for debugging purposes
ESP_LOGI(TAG, "Get secure origin\n");
ESP_LOGI(TAG, "Get secure origin");
char *sec_ori = calloc(SEC_ORIG_LEN, 1);
assert(storage_get_secure_origin(sec_ori) > 0);
ESP_LOGI(TAG, "Current secure origin: %s", sec_ori);
@@ -111,7 +111,7 @@ void app_main(void)
// Check current device enrollment status
ESP_LOGI(TAG, "Check enrollment status");
enum DevEnrollmentStatus status = secure_api_get_device_enrollment_status();
ESP_LOGI(TAG, "Current enrollment status: %d\n", status);
ESP_LOGI(TAG, "Current enrollment status: %d", status);
switch (status)
{

View File

@@ -1,7 +1,232 @@
#include "esp_log.h"
#include "esp_partition.h"
#include "esp_ota_ops.h"
#include "esp_http_client.h"
#include "esp_app_format.h"
#include "constants.h"
#include "ota.h"
#include "storage.h"
#include "secure_api.h"
#include <string.h>
const char *TAG = "ota";
#define BUFF_SIZE 1024
static void http_cleanup(esp_http_client_handle_t client)
{
esp_http_client_close(client);
esp_http_client_cleanup(client);
}
bool ota_perform_update(const char *version)
{
// TODO
const esp_partition_t *configured = esp_ota_get_boot_partition();
const esp_partition_t *running = esp_ota_get_running_partition();
if (configured != running)
{
ESP_LOGW(TAG, "Configured OTA boot partition at offset 0x%08" PRIx32 ", but running from offset 0x%08" PRIx32,
configured->address, running->address);
ESP_LOGW(TAG, "(This can happen if either the OTA boot data or preferred boot image become corrupted somehow.)");
}
ESP_LOGI(TAG, "Running partition type %d subtype %d (offset 0x%08" PRIx32 ")",
running->type, running->subtype, running->address);
// Determine firmware download URL
char *update_url = calloc(256, 1);
assert(update_url != NULL);
assert(storage_get_secure_origin(update_url) > 0);
strcat(update_url, "/devices_api/ota/Wt32-Eth01/");
strcat(update_url, version);
ESP_LOGI(TAG, "Firmware URL: %s", update_url);
char *root_ca = calloc(1, ROOT_CA_MAX_BYTES);
assert(root_ca);
assert(storage_get_root_ca(root_ca) > 0);
esp_http_client_config_t config = {
.url = update_url,
.cert_pem = root_ca,
.timeout_ms = OTA_REC_TIMEOUT,
.keep_alive_enable = true,
};
esp_http_client_handle_t client = esp_http_client_init(&config);
if (client == NULL)
{
ESP_LOGE(TAG, "Failed to initialise HTTP connection");
free(update_url);
free(root_ca);
return false;
}
int err = esp_http_client_open(client, 0);
free(update_url);
free(root_ca);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err));
esp_http_client_cleanup(client);
return false;
}
esp_http_client_fetch_headers(client);
const esp_partition_t *update_partition = esp_ota_get_next_update_partition(NULL);
assert(update_partition != NULL);
ESP_LOGI(TAG, "Writing to partition subtype %d at offset 0x%" PRIx32,
update_partition->subtype, update_partition->address);
// OTA update loop
int binary_file_length = 0;
esp_ota_handle_t update_handle = 0;
/*deal with all receive packet*/
bool image_header_was_checked = false;
char *ota_write_data = calloc(BUFF_SIZE + 1, 1);
while (1)
{
int data_read = esp_http_client_read(client, ota_write_data, BUFF_SIZE);
if (data_read < 0)
{
ESP_LOGE(TAG, "Error: SSL data read error");
http_cleanup(client);
free(ota_write_data);
return false;
}
if (data_read == 0)
{
/*
* As esp_http_client_read never returns negative error code, we rely on
* `errno` to check for underlying transport connectivity closure if any
*/
if (errno == ECONNRESET || errno == ENOTCONN)
{
ESP_LOGE(TAG, "Connection closed, errno = %d", errno);
break;
}
if (esp_http_client_is_complete_data_received(client) == true)
{
ESP_LOGI(TAG, "Connection closed");
break;
}
// No data received yet
continue;
}
if (image_header_was_checked == false)
{
esp_app_desc_t new_app_info;
if (data_read > sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t) + sizeof(esp_app_desc_t))
{
// check current version with downloading
memcpy(&new_app_info, &ota_write_data[sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t)], sizeof(esp_app_desc_t));
ESP_LOGI(TAG, "New firmware version: %s", new_app_info.version);
esp_app_desc_t running_app_info;
if (esp_ota_get_partition_description(running, &running_app_info) == ESP_OK)
{
ESP_LOGI(TAG, "Running firmware version: %s", running_app_info.version);
}
const esp_partition_t *last_invalid_app = esp_ota_get_last_invalid_partition();
esp_app_desc_t invalid_app_info;
if (esp_ota_get_partition_description(last_invalid_app, &invalid_app_info) == ESP_OK)
{
ESP_LOGI(TAG, "Last invalid firmware version: %s", invalid_app_info.version);
}
// check current version with last invalid partition
if (last_invalid_app != NULL)
{
if (memcmp(invalid_app_info.version, new_app_info.version, sizeof(new_app_info.version)) == 0)
{
ESP_LOGW(TAG, "New version is the same as invalid version.");
ESP_LOGW(TAG, "Previously, there was an attempt to launch the firmware with %s version, but it failed.", invalid_app_info.version);
ESP_LOGW(TAG, "The firmware has been rolled back to the previous version.");
http_cleanup(client);
free(ota_write_data);
secure_api_report_log_message(Error, "New version is the same as last invalid version. Could not perform update!");
return false;
}
}
image_header_was_checked = true;
err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &update_handle);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "esp_ota_begin failed (%s)", esp_err_to_name(err));
http_cleanup(client);
free(ota_write_data);
esp_ota_abort(update_handle);
return false;
}
ESP_LOGI(TAG, "esp_ota_begin succeeded");
}
else
{
ESP_LOGE(TAG, "received package is not fit len");
http_cleanup(client);
free(ota_write_data);
esp_ota_abort(update_handle);
return false;
}
}
err = esp_ota_write(update_handle, (const void *)ota_write_data, data_read);
if (err != ESP_OK)
{
http_cleanup(client);
free(ota_write_data);
esp_ota_abort(update_handle);
return false;
}
binary_file_length += data_read;
ESP_LOGD(TAG, "Written image length %d", binary_file_length);
}
free(ota_write_data);
ESP_LOGI(TAG, "Total Write binary data length: %d", binary_file_length);
if (esp_http_client_is_complete_data_received(client) != true)
{
ESP_LOGE(TAG, "Error in receiving complete file");
http_cleanup(client);
esp_ota_abort(update_handle);
return false;
}
err = esp_ota_end(update_handle);
if (err != ESP_OK)
{
if (err == ESP_ERR_OTA_VALIDATE_FAILED)
{
ESP_LOGE(TAG, "Image validation failed, image is corrupted");
}
else
{
ESP_LOGE(TAG, "esp_ota_end failed (%s)!", esp_err_to_name(err));
}
http_cleanup(client);
return false;
}
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (%s)!", esp_err_to_name(err));
http_cleanup(client);
return false;
}
ESP_LOGI(TAG, "End of OTA procedure!");
return true;
}

View File

@@ -20,7 +20,7 @@ bool storage_init()
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND)
{
ESP_LOGI(TAG, "Need to reset storage\n");
ESP_LOGI(TAG, "Need to reset storage");
// NVS partition was truncated and needs to be erased
// Retry nvs_flash_init