From ffe80b62c4fc28a7d5ff5e0351773d60acb810e0 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 6 Jun 2026 02:27:04 +0700 Subject: [PATCH] fix: harden reminders and state handling --- package-lock.json | 1404 ++++++++++++++++++--- package.json | 2 + postcss.config.js => postcss.config.mjs | 0 scripts/release.ps1 | 9 +- src/main/games/registry.test.ts | 119 ++ src/main/games/registry.ts | 3 +- src/main/ipc.ts | 23 +- src/main/meeting-detect.test.ts | 24 +- src/main/scheduler.test.ts | 15 + src/main/scheduler.ts | 5 +- src/main/store.test.ts | 113 ++ src/main/store.ts | 185 ++- src/main/validate.ts | 6 +- src/renderer/src/ReminderApp.tsx | 125 +- src/shared/ipc.ts | 2 +- src/shared/types.ts | 5 +- tailwind.config.js => tailwind.config.mjs | 0 17 files changed, 1752 insertions(+), 288 deletions(-) rename postcss.config.js => postcss.config.mjs (100%) create mode 100644 src/main/games/registry.test.ts rename tailwind.config.js => tailwind.config.mjs (100%) diff --git a/package-lock.json b/package-lock.json index b8d79e7..21a922f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "laude", - "version": "0.5.4", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "laude", - "version": "0.5.4", + "version": "0.6.0", "dependencies": { "@fontsource/bricolage-grotesque": "^5.2.10", "@fontsource/jetbrains-mono": "^5.2.8", @@ -26,10 +26,12 @@ "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "@vitejs/plugin-react": "^4.3.3", + "@vitest/coverage-v8": "^4.1.6", "autoprefixer": "^10.4.20", "electron": "^33.2.0", "electron-builder": "^25.1.8", "electron-vite": "^2.3.0", + "esbuild": "^0.28.0", "eslint": "^8.57.1", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", @@ -352,6 +354,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@develar/schema-utils": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz", @@ -773,9 +785,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", "cpu": [ "ppc64" ], @@ -786,13 +798,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", "cpu": [ "arm" ], @@ -803,13 +815,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", "cpu": [ "arm64" ], @@ -820,13 +832,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", "cpu": [ "x64" ], @@ -837,13 +849,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", "cpu": [ "arm64" ], @@ -854,13 +866,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", "cpu": [ "x64" ], @@ -871,13 +883,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", "cpu": [ "arm64" ], @@ -888,13 +900,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", "cpu": [ "x64" ], @@ -905,13 +917,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", "cpu": [ "arm" ], @@ -922,13 +934,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", "cpu": [ "arm64" ], @@ -939,13 +951,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", "cpu": [ "ia32" ], @@ -956,13 +968,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", "cpu": [ "loong64" ], @@ -973,13 +985,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", "cpu": [ "mips64el" ], @@ -990,13 +1002,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", "cpu": [ "ppc64" ], @@ -1007,13 +1019,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", "cpu": [ "riscv64" ], @@ -1024,13 +1036,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", "cpu": [ "s390x" ], @@ -1041,13 +1053,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", "cpu": [ "x64" ], @@ -1058,13 +1070,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", "cpu": [ "x64" ], @@ -1075,13 +1104,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", "cpu": [ "x64" ], @@ -1092,13 +1138,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", "cpu": [ "x64" ], @@ -1109,13 +1172,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", "cpu": [ "arm64" ], @@ -1126,13 +1189,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", "cpu": [ "ia32" ], @@ -1143,13 +1206,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", "cpu": [ "x64" ], @@ -1160,7 +1223,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1722,9 +1785,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", + "integrity": "sha512-4An71tdz9X8+3sI4Qqqd2LWd9vS39J7sqd9EU4Scw7TJE/qB10Flv/UuqbPVgfQV9XoK8Np6jNquZitnZq5i+Q==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -1823,9 +1886,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1843,9 +1903,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1863,9 +1920,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1883,9 +1937,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1903,9 +1954,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1923,9 +1971,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2104,9 +2149,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2121,9 +2163,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2138,9 +2177,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2155,9 +2191,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2172,9 +2205,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2189,9 +2219,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2206,9 +2233,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2223,9 +2247,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2240,9 +2261,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2257,9 +2275,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2274,9 +2289,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2291,9 +2303,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2308,9 +2317,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2935,6 +2941,37 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.6.tgz", + "integrity": "sha512-36l628fQ/9a/8ihy97eOtEnvWQEdqULQOJtcaxtoNq0G1w3Mxd4szSahOaMM9/NGyZ+hyKcMtIW/WIxq0XQViQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.6", + "ast-v8-to-istanbul": "^1.0.0", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "4.1.6", + "vitest": "4.1.6" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", @@ -3558,6 +3595,25 @@ "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.3.tgz", + "integrity": "sha512-jCMQ6ZylLPudp0CDfBmQBZUsrh1/8psbmu9ibeVWKuHWD0YrH9YABwlKu5kVEFoT0GCQQW9Z/SxfuEbbkGQCRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -5335,6 +5391,436 @@ } } }, + "node_modules/electron-vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/electron-vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/electron/node_modules/@types/node": { "version": "20.19.41", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", @@ -5583,9 +6069,9 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5593,32 +6079,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/escalade": { @@ -6843,6 +7332,13 @@ "dev": true, "license": "ISC" }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-cache-semantics": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", @@ -7548,6 +8044,45 @@ "dev": true, "license": "ISC" }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -7932,9 +8467,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7956,9 +8488,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -7980,9 +8509,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8004,9 +8530,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -8233,6 +8756,47 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.3.tgz", + "integrity": "sha512-pVKE4UdSQ7DvHzivsCIFx2BJn1mHG6KsyrFcaxFx6tONdneEuThrDx0Cj3AMg58KyN4pzYT+LHOotxDQDjNvkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.3", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.2.tgz", + "integrity": "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/make-fetch-happen": { "version": "10.2.1", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz", @@ -9610,12 +10174,12 @@ } }, "node_modules/react-router": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", - "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.4.tgz", + "integrity": "sha512-SVUsDe+DybHM/WmYKIVYhZh1o5Dcuf16yM6WjG02Q9XVFMZIJyHYhwrr6bFBXZkVP6z69kNkMyBCujt8FaFLJA==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2" + "@remix-run/router": "1.23.3" }, "engines": { "node": ">=14.0.0" @@ -9625,13 +10189,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", - "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "version": "6.30.4", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.4.tgz", + "integrity": "sha512-q4HvNl+mmDdkS0g+MqiBZNteQJCuimWoOyHMy4T/RQLAn9Z29+E91QXRaxOujeMl2HTzRSS0KFPd7lxX3PjV0Q==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" + "@remix-run/router": "1.23.3", + "react-router": "6.30.4" }, "engines": { "node": ">=14.0.0" @@ -11060,9 +11624,9 @@ } }, "node_modules/tmp": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", - "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", "dev": true, "license": "MIT", "engines": { @@ -11440,6 +12004,436 @@ } } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, "node_modules/vitest": { "version": "4.1.6", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", diff --git a/package.json b/package.json index 0a68a31..65b32eb 100644 --- a/package.json +++ b/package.json @@ -43,10 +43,12 @@ "@typescript-eslint/eslint-plugin": "^8.15.0", "@typescript-eslint/parser": "^8.15.0", "@vitejs/plugin-react": "^4.3.3", + "@vitest/coverage-v8": "^4.1.6", "autoprefixer": "^10.4.20", "electron": "^33.2.0", "electron-builder": "^25.1.8", "electron-vite": "^2.3.0", + "esbuild": "^0.28.0", "eslint": "^8.57.1", "eslint-plugin-react": "^7.37.2", "eslint-plugin-react-hooks": "^5.0.0", diff --git a/postcss.config.js b/postcss.config.mjs similarity index 100% rename from postcss.config.js rename to postcss.config.mjs diff --git a/scripts/release.ps1 b/scripts/release.ps1 index fc8aae5..a77ad31 100644 --- a/scripts/release.ps1 +++ b/scripts/release.ps1 @@ -116,9 +116,6 @@ $pkgJson = [System.IO.File]::ReadAllText($pkgPath, $utf8NoBom) $pkgJson = $pkgJson -replace "`"version`":\s*`"$current`"", "`"version`": `"$next`"" [System.IO.File]::WriteAllText($pkgPath, $pkgJson, $utf8NoBom) -git add package.json -git commit -m "chore(release): $tag" - # --- Quality gates ------------------------------------------------------ if (-not $SkipBuild) { Write-Host "Typecheck..." -ForegroundColor Cyan @@ -145,6 +142,12 @@ foreach ($f in @($installer, $blockmap, $manifest)) { } } +# Commit only after quality gates and artifact verification pass. If a check +# fails, package.json remains modified but history does not get a broken +# release commit/tag. +git add package.json +git commit -m "chore(release): $tag" + # --- Tag + push --------------------------------------------------------- Write-Host "Tagging $tag and pushing..." -ForegroundColor Cyan git tag -a $tag -m "Release $tag" diff --git a/src/main/games/registry.test.ts b/src/main/games/registry.test.ts new file mode 100644 index 0000000..59c57af --- /dev/null +++ b/src/main/games/registry.test.ts @@ -0,0 +1,119 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const h = vi.hoisted(() => ({ + provider: { + displayName: 'Dota 2', + start: vi.fn(), + stop: vi.fn(), + detect: vi.fn(), + install: vi.fn(), + uninstall: vi.fn(), + reconcile: vi.fn() + }, + startGsiServer: vi.fn(), + stopGsiServer: vi.fn(), + onLaunchOptionsApplied: vi.fn(), + gamesEnabled: { dota2: true }, + fireMatchSummary: vi.fn(), + log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } +})) + +vi.mock('electron', () => ({ + BrowserWindow: { getAllWindows: () => [] } +})) + +vi.mock('./dota2', () => ({ + Dota2Provider: vi.fn(function Dota2Provider() { + return h.provider + }) +})) + +vi.mock('./gsi-server', () => ({ + startGsiServer: h.startGsiServer, + stopGsiServer: h.stopGsiServer +})) + +vi.mock('./steam-launch-options', () => ({ + onLaunchOptionsApplied: h.onLaunchOptionsApplied +})) + +vi.mock('../store', () => ({ + getChallenges: () => [], + getGamesEnabled: () => h.gamesEnabled +})) + +vi.mock('../notifications', () => ({ + fireMatchSummary: h.fireMatchSummary +})) + +vi.mock('../logger', () => ({ + log: h.log +})) + +async function loadRegistry(): Promise { + return import('./registry') +} + +beforeEach(() => { + vi.resetModules() + h.provider.start.mockResolvedValue(undefined) + h.provider.stop.mockResolvedValue(undefined) + h.provider.detect.mockResolvedValue({ + id: 'dota2', + name: 'Dota 2', + installed: true, + integrationActive: true, + launchOptionStatus: 'applied', + enabled: true + }) + h.provider.install.mockResolvedValue(undefined) + h.provider.uninstall.mockResolvedValue(undefined) + h.provider.reconcile.mockResolvedValue(undefined) + h.startGsiServer.mockReset() + h.startGsiServer.mockResolvedValue(undefined) + h.stopGsiServer.mockReset() + h.stopGsiServer.mockResolvedValue(undefined) + h.onLaunchOptionsApplied.mockClear() + h.fireMatchSummary.mockClear() + h.log.info.mockClear() + h.log.warn.mockClear() + h.log.error.mockClear() + h.log.debug.mockClear() +}) + +describe('games registry lifecycle', () => { + it('сбрасывает running после ошибки старта GSI и позволяет повторный старт', async () => { + h.startGsiServer + .mockRejectedValueOnce(new Error('port busy')) + .mockResolvedValueOnce(undefined) + + const { startGamesRegistry } = await loadRegistry() + await startGamesRegistry() + await startGamesRegistry() + + expect(h.startGsiServer).toHaveBeenCalledTimes(2) + expect(h.provider.start).toHaveBeenCalledTimes(1) + }) + + it('stopGamesRegistry ждёт полного shutdown GSI-сервера', async () => { + let releaseStop!: () => void + const stopPromise = new Promise((resolve) => { + releaseStop = resolve + }) + h.stopGsiServer.mockReturnValue(stopPromise) + + const { startGamesRegistry, stopGamesRegistry } = await loadRegistry() + await startGamesRegistry() + + let resolved = false + const pending = stopGamesRegistry().then(() => { + resolved = true + }) + await Promise.resolve() + + expect(resolved).toBe(false) + releaseStop() + await pending + expect(resolved).toBe(true) + }) +}) diff --git a/src/main/games/registry.ts b/src/main/games/registry.ts index 6c624e3..0073cfb 100644 --- a/src/main/games/registry.ts +++ b/src/main/games/registry.ts @@ -87,6 +87,7 @@ export async function startGamesRegistry(): Promise { await startGsiServer() log.info('[games] GSI server started on port 4701') } catch (err) { + running = false log.error('[games] GSI server failed to start', err) return } @@ -119,7 +120,7 @@ export async function stopGamesRegistry(): Promise { for (const id of Object.keys(providers) as GameId[]) { await providers[id].stop() } - stopGsiServer() + await stopGsiServer() } export async function listGamesStatus(): Promise { diff --git a/src/main/ipc.ts b/src/main/ipc.ts index 40873ae..a177396 100644 --- a/src/main/ipc.ts +++ b/src/main/ipc.ts @@ -176,8 +176,10 @@ export function registerIpc(): void { const id = validateId(idRaw) if (!id) return null const ex = markDone(id, validateActualReps(repsRaw)) - broadcastState() - broadcastHistoryChanged() + if (ex) { + broadcastState() + broadcastHistoryChanged() + } return ex }) @@ -186,8 +188,10 @@ export function registerIpc(): void { const minutes = validateSnoozeMinutes(minRaw) if (!id || minutes === null) return null const ex = snooze(id, minutes) - broadcastState() - broadcastHistoryChanged() + if (ex) { + broadcastState() + broadcastHistoryChanged() + } return ex }) @@ -195,8 +199,10 @@ export function registerIpc(): void { const id = validateId(idRaw) if (!id) return null const ex = skip(id) - broadcastState() - broadcastHistoryChanged() + if (ex) { + broadcastState() + broadcastHistoryChanged() + } return ex }) @@ -238,7 +244,10 @@ export function registerIpc(): void { const id = validateId(idRaw) if (!id) return null const m = markMealDone(id) - broadcastState() + if (m) { + broadcastState() + broadcastHistoryChanged() + } return m }) diff --git a/src/main/meeting-detect.test.ts b/src/main/meeting-detect.test.ts index 76b4043..54b896f 100644 --- a/src/main/meeting-detect.test.ts +++ b/src/main/meeting-detect.test.ts @@ -21,6 +21,8 @@ const h = vi.hoisted(() => ({ log: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() } })) +const originalPlatform = process.platform + vi.mock('node:child_process', () => ({ exec: (cmd: string, opts: unknown, cb: ExecCb) => { h.calls += 1 @@ -39,8 +41,16 @@ async function load(): Promise { return import('./meeting-detect') } +function setPlatform(platform: NodeJS.Platform): void { + Object.defineProperty(process, 'platform', { + value: platform, + configurable: true + }) +} + beforeEach(() => { vi.resetModules() + setPlatform('win32') h.calls = 0 h.execImpl = (_cmd, _opts, cb) => cb(null, { stdout: '' }) h.log.info.mockClear() @@ -48,6 +58,7 @@ beforeEach(() => { }) afterEach(() => { + setPlatform(originalPlatform) vi.restoreAllMocks() }) @@ -96,14 +107,9 @@ describe('isMeetingActive', () => { }) it('на не-Windows возвращает false без вызова tasklist', async () => { - const original = process.platform - Object.defineProperty(process, 'platform', { value: 'linux' }) - try { - const { isMeetingActive } = await load() - expect(await isMeetingActive()).toBe(false) - expect(h.calls).toBe(0) - } finally { - Object.defineProperty(process, 'platform', { value: original }) - } + setPlatform('linux') + const { isMeetingActive } = await load() + expect(await isMeetingActive()).toBe(false) + expect(h.calls).toBe(0) }) }) diff --git a/src/main/scheduler.test.ts b/src/main/scheduler.test.ts index 7b18b3b..b6b34e8 100644 --- a/src/main/scheduler.test.ts +++ b/src/main/scheduler.test.ts @@ -186,6 +186,21 @@ describe('checkDueExercises gating', () => { expect(h.updateExercise).toHaveBeenCalled() }) + it('dailyGoal использует reps snapshot из истории, а не текущие reps упражнения', async () => { + h.exercises = [makeExercise({ reps: 25, dailyGoal: 20 })] + h.history = [ + { + ts: Date.now(), + exerciseId: 'ex1', + action: 'done', + reps: 10 + } + ] + const { forceCheck } = await loadScheduler() + forceCheck() + expect(h.fireReminder).toHaveBeenCalledTimes(1) + }) + it('adaptive: применяет adjustNextFireAt к кандидату', async () => { h.exercises = [makeExercise({ adaptive: true })] const { forceCheck } = await loadScheduler() diff --git a/src/main/scheduler.ts b/src/main/scheduler.ts index 10460fc..7ae1080 100644 --- a/src/main/scheduler.ts +++ b/src/main/scheduler.ts @@ -17,7 +17,8 @@ import { adjustNextFireAt } from './adaptive' /** * Сколько reps пользователь сделал по упражнению `ex` за сегодня (local day). - * Учитываем actualReps если задано (частичное выполнение), иначе planned reps. + * Учитываем actualReps если задано (частичное выполнение), затем snapshot + * reps из истории, и только потом текущие planned reps упражнения. */ function repsDoneToday(ex: Exercise, history: HistoryEntry[]): number { const todayKey = new Date() @@ -28,7 +29,7 @@ function repsDoneToday(ex: Exercise, history: HistoryEntry[]): number { if (e.action !== 'done') continue if (e.exerciseId !== ex.id) continue if (e.ts < startMs) continue - sum += e.actualReps ?? ex.reps + sum += e.actualReps ?? e.reps ?? ex.reps } return sum } diff --git a/src/main/store.test.ts b/src/main/store.test.ts index 721a470..acdb7a5 100644 --- a/src/main/store.test.ts +++ b/src/main/store.test.ts @@ -8,6 +8,7 @@ import { } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' +import { DEFAULT_SETTINGS } from '@shared/types' /** * Тесты persistence-слоя. Мокаем electron.app.getPath на временную директорию @@ -147,6 +148,43 @@ describe('store · history cap', () => { }) }) +describe('store · meal history', () => { + it('markMealDone пишет meal-entry в историю', async () => { + writeFileSync( + statePath(), + JSON.stringify({ + exercises: [], + meals: [ + { + id: 'm1', + name: 'Обед', + time: '13:00', + icon: 'Soup', + enabled: true, + days: [], + nextFireAt: Date.now() + 60_000 + } + ], + challenges: [], + history: [] + }), + 'utf-8' + ) + + const { markMealDone, getHistory } = await load() + expect(markMealDone('m1')).toBeDefined() + expect(getHistory()).toMatchObject([ + { + exerciseId: 'meal:m1', + action: 'done', + reps: 1, + name: 'Обед', + source: 'meal' + } + ]) + }) +}) + describe('store · clearHistory', () => { it('удаляет записи старше границы и возвращает количество', async () => { const ex = { @@ -195,4 +233,79 @@ describe('store · export / import', () => { expect(importState('not json at all')).toBe(false) expect(importState('42')).toBe(false) }) + + it('import сохраняет валидные части snapshot и отбрасывает повреждённые записи', async () => { + const validExercise = { + id: 'x1', + name: 'Тест', + reps: 10, + icon: 'Activity', + intervalMinutes: 30, + enabled: true, + nextFireAt: Date.now() + 1000 + } + const validMeal = { + id: 'm1', + name: 'Обед', + time: '13:00', + icon: 'Soup', + enabled: true, + days: [], + nextFireAt: Date.now() + 1000 + } + const validChallenge = { + id: 'c1', + name: 'За убийства', + gameId: 'dota2', + stat: 'kills', + multiplier: 1, + exerciseName: 'Отжимания', + icon: 'Dumbbell', + enabled: true + } + + const { importState, getState, getSettings, getHistory } = await load() + expect( + importState( + JSON.stringify({ + exercises: [ + validExercise, + { ...validExercise, id: 'bad-ex', intervalMinutes: -5 } + ], + meals: [validMeal, { ...validMeal, id: 'bad-meal', time: '25:00' }], + settings: { + globalEnabled: false, + snoozeMinutes: -1, + language: 'xx' + }, + challenges: [ + validChallenge, + { ...validChallenge, id: 'bad-challenge', gameId: 'cs2' } + ], + gamesEnabled: { dota2: true, cs2: true }, + history: [ + { ts: 100, exerciseId: 'x1', action: 'done', reps: 10 }, + { ts: -1, exerciseId: 'x1', action: 'done', reps: 10 }, + { + ts: 200, + exerciseId: 'meal:m1', + action: 'done', + reps: 1, + source: 'meal' + } + ] + }) + ) + ).toBe(true) + + const state = getState() + expect(state.exercises.map((e) => e.id)).toEqual(['x1']) + expect(state.meals.map((m) => m.id)).toEqual(['m1']) + expect(state.challenges.map((c) => c.id)).toEqual(['c1']) + expect(state.gamesEnabled).toEqual({ dota2: true }) + expect(getHistory().map((e) => e.ts)).toEqual([100, 200]) + expect(getSettings().globalEnabled).toBe(false) + expect(getSettings().snoozeMinutes).toBe(DEFAULT_SETTINGS.snoozeMinutes) + expect(getSettings().language).toBe(DEFAULT_SETTINGS.language) + }) }) diff --git a/src/main/store.ts b/src/main/store.ts index 6845c9d..7ed29a5 100644 --- a/src/main/store.ts +++ b/src/main/store.ts @@ -17,6 +17,7 @@ import { GameId, HistoryAction, HistoryEntry, + HistorySource, Meal, nextMealOccurrence, PersistedState, @@ -25,6 +26,13 @@ import { Settings } from '@shared/types' import { log } from './logger' +import { + validateChallengeInput, + validateExerciseInput, + validateId, + validateMealInput, + validateSettingsPatch +} from './validate' /** * Keep at most this many history entries (≈2.7 years at 10/day). @@ -110,6 +118,136 @@ function isValidParsed(v: unknown): v is Record { return typeof v === 'object' && v !== null && !Array.isArray(v) } +function finiteMs(v: unknown): number | undefined { + return typeof v === 'number' && + Number.isFinite(v) && + v >= 0 && + v <= Number.MAX_SAFE_INTEGER + ? v + : undefined +} + +function intInRange(v: unknown, min: number, max: number): number | undefined { + if (typeof v !== 'number' || !Number.isFinite(v)) return undefined + const n = Math.trunc(v) + return n >= min && n <= max ? n : undefined +} + +function safeStr(v: unknown, max = 200): string | undefined { + if (typeof v !== 'string') return undefined + if (v.length === 0 || v.length > max) return undefined + return v +} + +const SETTINGS_KEYS: (keyof Settings)[] = [ + 'globalEnabled', + 'notificationMode', + 'soundEnabled', + 'voicePromptsEnabled', + 'meetingAutoPause', + 'startWithWindows', + 'minimizeToTray', + 'startMinimized', + 'theme', + 'language', + 'snoozeMinutes', + 'quietHours', + 'lastSeenVersion' +] + +const GAME_IDS: GameId[] = ['dota2'] +const HISTORY_ACTIONS: HistoryAction[] = ['done', 'skip', 'snooze'] +const HISTORY_SOURCES: HistorySource[] = ['reminder', 'meal', 'match'] + +function sanitizeSettings(raw: unknown): Settings { + const out: Settings = { ...DEFAULT_SETTINGS } + if (!isValidParsed(raw)) return out + + for (const key of SETTINGS_KEYS) { + if (!(key in raw)) continue + const patch = validateSettingsPatch({ [key]: raw[key] }) + if (patch) Object.assign(out, patch) + } + + return out +} + +function sanitizeExercise(raw: unknown, now = Date.now()): Exercise | null { + if (!isValidParsed(raw)) return null + const id = validateId(raw.id) + const base = validateExerciseInput(raw) + if (!id || !base) return null + + const exercise: Exercise = { + ...base, + id, + nextFireAt: finiteMs(raw.nextFireAt) ?? now + base.intervalMinutes * 60_000 + } + const lastDoneAt = finiteMs(raw.lastDoneAt) + if (lastDoneAt !== undefined) exercise.lastDoneAt = lastDoneAt + return exercise +} + +function sanitizeMeal(raw: unknown, now = Date.now()): Meal | null { + if (!isValidParsed(raw)) return null + const id = validateId(raw.id) + const base = validateMealInput(raw) + if (!id || !base) return null + + const meal: Meal = { + ...base, + id, + nextFireAt: finiteMs(raw.nextFireAt) ?? nextMealOccurrence(base.time, base.days, now) + } + const lastDoneAt = finiteMs(raw.lastDoneAt) + if (lastDoneAt !== undefined) meal.lastDoneAt = lastDoneAt + return meal +} + +function sanitizeChallenge(raw: unknown): Challenge | null { + if (!isValidParsed(raw)) return null + const id = validateId(raw.id) + const base = validateChallengeInput(raw) + if (!id || !base) return null + return { ...base, id } +} + +function sanitizeGamesEnabled(raw: unknown): Partial> { + const out: Partial> = {} + if (!isValidParsed(raw)) return out + for (const id of GAME_IDS) { + if (typeof raw[id] === 'boolean') out[id] = raw[id] + } + return out +} + +function sanitizeHistoryEntry(raw: unknown): HistoryEntry | null { + if (!isValidParsed(raw)) return null + const ts = finiteMs(raw.ts) + const exerciseId = validateId(raw.exerciseId) + const action = + typeof raw.action === 'string' && + HISTORY_ACTIONS.includes(raw.action as HistoryAction) + ? (raw.action as HistoryAction) + : undefined + if (ts === undefined || !exerciseId || action === undefined) return null + + const entry: HistoryEntry = { ts, exerciseId, action } + const actualReps = intInRange(raw.actualReps, 0, 100_000) + if (actualReps !== undefined) entry.actualReps = actualReps + const reps = intInRange(raw.reps, 0, 100_000) + if (reps !== undefined) entry.reps = reps + const name = safeStr(raw.name) + if (name !== undefined) entry.name = name + if ( + typeof raw.source === 'string' && + HISTORY_SOURCES.includes(raw.source as HistorySource) + ) { + entry.source = raw.source as HistorySource + } + return entry +} + /** * Current persisted-state schema version. Bump this and add a migration to * MIGRATIONS whenever the on-disk shape changes in a non-additive way. @@ -155,22 +293,36 @@ function runMigrations(s: StoredState): StoredState { /** Coerce a (possibly partial) migrated state into a fully-formed PersistedState. */ function coerce(s: StoredState): PersistedState { + const now = Date.now() return { - exercises: Array.isArray(s.exercises) ? (s.exercises as Exercise[]) : [], + exercises: Array.isArray(s.exercises) + ? s.exercises.flatMap((raw) => { + const exercise = sanitizeExercise(raw, now) + return exercise ? [exercise] : [] + }) + : [], // Additive: старые state'ы без `meals` получают пустой список (см. философию // миграций — additive-поля не требуют bump'а схемы). - meals: Array.isArray(s.meals) ? (s.meals as Meal[]) : [], - settings: { - ...DEFAULT_SETTINGS, - ...(isValidParsed(s.settings) ? (s.settings as Partial) : {}) - }, - challenges: Array.isArray(s.challenges) - ? (s.challenges as Challenge[]) + meals: Array.isArray(s.meals) + ? s.meals.flatMap((raw) => { + const meal = sanitizeMeal(raw, now) + return meal ? [meal] : [] + }) : [], - gamesEnabled: isValidParsed(s.gamesEnabled) - ? (s.gamesEnabled as Partial>) - : {}, - history: Array.isArray(s.history) ? (s.history as HistoryEntry[]) : [] + settings: sanitizeSettings(s.settings), + challenges: Array.isArray(s.challenges) + ? s.challenges.flatMap((raw) => { + const challenge = sanitizeChallenge(raw) + return challenge ? [challenge] : [] + }) + : [], + gamesEnabled: sanitizeGamesEnabled(s.gamesEnabled), + history: Array.isArray(s.history) + ? s.history.flatMap((raw) => { + const entry = sanitizeHistoryEntry(raw) + return entry ? [entry] : [] + }) + : [] } } @@ -543,6 +695,11 @@ export function markMealDone(id: string): Meal | undefined { if (meal.nextFireAt <= Date.now()) { meal.nextFireAt = nextMealOccurrence(meal.time, meal.days, Date.now()) } + appendHistory(`meal:${id}`, 'done', { + reps: 1, + name: meal.name, + source: 'meal' + }) scheduleWrite() return meal } @@ -641,8 +798,8 @@ export function exportState(): string { /** * Импорт snapshot'а. Перезаписывает текущий state. Возвращает true при - * успехе. Идёт через тот же coerce + runMigrations что и load() — это - * валидирует тип/диапазоны. + * успехе. Идёт через тот же coerce + runMigrations что и load(): валидные + * записи сохраняются, повреждённые записи/поля отбрасываются. * * НЕ объединяет с текущим state (merge сложен: дубликаты id, конфликты * settings) — простое replace. Перед импортом UI должен спросить diff --git a/src/main/validate.ts b/src/main/validate.ts index 93f4085..397166b 100644 --- a/src/main/validate.ts +++ b/src/main/validate.ts @@ -14,6 +14,7 @@ import type { Challenge, Exercise, + GameId, GameStat, Meal, Settings, @@ -27,6 +28,7 @@ const MAX_STR_LEN = 200 const VALID_THEMES: Theme[] = ['system', 'light', 'dark'] const VALID_LANGS: Language[] = ['ru', 'en'] const VALID_NOTIFY: NotificationMode[] = ['toast', 'modal', 'both'] +const VALID_GAME_IDS: GameId[] = ['dota2'] const VALID_STATS: GameStat[] = [ 'deaths', 'kills', @@ -289,7 +291,7 @@ export function validateChallengeInput( ): Omit | null { if (!isObj(raw)) return null const name = safeStr(raw.name) - const gameId = safeStr(raw.gameId, 32) + const gameId = oneOf(raw.gameId, VALID_GAME_IDS) const stat = oneOf(raw.stat, VALID_STATS) const multiplier = numInRange(raw.multiplier, 0, 1000) const exerciseName = safeStr(raw.exerciseName) @@ -306,7 +308,7 @@ export function validateChallengeInput( } return { name, - gameId: gameId as Challenge['gameId'], + gameId, stat, multiplier, exerciseName, diff --git a/src/renderer/src/ReminderApp.tsx b/src/renderer/src/ReminderApp.tsx index d83528b..35d5d7b 100644 --- a/src/renderer/src/ReminderApp.tsx +++ b/src/renderer/src/ReminderApp.tsx @@ -30,6 +30,8 @@ type Mode = | { kind: 'meal'; meal: Meal } | { kind: 'match'; summary: MatchSummary; done: Set } +type ActiveMode = Exclude + /** Минимальный нативный confirm. В reminder-окне нет места для модалки, * проще использовать встроенный диалог. */ function nativeConfirm(message: string): boolean { @@ -41,6 +43,8 @@ export default function ReminderApp(): JSX.Element { const [mode, setMode] = useState({ kind: 'idle' }) const [settings, setSettings] = useState(null) const settingsRef = useRef(null) + const modeRef = useRef({ kind: 'idle' }) + const queueRef = useRef([]) // ChallengeId'ы, для которых уже отправили markChallengeDone IPC. ref, // не state — нужен только для дедупа rapid double-click. Сбрасывается // когда приходит новый match summary (см. onMatchEnd ниже). @@ -50,53 +54,21 @@ export default function ReminderApp(): JSX.Element { settingsRef.current = settings }, [settings]) + useEffect(() => { + modeRef.current = mode + }, [mode]) + useEffect(() => { window.api.getState().then((s) => setSettings(s.settings)) const u0 = window.api.onStateChanged((s) => setSettings(s.settings)) const u1 = window.api.onFire((ex) => { - setMode({ kind: 'exercise', exercise: ex }) - const s = settingsRef.current - if (s?.soundEnabled) playBeep() - if (s?.voicePromptsEnabled) { - // Задержка 800ms даёт пользователю шанс decrement'нуть stepper до - // фактического количества — TTS прозвучит уже под реальную цифру, - // если успел нажать -. Иначе скажет планируемые reps. - const lang = s.language ?? 'ru' - setTimeout(() => { - const phrase = - lang === 'ru' - ? `${ex.name}. ${ex.reps} ${repWordRu(ex.reps)}` - : `${ex.name}. ${ex.reps} ${ex.reps === 1 ? 'rep' : 'reps'}` - speak(phrase, lang) - }, 800) - } + enqueueMode({ kind: 'exercise', exercise: ex }) }) const u1b = window.api.onFireMeal((meal) => { - setMode({ kind: 'meal', meal }) - const s = settingsRef.current - if (s?.soundEnabled) playBeep() - if (s?.voicePromptsEnabled) { - const lang = s.language ?? 'ru' - const phrase = - lang === 'ru' ? `Пора поесть. ${meal.name}` : `Time to eat. ${meal.name}` - speak(phrase, lang) - } + enqueueMode({ kind: 'meal', meal }) }) const u2 = window.api.onMatchEnd((summary) => { - // Новый матч — сбрасываем дедуп challenge'ей. - sentChallengesRef.current = new Set() - setMode({ kind: 'match', summary, done: new Set() }) - const s = settingsRef.current - if (s?.soundEnabled) playBeep() - if (s?.voicePromptsEnabled) { - const total = summary.results.reduce((acc, r) => acc + r.reps, 0) - const lang = s.language ?? 'ru' - const phrase = - lang === 'ru' - ? `Матч завершён. ${total} ${repWordRu(total)} ждут.` - : `Match complete. ${total} ${total === 1 ? 'rep' : 'reps'} await.` - speak(phrase, lang) - } + enqueueMode({ kind: 'match', summary, done: new Set() }) }) return () => { u0() @@ -104,6 +76,9 @@ export default function ReminderApp(): JSX.Element { u1b() u2() } + // IPC-подписки должны жить один раз; enqueueMode читает актуальный mode + // через ref, поэтому зависимость здесь не нужна. + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // ESC closes the match summary view too — keyboard parity with exercise mode. @@ -117,6 +92,63 @@ export default function ReminderApp(): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [mode.kind]) + function enqueueMode(next: ActiveMode): void { + if (modeRef.current.kind === 'idle') { + activateMode(next) + return + } + queueRef.current.push(next) + } + + function activateMode(next: ActiveMode): void { + if (next.kind === 'match') { + // Новый match summary получает чистый дедуп-сет только когда реально + // становится активным; иначе queued summary не сбивает текущий матч. + sentChallengesRef.current = new Set() + } + modeRef.current = next + setMode(next) + playAlertFor(next) + } + + function playAlertFor(next: ActiveMode): void { + const s = settingsRef.current + if (s?.soundEnabled) playBeep() + if (!s?.voicePromptsEnabled) return + + const lang = s.language ?? 'ru' + if (next.kind === 'exercise') { + const ex = next.exercise + // Задержка 800ms даёт пользователю шанс decrement'нуть stepper до + // фактического количества — TTS прозвучит уже под реальную цифру, + // если успел нажать -. Иначе скажет планируемые reps. + setTimeout(() => { + const phrase = + lang === 'ru' + ? `${ex.name}. ${ex.reps} ${repWordRu(ex.reps)}` + : `${ex.name}. ${ex.reps} ${ex.reps === 1 ? 'rep' : 'reps'}` + speak(phrase, lang) + }, 800) + return + } + + if (next.kind === 'meal') { + const phrase = + lang === 'ru' + ? `Пора поесть. ${next.meal.name}` + : `Time to eat. ${next.meal.name}` + speak(phrase, lang) + return + } + + const total = next.summary.results.reduce((acc, r) => acc + r.reps, 0) + const phrase = + lang === 'ru' + ? `Матч завершён. ${total} ${repWordRu(total)} ждут.` + : `Match complete. ${total} ${total === 1 ? 'rep' : 'reps'} await.` + speak(phrase, lang) + } + function close(): void { // Если в Match Summary остались незакрытые челленджи — подтверждаем, // чтобы пользователь не «пролетел» окно по привычке и не потерял @@ -139,6 +171,12 @@ export default function ReminderApp(): JSX.Element { if (!nativeConfirm(msg)) return } } + const next = queueRef.current.shift() + if (next) { + activateMode(next) + return + } + modeRef.current = { kind: 'idle' } setMode({ kind: 'idle' }) window.api.reminderClose() } @@ -189,13 +227,16 @@ export default function ReminderApp(): JSX.Element { } // 2) Functional update: rapid-click race-safe. setMode((m) => - m.kind === 'match' - ? { + { + if (m.kind !== 'match') return m + const nextMode: Mode = { kind: 'match', summary: m.summary, done: new Set([...m.done, id]) } - : m + modeRef.current = nextMode + return nextMode + } ) }} onClose={close} diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 3d46c48..eacf85f 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -77,7 +77,7 @@ export const IPC = { evtMaximizeChanged: 'evt:maximizeChanged', evtMeetingChanged: 'evt:meetingChanged', /** - * Шлётся когда история мутирует (markDone / snooze / skip / + * Шлётся когда история мутирует (markDone / markMealDone / snooze / skip / * markChallengeDone / clearHistory / import). Renderer'у достаточно * перезапросить getHistory. Раньше Dashboard переключал history по * `exercises` ref'у — но markDone мутирует Exercise in place, ref не diff --git a/src/shared/types.ts b/src/shared/types.ts index 8b291c9..343140c 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -147,10 +147,11 @@ export type PersistedState = AppState & { export type HistoryAction = 'done' | 'skip' | 'snooze' /** - * Источник записи: обычное напоминание (от scheduler'а) или матч (челлендж). + * Источник записи: обычное напоминание (от scheduler'а), приём пищи или + * матч (челлендж). * Используется для UI («подтянулся в матче» vs «по таймеру») и аналитики. */ -export type HistorySource = 'reminder' | 'match' +export type HistorySource = 'reminder' | 'meal' | 'match' export type HistoryEntry = { /** ms epoch */ diff --git a/tailwind.config.js b/tailwind.config.mjs similarity index 100% rename from tailwind.config.js rename to tailwind.config.mjs