diff --git a/package-lock.json b/package-lock.json index f427042..abd5f26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -285,13 +285,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2200.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2200.3.tgz", - "integrity": "sha512-Ru+ucNkTZr98gmeaBYjq3zZwh32yGofAeB8+GJL/ZNy0x+7NzK6b+OatdzwT4l7mCWFC5vL8iYu0B4++M66Jpg==", + "version": "0.2200.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2200.4.tgz", + "integrity": "sha512-X/iEQiZ0pRmpjUt11jCM+mtOLRl4XxI9hnM0IC9aAcsm5AzRBb9WY6QIEqOSficjxC/+MI7MGwrerrcP6QN8UA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "22.0.3", + "@angular-devkit/core": "22.0.4", "rxjs": "7.8.2" }, "bin": { @@ -304,9 +304,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "22.0.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.0.3.tgz", - "integrity": "sha512-pBjo1JKwI8GbNdTo/Z0g+ZekqlTBCJWmzIC5fgGW9q5eRjl1y+5N5jlX8UAyyMCeUTTwsfpQdkAM2jyi/jcvjA==", + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-22.0.4.tgz", + "integrity": "sha512-zA2UJSMAU3su5uJTOn5ul/gCLRcw6/uIQ6EC5v/Ju/ePjgDIw9R3y3MAvWQ4Ibi/fXiq0FVxpF8hE7RUclYmJA==", "dev": true, "license": "MIT", "dependencies": { @@ -332,13 +332,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "22.0.3", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.0.3.tgz", - "integrity": "sha512-aIp5sQDHdhyLbeVJF/k3w079XhW91mNAo2OliZllBCjoYhkIXNnWECOx5y2nXtCChyFJA2+ZgNST7NIDvtz1/w==", + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-22.0.4.tgz", + "integrity": "sha512-VRxL1hD/Q3TQglM6EfQ0ksAW4OIvtKyZgtaUpyGsJRfD6tGmLFn7MDnmyyq1ceLX/Clq+3tzH/wN0tF6rcE0jA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "22.0.3", + "@angular-devkit/core": "22.0.4", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.4.0", @@ -351,9 +351,9 @@ } }, "node_modules/@angular/animations": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-22.0.2.tgz", - "integrity": "sha512-l9lVG9k+baFMWXNsFUxwmxQaUZMkpkTn3vRpa1hs/vABzT/KnaDeweDtvvkS0NS1RYJenoxhONlMNEWuJ4VR1A==", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-22.0.3.tgz", + "integrity": "sha512-j6N7s/tffYoTch++g45eKBp0pI0vsYsT1veyrALsTn732qa1E6rMsaiL2+L2s2PkHzkZosNMW1U3e0gRv6auzA==", "deprecated": "@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.", "license": "MIT", "dependencies": { @@ -363,18 +363,18 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/core": "22.0.2" + "@angular/core": "22.0.3" } }, "node_modules/@angular/build": { - "version": "22.0.3", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.0.3.tgz", - "integrity": "sha512-pwFDRCp+r8JK+fCtScPldizcS75wSpn3u/4goDf2FRa4Y9wzTvq6T0XpFHqdpgq6HcIlIZWwAqqW6XqEM9/pKQ==", + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-22.0.4.tgz", + "integrity": "sha512-hst/KhP5mMPajY32l7qmyW/5fuqrMHCjHEeNhMMNqP82+j8jPBFfMEL26AH9w2kIIdKGpC3WKN5JgLotyFD7tQ==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2200.3", + "@angular-devkit/architect": "0.2200.4", "@babel/core": "7.29.0", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -390,7 +390,7 @@ "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.1", "picomatch": "4.0.4", - "piscina": "5.1.4", + "piscina": "5.2.0", "rollup": "4.60.2", "sass": "1.99.0", "semver": "7.7.4", @@ -415,7 +415,7 @@ "@angular/platform-browser": "^22.0.0", "@angular/platform-server": "^22.0.0", "@angular/service-worker": "^22.0.0", - "@angular/ssr": "^22.0.3", + "@angular/ssr": "^22.0.4", "istanbul-lib-instrument": "^6.0.0", "karma": "^6.4.0", "less": "^4.2.0", @@ -485,19 +485,19 @@ } }, "node_modules/@angular/cli": { - "version": "22.0.3", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.0.3.tgz", - "integrity": "sha512-YgFzfQu3Il6Aka8IdH4pk7faieICaca5Wklke0jMTKBUxzLGWw82X7+J/Lox7FERb6LHtxiHpa6ttXqerCZvgg==", + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-22.0.4.tgz", + "integrity": "sha512-3eJy6VoNlgskKFzvqy3AJsYXFRhSBLLGObF2iTpJymsukuxWUen7hlVVVWrO5++bW1LEgd6PTCCD5fdT2UuRiA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2200.3", - "@angular-devkit/core": "22.0.3", - "@angular-devkit/schematics": "22.0.3", + "@angular-devkit/architect": "0.2200.4", + "@angular-devkit/core": "22.0.4", + "@angular-devkit/schematics": "22.0.4", "@inquirer/prompts": "8.4.2", "@listr2/prompt-adapter-inquirer": "4.2.3", "@modelcontextprotocol/sdk": "1.29.0", - "@schematics/angular": "22.0.3", + "@schematics/angular": "22.0.4", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.52.0", "ini": "6.0.0", @@ -520,9 +520,9 @@ } }, "node_modules/@angular/common": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.0.2.tgz", - "integrity": "sha512-XSkHYRwrM54v4GZ+fg9KU1KbSIE/iQF33VXKo5zqVNKO11MnAbJ59qzyqX/5EzSeogHyBoHApprFKACsCAKm/Q==", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-22.0.3.tgz", + "integrity": "sha512-LRglsR4Xerw/vrqoEMd489fF5PMJZ3kqGAPwO1332S42lN5KXlYITCB8WJw1iIqvoBqZvlR9R8u85cLUsfDzbg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -531,14 +531,14 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/core": "22.0.2", + "@angular/core": "22.0.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.0.2.tgz", - "integrity": "sha512-5G+h/4/iCfqdTBsSgjB46Oe4oC6jXutCpFc5JYWRpnJWsbp3UfwRhwGVWIV1DBPnR8H/3QZzteRP1jINiH5+hg==", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-22.0.3.tgz", + "integrity": "sha512-tZYq4RYYGCT6enI3wlOZsG81VJkR9wGhf5kexhvffCiMHfcA9IACHu7gjvtZPZlibeKs1PY5TMoOG3/rUqJH6Q==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -548,13 +548,13 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.0.2.tgz", - "integrity": "sha512-jBGGWdbrPQhIHWUz523CLQqEh/iYWxzZt7U9y0Ocdbas4/OlHcqaERO/K4ULkxclWX8MWYQoxal/MZbYOBfXgw==", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-22.0.3.tgz", + "integrity": "sha512-TMpKn2KefhGXM1FzcLYeyTMsBbxtFJU83bYnlI0pTw7k3jBAmN6H+ydRcNhdEYr3R2Ja3ncOXGV98RWLm9ElcQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "7.29.0", + "@babel/core": "7.29.7", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^5.0.0", "convert-source-map": "^1.5.1", @@ -571,7 +571,7 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/compiler": "22.0.2", + "@angular/compiler": "22.0.3", "typescript": ">=6.0 <6.1" }, "peerDependenciesMeta": { @@ -580,10 +580,58 @@ } } }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@angular/core": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.0.2.tgz", - "integrity": "sha512-YMs6OZNeXh4tg67ePwSRN426WYvjqGdjxEwLrdOONKAruOmJAzW/Tqe328k/4SHfdbJTR87GPpRi5FzVP43DRA==", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-22.0.3.tgz", + "integrity": "sha512-u6bYkPBB9PfYyyQ29JiytYbTqHO0lhigkkSVFoAT0WXu3R0ohycjYJooxJbHNBivWkQsASwv/k125Wb3SQoL2g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -592,7 +640,7 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/compiler": "22.0.2", + "@angular/compiler": "22.0.3", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -606,9 +654,9 @@ } }, "node_modules/@angular/forms": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.0.2.tgz", - "integrity": "sha512-k2WhkE8Of8/JRYEojSgfygiXbP6I7f/yZ/jgJzFGRC1FlF5w5erQMFx8KPg1J5CRE8kYPzW8rM4tSVCq7AaDUg==", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-22.0.3.tgz", + "integrity": "sha512-AhZEKvOw+5cqS3j1hjM0jbrWEg4xUg/l0h7yDpWAsgenWFutATGzeeRvl9dJ3HPh2Ga6leIraDi0Q/+YLCDE/A==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -619,9 +667,9 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "22.0.2", - "@angular/core": "22.0.2", - "@angular/platform-browser": "22.0.2", + "@angular/common": "22.0.3", + "@angular/core": "22.0.3", + "@angular/platform-browser": "22.0.3", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -657,9 +705,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.2.tgz", - "integrity": "sha512-xUkpJo/Jwa7rgpoSnZs5TeuOD3SDQL+CPJrMGjHivsqWMcAqzSNnIOcbNDJRSxAYkZ9zlJ1+h39JWSUk99rRBw==", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-22.0.3.tgz", + "integrity": "sha512-MAOGciS6zrw4CzEH6n/Cry0tl5MtgWWYEs/IH2McMtixpun1EOr5TVn84xIELloOtNBNT4679g5rpAVm6onLAw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -668,9 +716,9 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/animations": "22.0.2", - "@angular/common": "22.0.2", - "@angular/core": "22.0.2" + "@angular/animations": "22.0.3", + "@angular/common": "22.0.3", + "@angular/core": "22.0.3" }, "peerDependenciesMeta": { "@angular/animations": { @@ -679,9 +727,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-22.0.2.tgz", - "integrity": "sha512-5jDZzbesBBPCt41oq166B23TCW4ue9ZJyX4KlSRpGP/x8fjPGF22+AKASU6OPRnCNmmUsNk8DpenaBj+eFg/Sw==", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-22.0.3.tgz", + "integrity": "sha512-3tAPLVfOLXAwoZQR8agPOrs42J9R/zNrE1u9PnHdT+DFu2weQjRqRGGKz0iyAbLa9ljPoxUFFG9WFoEGj2Z0HA==", "deprecated": "@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.", "license": "MIT", "dependencies": { @@ -691,16 +739,16 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "22.0.2", - "@angular/compiler": "22.0.2", - "@angular/core": "22.0.2", - "@angular/platform-browser": "22.0.2" + "@angular/common": "22.0.3", + "@angular/compiler": "22.0.3", + "@angular/core": "22.0.3", + "@angular/platform-browser": "22.0.3" } }, "node_modules/@angular/router": { - "version": "22.0.2", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.0.2.tgz", - "integrity": "sha512-uiYlcbOyBliFq1v7O3nMyZtM8scDBurjk4AU2wEPWxSVAXuEjyfnAvowyPzVzGYAEKrsYtcg2TWSsQraqHUbnA==", + "version": "22.0.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-22.0.3.tgz", + "integrity": "sha512-wkD7axjX43fIbhJKLy83O/XnJI1LO/C6aXdVFE+D6dVWCzwI8wL+cqHk9ovW9TsLOtxWvp6DE7emcc+54vlshw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -709,9 +757,9 @@ "node": "^22.22.3 || ^24.15.0 || >=26.0.0" }, "peerDependencies": { - "@angular/common": "22.0.2", - "@angular/core": "22.0.2", - "@angular/platform-browser": "22.0.2", + "@angular/common": "22.0.3", + "@angular/core": "22.0.3", + "@angular/platform-browser": "22.0.3", "rxjs": "^6.5.3 || ^7.4.0" } }, @@ -1113,9 +1161,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", "dev": true, "funding": [ { @@ -1157,9 +1205,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.8", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", - "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", "dev": true, "funding": [ { @@ -1173,7 +1221,7 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.2", + "@csstools/color-helpers": "^6.1.0", "@csstools/css-calc": "^3.2.1" }, "engines": { @@ -3708,14 +3756,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "22.0.3", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.0.3.tgz", - "integrity": "sha512-iAUqIoRcK1CCHDm5E4Q1SI7rpVtsHJ+0qv5ll72wV3C1eCNdeDuGV0lX7PXEEkwd4y//s6yqI9o7f6VZZd6Fbw==", + "version": "22.0.4", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-22.0.4.tgz", + "integrity": "sha512-P3V3tkqIR+n0GJSv0ibf34/zMtKbFp6kaTjBe5cm/RyXuHbmdaPYjk8PNphkGMypDtWCof1RtNnW/hl832Wnew==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "22.0.3", - "@angular-devkit/schematics": "22.0.3", + "@angular-devkit/core": "22.0.4", + "@angular-devkit/schematics": "22.0.4", "jsonc-parser": "3.3.1", "typescript": "6.0.3" }, @@ -4628,9 +4676,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "version": "10.5.2", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.2.tgz", + "integrity": "sha512-rD5t5DwOjJdmSORcTq64j8MawTC+tbQ+HHqjR4NDumamy/ambn1UJrlKL+KdwujWxMkFjPM3pPHOEA9tl4767Q==", "dev": true, "funding": [ { @@ -4648,8 +4696,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", + "browserslist": "^4.28.4", + "caniuse-lite": "^1.0.30001799", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -4675,9 +4723,9 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.10.38", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", - "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "version": "2.10.40", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz", + "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4778,9 +4826,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz", + "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==", "dev": true, "funding": [ { @@ -4798,10 +4846,10 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", + "baseline-browser-mapping": "^2.10.38", + "caniuse-lite": "^1.0.30001799", + "electron-to-chromium": "^1.5.376", + "node-releases": "^2.0.48", "update-browserslist-db": "^1.2.3" }, "bin": { @@ -4936,9 +4984,9 @@ } }, "node_modules/chardet": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", - "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", "dev": true, "license": "MIT" }, @@ -5385,9 +5433,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.376", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", - "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "version": "1.5.379", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz", + "integrity": "sha512-v/qV5aV5EUA2pGilzUCq5/eyOloZAqDZBu9UMBIzgPpLlprjSR6zswsWBTv0KpqxLGUAZEwhO95ZCt7srymNVA==", "dev": true, "license": "ISC" }, @@ -5608,9 +5656,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6004,9 +6052,9 @@ } }, "node_modules/hono": { - "version": "4.12.26", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", - "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", + "version": "4.12.27", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.27.tgz", + "integrity": "sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==", "dev": true, "license": "MIT", "engines": { @@ -6179,9 +6227,9 @@ } }, "node_modules/immutable": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.6.tgz", - "integrity": "sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==", + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.8.tgz", + "integrity": "sha512-TM5YqrGeTsVIPPpILzeqZ8D2Zc2TvNgSDi88zPF2a4cyqQdWV/wVWBDRDbNzzrLeRWScrFcOX9lW2iX6GOtUDw==", "dev": true, "license": "MIT" }, @@ -7240,9 +7288,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.13", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz", - "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", "dev": true, "funding": [ { @@ -7354,9 +7402,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.48", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", - "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "version": "2.0.50", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz", + "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==", "dev": true, "license": "MIT", "engines": { @@ -7797,9 +7845,9 @@ } }, "node_modules/piscina": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.1.4.tgz", - "integrity": "sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", + "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", "dev": true, "license": "MIT", "engines": { @@ -7940,13 +7988,14 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.15.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", + "integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -7956,13 +8005,17 @@ } }, "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/raw-body": { @@ -8628,9 +8681,9 @@ } }, "node_modules/tar": { - "version": "7.5.16", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz", - "integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==", + "version": "7.5.17", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.17.tgz", + "integrity": "sha512-wPEBwzapC+2PaTYPH6e2L+cNOEE227S47wUYFqlegcs8zlLLmeb9Fcff1HVZY4Fwku/1Eyv38n7GYwB2aaS71g==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -8699,22 +8752,22 @@ } }, "node_modules/tldts": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", - "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.4.tgz", + "integrity": "sha512-kFXFK7O4WPextIUAOk8qtnw9dxR9UIXP9CjuH1cTBVBZMDeQcUPgr/IazGiw1B0Yiw5L75gHLWeW4iD793r90g==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.3" + "tldts-core": "^7.4.4" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.3", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", - "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.4.tgz", + "integrity": "sha512-vwVLJVvvpslm7vqAH7+XNj/neA/Ynq7DT2EEcMuwc5YzN5XaMyRAqxwU+uX3azZ1FQtB2gvrvnLnAEkvYlVdfg==", "dev": true, "license": "MIT" }, diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fd17859..049202a 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -688,6 +688,16 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "chrono-tz" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3" +dependencies = [ + "chrono", + "phf 0.12.1", +] + [[package]] name = "cipher" version = "0.4.4" @@ -914,7 +924,7 @@ dependencies = [ "cssparser-macros", "dtoa-short", "itoa", - "phf", + "phf 0.13.1", "smallvec", ] @@ -3387,6 +3397,15 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" +dependencies = [ + "phf_shared 0.12.1", +] + [[package]] name = "phf" version = "0.13.1" @@ -3394,7 +3413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" dependencies = [ "phf_macros", - "phf_shared", + "phf_shared 0.13.1", "serde", ] @@ -3405,7 +3424,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.13.1", ] [[package]] @@ -3415,7 +3434,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" dependencies = [ "fastrand", - "phf_shared", + "phf_shared 0.13.1", ] [[package]] @@ -3425,12 +3444,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.13.1", "proc-macro2", "quote", "syn 2.0.118", ] +[[package]] +name = "phf_shared" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" +dependencies = [ + "siphasher", +] + [[package]] name = "phf_shared" version = "0.13.1" @@ -4209,7 +4237,7 @@ dependencies = [ "derive_more", "log", "new_debug_unreachable", - "phf", + "phf 0.13.1", "phf_codegen", "precomputed-hash", "rustc-hash", @@ -4516,6 +4544,7 @@ version = "1.2.3" dependencies = [ "apple-native-keyring-store", "chrono", + "chrono-tz", "diesel", "diesel_migrations", "git-version", @@ -4844,7 +4873,7 @@ checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared", + "phf_shared 0.13.1", "precomputed-hash", ] @@ -4855,7 +4884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" dependencies = [ "phf_generator", - "phf_shared", + "phf_shared 0.13.1", "proc-macro2", "quote", ] @@ -5353,7 +5382,7 @@ dependencies = [ "json-patch", "log", "memchr", - "phf", + "phf 0.13.1", "plist", "proc-macro2", "quote", @@ -6135,7 +6164,7 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" dependencies = [ - "phf", + "phf 0.13.1", "phf_codegen", "string_cache", "string_cache_codegen", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5308987..9303a22 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -36,6 +36,7 @@ time = "0.3.37" tokio = { version = "1.47.1", features = ["sync"] } tokio-stream = "0.1.17" uuid = { version = "1.0", features = ["v4"] } +chrono-tz = "0.10.4" [target.'cfg(target_os = "windows")'.dependencies] windows-native-keyring-store = "1.0" diff --git a/src-tauri/src/commands/electricity.rs b/src-tauri/src/commands/electricity.rs index 8ce4796..e0a2fda 100644 --- a/src-tauri/src/commands/electricity.rs +++ b/src-tauri/src/commands/electricity.rs @@ -19,6 +19,7 @@ use super::ApiError; #[derive(Serialize, PartialEq, Debug)] pub struct ElectricityConsumption { + #[serde(serialize_with = "crate::serde_utils::serialize_naive_as_utc")] pub timestamp: NaiveDateTime, pub value: f64, } diff --git a/src-tauri/src/commands/gas.rs b/src-tauri/src/commands/gas.rs index 16fc4c4..29c5d41 100644 --- a/src-tauri/src/commands/gas.rs +++ b/src-tauri/src/commands/gas.rs @@ -18,6 +18,7 @@ use crate::{ #[derive(Serialize, PartialEq, Debug)] pub struct GasConsumption { + #[serde(serialize_with = "crate::serde_utils::serialize_naive_as_utc")] pub timestamp: NaiveDateTime, pub value: f64, } diff --git a/src-tauri/src/data/consumption.rs b/src-tauri/src/data/consumption.rs index 6ad4228..1d0689c 100644 --- a/src-tauri/src/data/consumption.rs +++ b/src-tauri/src/data/consumption.rs @@ -9,6 +9,7 @@ use diesel::{prelude::*, upsert::excluded}; use log::error; use crate::schema::{electricity_consumption, gas_consumption}; +use crate::utils::london_midnight_as_utc; use super::RepositoryError; @@ -130,8 +131,8 @@ impl ConsumptionRepository(&mut *conn)?) } @@ -145,8 +146,8 @@ impl ConsumptionRepository("DATE(timestamp)"), sql::("COALESCE(SUM(energy_consumption_kwh), 0.0)"), @@ -166,8 +167,8 @@ impl ConsumptionRepository("DATE(timestamp, 'start of month')"), sql::("COALESCE(SUM(energy_consumption_kwh), 0.0)"), @@ -240,8 +241,8 @@ impl ConsumptionRepository let mut conn = self.get_connection()?; Ok(gas_consumption - .filter(timestamp.ge(NaiveDateTime::from(start))) - .filter(timestamp.lt(NaiveDateTime::from(end))) + .filter(timestamp.ge(london_midnight_as_utc(&start))) + .filter(timestamp.lt(london_midnight_as_utc(&end))) .filter(energy_consumption_kwh.ne(ENERGY_CONSUMPTION_KWH_ERROR_CODE)) .load::(&mut *conn)?) } @@ -256,8 +257,8 @@ impl ConsumptionRepository let mut conn = self.get_connection()?; Ok(gas_consumption - .filter(timestamp.ge(NaiveDateTime::from(start))) - .filter(timestamp.lt(NaiveDateTime::from(end))) + .filter(timestamp.ge(london_midnight_as_utc(&start))) + .filter(timestamp.lt(london_midnight_as_utc(&end))) .filter(energy_consumption_kwh.ne(ENERGY_CONSUMPTION_KWH_ERROR_CODE)) .select(( sql::("DATE(timestamp)"), @@ -278,8 +279,8 @@ impl ConsumptionRepository let mut conn = self.get_connection()?; Ok(gas_consumption - .filter(timestamp.ge(NaiveDateTime::from(start))) - .filter(timestamp.lt(NaiveDateTime::from(end))) + .filter(timestamp.ge(london_midnight_as_utc(&start))) + .filter(timestamp.lt(london_midnight_as_utc(&end))) .filter(energy_consumption_kwh.ne(ENERGY_CONSUMPTION_KWH_ERROR_CODE)) .select(( sql::("DATE(timestamp, 'start of month')"), diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1f61f2e..202a4a0 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -35,6 +35,7 @@ mod download; mod mqtt; mod retry; mod schema; +mod serde_utils; mod utils; struct AppState { diff --git a/src-tauri/src/serde_utils.rs b/src-tauri/src/serde_utils.rs new file mode 100644 index 0000000..c3e8d33 --- /dev/null +++ b/src-tauri/src/serde_utils.rs @@ -0,0 +1,63 @@ +use chrono::{NaiveDateTime, TimeZone, Utc}; +use serde::Serializer; + +pub fn serialize_naive_as_utc(date: &NaiveDateTime, serializer: S) -> Result +where + S: Serializer, +{ + let utc_date = Utc.from_utc_datetime(date); + serializer.serialize_str(&utc_date.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)) +} + +#[cfg(test)] +mod tests { + use super::*; + use chrono::NaiveDate; + use serde::Serialize; + + #[derive(Serialize)] + struct TestWrapper { + #[serde(serialize_with = "serialize_naive_as_utc")] + pub timestamp: NaiveDateTime, + } + + #[test] + fn test_serialize_naive_as_utc_standard() { + let naive_dt = NaiveDate::from_ymd_opt(2026, 6, 26) + .unwrap() + .and_hms_opt(10, 45, 30) + .unwrap(); + + let wrapper = TestWrapper { + timestamp: naive_dt, + }; + + let json_result = serde_json::to_string(&wrapper); + + assert!(json_result.is_ok()); + assert_eq!( + json_result.unwrap(), + r#"{"timestamp":"2026-06-26T10:45:30Z"}"# + ); + } + + #[test] + fn test_serialize_naive_as_utc_midnight() { + let naive_dt = NaiveDate::from_ymd_opt(2026, 1, 1) + .unwrap() + .and_hms_opt(0, 0, 0) + .unwrap(); + + let wrapper = TestWrapper { + timestamp: naive_dt, + }; + + let json_result = serde_json::to_string(&wrapper); + + assert!(json_result.is_ok()); + assert_eq!( + json_result.unwrap(), + r#"{"timestamp":"2026-01-01T00:00:00Z"}"# + ); + } +} diff --git a/src-tauri/src/utils.rs b/src-tauri/src/utils.rs index 06d83a4..43445b7 100644 --- a/src-tauri/src/utils.rs +++ b/src-tauri/src/utils.rs @@ -1,6 +1,7 @@ use std::sync::{Arc, Mutex}; -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime, TimeZone, Utc}; +use chrono_tz::Europe::London; use diesel::SqliteConnection; use keyring_core::Entry; use serde::{Deserialize, Serialize}; @@ -18,6 +19,16 @@ pub fn parse_iso_string_to_naive_date(iso_date_str: &str) -> Result NaiveDateTime { + let london_midnight_local = London + .from_local_datetime(&date.and_hms_opt(0, 0, 0).unwrap()) + .unwrap(); + + let london_midnight_utc = london_midnight_local.with_timezone(&Utc); + + london_midnight_utc.naive_utc() +} + pub fn emit_event(app_handle: &AppHandle, event: &str, payload: T) -> Result<(), AppError> where T: Serialize + Clone, @@ -380,6 +391,7 @@ pub async fn reset_mqtt_settings(app_handle: &AppHandle) -> Result<(), AppError> mod tests { use super::*; use chrono::NaiveDate; + use chrono::NaiveTime; fn complete_settings() -> MqttSettings { MqttSettings { @@ -466,4 +478,60 @@ mod tests { }; assert!(!settings.is_complete()); } + + #[test] + fn test_london_midnight_during_gmt_winter() { + let winter_date = NaiveDate::from_ymd_opt(2026, 1, 15).unwrap(); + + let result = london_midnight_as_utc(&winter_date); + + let expected = winter_date.and_hms_opt(0, 0, 0).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_london_midnight_during_bst_summer() { + let summer_date = NaiveDate::from_ymd_opt(2026, 6, 26).unwrap(); + + let result = london_midnight_as_utc(&summer_date); + + let expected_date = NaiveDate::from_ymd_opt(2026, 6, 25).unwrap(); + let expected_time = NaiveTime::from_hms_opt(23, 0, 0).unwrap(); + let expected = expected_date.and_time(expected_time); + + assert_eq!(result, expected); + } + + #[test] + fn test_london_midnight_on_clocks_forward_transition_day() { + let transition_date = NaiveDate::from_ymd_opt(2026, 3, 29).unwrap(); + + let result = london_midnight_as_utc(&transition_date); + + let expected = transition_date.and_hms_opt(0, 0, 0).unwrap(); + assert_eq!(result, expected); + } + + #[test] + fn test_london_midnight_on_clocks_back_transition_day() { + let transition_sunday = NaiveDate::from_ymd_opt(2026, 10, 25).unwrap(); + + let result = london_midnight_as_utc(&transition_sunday); + + let expected_date = NaiveDate::from_ymd_opt(2026, 10, 24).unwrap(); + let expected_time = NaiveTime::from_hms_opt(23, 0, 0).unwrap(); + let expected = expected_date.and_time(expected_time); + + assert_eq!(result, expected); + } + + #[test] + fn test_london_midnight_day_after_clocks_back() { + let post_transition_monday = NaiveDate::from_ymd_opt(2026, 10, 26).unwrap(); + + let result = london_midnight_as_utc(&post_transition_monday); + + let expected = post_transition_monday.and_hms_opt(0, 0, 0).unwrap(); + assert_eq!(result, expected); + } } diff --git a/src/app/components/electricity-consumption-chart/electricity-consumption-chart.component.ts b/src/app/components/electricity-consumption-chart/electricity-consumption-chart.component.ts index 3eb7a3f..424e91b 100644 --- a/src/app/components/electricity-consumption-chart/electricity-consumption-chart.component.ts +++ b/src/app/components/electricity-consumption-chart/electricity-consumption-chart.component.ts @@ -1,6 +1,10 @@ -import { parseISO } from 'date-fns'; - -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + signal, +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; @@ -191,7 +195,9 @@ export class ElectricityConsumptionChartComponent implements OnInit, OnDestroy { { label: 'Electricity', data: values.map((x) => ({ - x: x.timestamp ? new Date(parseISO(x.timestamp)) : undefined, + x: x.timestamp + ? new Date(this.dateService.parseISO(x.timestamp)) + : undefined, y: x.energyConsumptionKwh, })), }, diff --git a/src/app/components/electricity-daily-pace/electricity-daily-pace.component.ts b/src/app/components/electricity-daily-pace/electricity-daily-pace.component.ts index ec90987..07c62b6 100644 --- a/src/app/components/electricity-daily-pace/electricity-daily-pace.component.ts +++ b/src/app/components/electricity-daily-pace/electricity-daily-pace.component.ts @@ -1,5 +1,4 @@ import { Chart, ChartConfiguration } from 'chart.js'; -import { addDays, format, set, startOfToday } from 'date-fns'; import { Component, @@ -18,6 +17,7 @@ import { MatInputModule } from '@angular/material/input'; import { UnlistenFn, listen } from '@tauri-apps/api/event'; +import { DateService } from '../../services/date/date.service'; import { ElectricityConsumption, ElectricityConsumptionService, @@ -48,14 +48,16 @@ type MyChart = ChartConfiguration<'line', ChartPoint[]>; styleUrl: './electricity-daily-pace.component.scss', }) export class ElectricityDailyPaceComponent implements OnDestroy { - private readonly dateToNormalizeTo = startOfToday(); + private readonly dateService = inject(DateService); + + private readonly dateToNormalizeTo = this.dateService.startOfToday(); private readonly electricityConsumptionService = inject( ElectricityConsumptionService, ); private readonly inputParams = signal({ - date: addDays(startOfToday(), -1), + date: this.dateService.addDays(this.dateService.startOfToday(), -1), }); protected readonly chartInstance = signal(undefined); @@ -144,7 +146,7 @@ export class ElectricityDailyPaceComponent implements OnDestroy { this.createCumulativeSeriesFromRawConsumption(data), ); - const label = format(date, 'eee d MMM'); + const label = this.dateService.format(date, 'eee d MMM'); return { label, data: cumulativeSeries }; })(), this.getDatasetForOtherDay(-1, date), @@ -155,9 +157,9 @@ export class ElectricityDailyPaceComponent implements OnDestroy { } private async getDatasetForOtherDay(dayDelta: number, referenceDate: Date) { - const otherDate = addDays(referenceDate, dayDelta); + const otherDate = this.dateService.addDays(referenceDate, dayDelta); - const label = format(otherDate, 'eee d MMM'); + const label = this.dateService.format(otherDate, 'eee d MMM'); const nextData = await this.electricityConsumptionService.getRawElectricityConsumption( @@ -237,7 +239,9 @@ export class ElectricityDailyPaceComponent implements OnDestroy { return { todayProgressBox: { type: 'box', - xMin: this.normalizeTimestamp(startOfToday()).getTime(), + xMin: this.normalizeTimestamp( + this.dateService.startOfToday(), + ).getTime(), xMax: now.getTime(), yMin: 0, @@ -271,7 +275,7 @@ export class ElectricityDailyPaceComponent implements OnDestroy { } private normalizeTimestamp(date: Date): Date { - return set(this.dateToNormalizeTo, { + return this.dateService.set(this.dateToNormalizeTo, { hours: date.getHours(), minutes: date.getMinutes(), seconds: date.getSeconds(), diff --git a/src/app/components/gas-consumption-chart/gas-consumption-chart.component.ts b/src/app/components/gas-consumption-chart/gas-consumption-chart.component.ts index 13ea0f2..33fc10e 100644 --- a/src/app/components/gas-consumption-chart/gas-consumption-chart.component.ts +++ b/src/app/components/gas-consumption-chart/gas-consumption-chart.component.ts @@ -1,6 +1,10 @@ -import { parseISO } from 'date-fns'; - -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + signal, +} from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; @@ -181,7 +185,9 @@ export class GasConsumptionChartComponent implements OnInit, OnDestroy { { label: 'Gas', data: values.map((x) => ({ - x: x.timestamp ? new Date(parseISO(x.timestamp)) : undefined, + x: x.timestamp + ? new Date(this.dateService.parseISO(x.timestamp)) + : undefined, y: x.energyConsumptionKwh, })), }, diff --git a/src/app/components/status-bar/status-bar.component.ts b/src/app/components/status-bar/status-bar.component.ts index 1c3e77c..26bfddf 100644 --- a/src/app/components/status-bar/status-bar.component.ts +++ b/src/app/components/status-bar/status-bar.component.ts @@ -1,7 +1,11 @@ -import { format } from 'date-fns'; - import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + OnDestroy, + OnInit, + inject, +} from '@angular/core'; import { MatIconModule } from '@angular/material/icon'; import { BehaviorSubject, Observable, ReplaySubject, from } from 'rxjs'; @@ -9,6 +13,7 @@ import { BehaviorSubject, Observable, ReplaySubject, from } from 'rxjs'; import { invoke } from '@tauri-apps/api/core'; import { UnlistenFn, listen } from '@tauri-apps/api/event'; +import { DateService } from '../../services/date/date.service'; import { DataDownloadingComponent } from '../data-downloading/data-downloading.component'; @Component({ @@ -24,6 +29,8 @@ import { DataDownloadingComponent } from '../data-downloading/data-downloading.c changeDetection: ChangeDetectionStrategy.OnPush, }) export class StatusBarComponent implements OnInit, OnDestroy { + private readonly dateService = inject(DateService); + public isDownloading$ = new BehaviorSubject(false); protected electricityPower$: Observable; @@ -64,7 +71,7 @@ export class StatusBarComponent implements OnInit, OnDestroy { this.electricityUpdateReceivedSubject.next(true); const lastUpdated = new Date(message.payload.electricitymeter.timestamp); - const friendlyTimestamp = format( + const friendlyTimestamp = this.dateService.format( lastUpdated, "dd/MM/yyyy 'at' h:mm:ss a", ); @@ -100,7 +107,7 @@ export class StatusBarComponent implements OnInit, OnDestroy { this.gasUpdateReceivedSubject.next(true); const lastUpdated = new Date(message.payload.gasmeter.timestamp); - const friendlyTimestamp = format( + const friendlyTimestamp = this.dateService.format( lastUpdated, "dd/MM/yyyy 'at' h:mm:ss a", ); diff --git a/src/app/services/date/date.service.ts b/src/app/services/date/date.service.ts index 185faaf..bb3f616 100644 --- a/src/app/services/date/date.service.ts +++ b/src/app/services/date/date.service.ts @@ -1,22 +1,28 @@ import { + DateValues, addDays, addMonths, endOfMonth, + format, formatISO, + parseISO, + set, startOfMonth, startOfToday, } from 'date-fns'; -import { Injectable } from '@angular/core'; +import { Service } from '@angular/core'; -@Injectable({ - providedIn: 'root', -}) +@Service() export class DateService { public formatISODate(date: Date): string { return formatISO(date, { format: 'extended', representation: 'date' }); } + public format(date: Date, formatStr: string): string { + return format(date, formatStr); + } + public addDays(date: Date, n: number): Date { return addDays(date, n); } @@ -36,4 +42,12 @@ export class DateService { public endOfMonth(date: Date): Date { return endOfMonth(date); } + + public set(date: Date, values: DateValues) { + return set(date, values); + } + + public parseISO(dateStr: string): Date { + return parseISO(dateStr); + } } diff --git a/src/app/services/electricity-consumption/electricity-consumption.service.ts b/src/app/services/electricity-consumption/electricity-consumption.service.ts index 583a145..c6dec98 100644 --- a/src/app/services/electricity-consumption/electricity-consumption.service.ts +++ b/src/app/services/electricity-consumption/electricity-consumption.service.ts @@ -1,5 +1,3 @@ -import { parseISO } from 'date-fns'; - import { Service, inject } from '@angular/core'; import { invoke } from '@tauri-apps/api/core'; @@ -35,7 +33,7 @@ export class ElectricityConsumptionService { ); return data.map((x) => ({ - timestamp: new Date(parseISO(x.timestamp)), + timestamp: new Date(this.dateService.parseISO(x.timestamp)), electricityConsumptionKwh: x.value, })); }