diff --git a/frontend/package.json b/frontend/package.json index 0ab63a6..944bab1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "cors": "^2.8.5", "eslint": "^9.29.0", "eslint-plugin-vue": "~10.2.0", + "express": "^5.1.0", "prettier": "3.5.3", "tailwindcss": "^4.0.0", "typescript": "~5.8.0", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 18aa805..3620d6d 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -57,6 +57,9 @@ importers: eslint-plugin-vue: specifier: ~10.2.0 version: 10.2.0(eslint@9.39.5(jiti@2.7.0))(vue-eslint-parser@10.4.1(eslint@9.39.5(jiti@2.7.0))) + express: + specifier: ^5.1.0 + version: 5.2.1 prettier: specifier: 3.5.3 version: 3.5.3 @@ -769,6 +772,10 @@ packages: vue: optional: true + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -810,6 +817,10 @@ packages: birpc@2.9.0: resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} @@ -827,10 +838,22 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -866,6 +889,26 @@ packages: engines: {node: '>=18'} hasBin: true + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + copy-anything@4.0.5: resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} engines: {node: '>=18'} @@ -905,13 +948,28 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + enhanced-resolve@5.24.5: resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} engines: {node: '>=10.13.0'} @@ -920,9 +978,21 @@ packages: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} @@ -932,6 +1002,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1023,10 +1096,18 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + expect-type@1.4.0: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1063,6 +1144,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1074,15 +1159,34 @@ packages: flatted@3.4.4: resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1095,6 +1199,10 @@ packages: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -1102,6 +1210,14 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} hasBin: true @@ -1109,6 +1225,14 @@ packages: hookable@5.5.3: resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -1125,6 +1249,13 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -1141,6 +1272,9 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} @@ -1268,6 +1402,18 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1276,6 +1422,14 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + minimatch@10.2.6: resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} engines: {node: 18 || 20 || >=22} @@ -1304,6 +1458,10 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -1311,6 +1469,17 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1327,6 +1496,10 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -1338,6 +1511,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} @@ -1389,13 +1565,29 @@ packages: engines: {node: '>=14'} hasBin: true + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -1416,17 +1608,35 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1439,6 +1649,22 @@ packages: resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} engines: {node: '>= 0.4'} + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1453,6 +1679,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -1523,6 +1753,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1540,6 +1774,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + typescript-eslint@8.65.0: resolution: {integrity: sha512-/ggrHAwyjENDusvyxbuqxAC2dTnZg/Z8F+fgQtYIz+L6n/9HfSlEZcFGV/NsMNa6CkGk0xUjUAFwC0vHOflvIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1555,6 +1793,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1684,6 +1926,9 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + xml-name-validator@4.0.0: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} @@ -2329,6 +2574,11 @@ snapshots: typescript: 5.8.3 vue: 3.5.40(typescript@5.8.3) + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn-jsx@5.3.2(acorn@8.18.0): dependencies: acorn: 8.18.0 @@ -2360,6 +2610,20 @@ snapshots: birpc@2.9.0: {} + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + boolbase@1.0.0: {} brace-expansion@1.1.18: @@ -2379,8 +2643,20 @@ snapshots: dependencies: fill-range: 7.1.1 + bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + callsites@3.1.0: {} chai@5.3.3: @@ -2421,6 +2697,16 @@ snapshots: tree-kill: 1.2.2 yargs: 17.7.2 + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + copy-anything@4.0.5: dependencies: is-what: 5.5.0 @@ -2450,10 +2736,22 @@ snapshots: deep-is@0.1.4: {} + depd@2.0.0: {} + detect-libc@2.1.2: {} + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ee-first@1.1.1: {} + emoji-regex@8.0.0: {} + encodeurl@2.0.0: {} + enhanced-resolve@5.24.5: dependencies: graceful-fs: 4.2.11 @@ -2461,8 +2759,16 @@ snapshots: entities@7.0.1: {} + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -2494,6 +2800,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)): @@ -2609,8 +2917,43 @@ snapshots: esutils@2.0.3: {} + etag@1.8.1: {} + expect-type@1.4.0: {} + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -2643,6 +2986,17 @@ snapshots: dependencies: to-regex-range: 5.0.1 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -2655,11 +3009,35 @@ snapshots: flatted@3.4.4: {} + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.3: optional: true + function-bind@1.1.2: {} + get-caller-file@2.0.5: {} + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -2670,14 +3048,34 @@ snapshots: globals@14.0.0: {} + gopd@1.2.0: {} + graceful-fs@4.2.11: {} has-flag@4.0.0: {} + has-symbols@1.1.0: {} + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + he@1.2.0: {} hookable@5.5.3: {} + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + ignore@5.3.2: {} ignore@7.0.6: {} @@ -2689,6 +3087,10 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -2699,6 +3101,8 @@ snapshots: is-number@7.0.0: {} + is-promise@4.0.0: {} + is-what@5.5.0: {} isexe@2.0.0: {} @@ -2795,6 +3199,12 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + merge2@1.4.1: {} micromatch@4.0.8: @@ -2802,6 +3212,12 @@ snapshots: braces: 3.0.3 picomatch: 2.3.2 + mime-db@1.54.0: {} + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + minimatch@10.2.6: dependencies: brace-expansion: 5.0.9 @@ -2824,12 +3240,24 @@ snapshots: natural-compare@1.4.0: {} + negotiator@1.0.0: {} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 object-assign@4.1.1: {} + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2851,12 +3279,16 @@ snapshots: dependencies: callsites: 3.1.0 + parseurl@1.3.3: {} + path-browserify@1.0.1: {} path-exists@4.0.0: {} path-key@3.1.1: {} + path-to-regexp@8.4.2: {} + pathe@2.0.3: {} pathval@2.0.1: {} @@ -2895,10 +3327,29 @@ snapshots: prettier@3.5.3: {} + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + queue-microtask@1.2.3: {} + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + require-directory@2.1.1: {} resolve-from@4.0.0: {} @@ -2939,6 +3390,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.62.4 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -2947,8 +3408,37 @@ snapshots: dependencies: tslib: 2.8.1 + safer-buffer@2.1.2: {} + semver@7.8.5: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -2957,6 +3447,34 @@ snapshots: shell-quote@1.9.0: {} + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} source-map-js@1.2.1: {} @@ -2965,6 +3483,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} string-width@4.2.3: @@ -3024,6 +3544,8 @@ snapshots: dependencies: is-number: 7.0.0 + toidentifier@1.0.1: {} + tree-kill@1.2.2: {} ts-api-utils@2.5.0(typescript@5.8.3): @@ -3036,6 +3558,12 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + typescript-eslint@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3): dependencies: '@typescript-eslint/eslint-plugin': 8.65.0(@typescript-eslint/parser@8.65.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.8.3) @@ -3051,6 +3579,8 @@ snapshots: undici-types@6.21.0: {} + unpipe@1.0.0: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -3187,6 +3717,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrappy@1.0.2: {} + xml-name-validator@4.0.0: {} y18n@5.0.8: {} diff --git a/frontend/src/assets/img/app-icons/mail.webp b/frontend/src/assets/img/app-icons/mail.webp new file mode 100644 index 0000000..fdfb9ea Binary files /dev/null and b/frontend/src/assets/img/app-icons/mail.webp differ diff --git a/frontend/src/assets/img/app-icons/notes.webp b/frontend/src/assets/img/app-icons/notes.webp index 959a61a..5058ef4 100644 Binary files a/frontend/src/assets/img/app-icons/notes.webp and b/frontend/src/assets/img/app-icons/notes.webp differ diff --git a/frontend/src/assets/main.css b/frontend/src/assets/main.css index 8ae99eb..6ddb7e6 100644 --- a/frontend/src/assets/main.css +++ b/frontend/src/assets/main.css @@ -2,7 +2,6 @@ @import 'konsta/vue/theme.css'; :root { - color-scheme: dark; font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue', sans-serif; @@ -21,7 +20,7 @@ body, min-height: 0 !important; margin: 0; overflow: hidden; - background: transparent; + background: transparent !important; user-select: none; } button, @@ -89,6 +88,10 @@ button { min-height: 0 !important; overflow: hidden; background: transparent !important; + color-scheme: dark; +} +.phone-app--light { + color-scheme: light; } .phone-notification { right: 8px !important; diff --git a/frontend/src/config/apps.test.ts b/frontend/src/config/apps.test.ts index 0138497..483e629 100644 --- a/frontend/src/config/apps.test.ts +++ b/frontend/src/config/apps.test.ts @@ -11,6 +11,11 @@ describe('app registry', () => { expect(PHONE_APPS.every((app) => app.iconImage.endsWith('.webp'))).toBe( true, ) + expect(PHONE_APPS.find((app) => app.id === 'mail')).toMatchObject({ + gridOrder: 3, + labelKey: 'Apps.mail.name', + route: '/apps/mail', + }) expect( PHONE_APPS.filter((app) => app.dockOrder !== null) .sort((a, b) => (a.dockOrder ?? 0) - (b.dockOrder ?? 0)) diff --git a/frontend/src/config/apps.ts b/frontend/src/config/apps.ts index a9469bb..b539ffc 100644 --- a/frontend/src/config/apps.ts +++ b/frontend/src/config/apps.ts @@ -3,6 +3,7 @@ import { Camera, Clock3, Images, + Mail, NotebookPen, Settings, ShoppingBag, @@ -13,6 +14,7 @@ import appStoreIcon from '@/assets/img/app-icons/apps.webp' import calculatorIcon from '@/assets/img/app-icons/calculator.webp' import cameraIcon from '@/assets/img/app-icons/camera.webp' import clockIcon from '@/assets/img/app-icons/clock.webp' +import mailIcon from '@/assets/img/app-icons/mail.webp' import notesIcon from '@/assets/img/app-icons/notes.webp' import photosIcon from '@/assets/img/app-icons/gallery.webp' import settingsIcon from '@/assets/img/app-icons/settings.webp' @@ -21,10 +23,23 @@ import type { PhoneAppDefinition, PhoneAppId } from '@/types/apps' export const PHONE_APPS: PhoneAppDefinition[] = [ { component: markRaw( - defineAsyncComponent(() => import('@/views/apps/NotesApp.vue')), + defineAsyncComponent(() => import('@/views/apps/MailApp.vue')), ), dockOrder: null, gridOrder: 3, + icon: markRaw(Mail), + iconClass: '', + iconImage: mailIcon, + id: 'mail', + labelKey: 'Apps.mail.name', + route: '/apps/mail', + }, + { + component: markRaw( + defineAsyncComponent(() => import('@/views/apps/NotesApp.vue')), + ), + dockOrder: null, + gridOrder: 4, icon: markRaw(NotebookPen), iconClass: '', iconImage: notesIcon, @@ -76,7 +91,7 @@ export const PHONE_APPS: PhoneAppDefinition[] = [ defineAsyncComponent(() => import('@/views/apps/PhotosApp.vue')), ), dockOrder: null, - gridOrder: 4, + gridOrder: 5, icon: markRaw(Images), iconClass: 'app-icon--photos', iconImage: photosIcon, @@ -89,7 +104,7 @@ export const PHONE_APPS: PhoneAppDefinition[] = [ defineAsyncComponent(() => import('@/views/apps/AppStoreApp.vue')), ), dockOrder: 0, - gridOrder: 5, + gridOrder: 6, icon: markRaw(ShoppingBag), iconClass: 'app-icon--store', iconImage: appStoreIcon, @@ -102,7 +117,7 @@ export const PHONE_APPS: PhoneAppDefinition[] = [ defineAsyncComponent(() => import('@/views/apps/SettingsApp.vue')), ), dockOrder: null, - gridOrder: 6, + gridOrder: 7, icon: markRaw(Settings), iconClass: 'app-icon--settings', iconImage: settingsIcon, diff --git a/frontend/src/stores/mail.test.ts b/frontend/src/stores/mail.test.ts new file mode 100644 index 0000000..f2c2295 --- /dev/null +++ b/frontend/src/stores/mail.test.ts @@ -0,0 +1,112 @@ +import { createPinia, setActivePinia } from 'pinia' +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { useMailStore } from '@/stores/mail' +import type { MailCounts, MailListItem } from '@/types/mail' +import { nuiCall } from '@/utils/nui' + +vi.mock('@/utils/nui', () => ({ + nuiCall: vi.fn(), +})) + +const mockNuiCall = vi.mocked(nuiCall) +const counts: MailCounts = { + drafts: 1, + inbox: 2, + sent: 3, + trash: 4, + unread: 1, +} + +function listItem(id: number): MailListItem { + return { + created_at: '2026-08-04 10:00:00', + folder: 'inbox', + id, + is_read: false, + message_id: `00000000-0000-0000-0000-${String(id).padStart(12, '0')}`, + preview: 'Body', + recipients: ['alex@ifruit.com'], + sender: 'morgan@ifruit.com', + subject: 'Plans', + } +} + +describe('mail store', () => { + beforeEach(() => { + setActivePinia(createPinia()) + mockNuiCall.mockReset() + }) + + it('tracks search state and appends paginated mailbox results', async () => { + mockNuiCall + .mockResolvedValueOnce({ + data: { hasMore: true, items: [listItem(1)] }, + success: true, + }) + .mockResolvedValueOnce({ + data: { hasMore: false, items: [listItem(2)] }, + success: true, + }) + + const mail = useMailStore() + await mail.loadFolder('inbox', 'plans') + await mail.loadFolder('inbox', 'plans', true) + + expect(mail.search).toBe('plans') + expect(mail.items.map((item) => item.id)).toEqual([1, 2]) + expect(mail.hasMore).toBe(false) + expect(mockNuiCall).toHaveBeenNthCalledWith(2, 'mail:list', { + folder: 'inbox', + offset: 1, + search: 'plans', + }) + }) + + it('saves draft content and refreshes mailbox counts', async () => { + mockNuiCall + .mockResolvedValueOnce({ data: { id: 'draft-id' }, success: true }) + .mockResolvedValueOnce({ data: counts, success: true }) + + const mail = useMailStore() + const id = await mail.saveDraft({ + body: 'Body', + recipients: ['alex@ifruit.com'], + subject: 'Subject', + }) + + expect(id).toBe('draft-id') + expect(mail.counts).toEqual(counts) + expect(mockNuiCall).toHaveBeenNthCalledWith(1, 'mail:save-draft', { + body: 'Body', + id: undefined, + recipients: ['alex@ifruit.com'], + subject: 'Subject', + }) + }) + + it('logs out the active session and resets mailbox state', async () => { + mockNuiCall + .mockResolvedValueOnce({ + data: { counts, email: 'alex@ifruit.com' }, + success: true, + }) + .mockResolvedValueOnce({ + data: { hasMore: true, items: [listItem(1)] }, + success: true, + }) + .mockResolvedValueOnce({ success: true }) + + const mail = useMailStore() + await mail.login('alex', 'secret') + await mail.loadFolder('sent', 'plans') + await mail.logout() + + expect(mockNuiCall).toHaveBeenLastCalledWith('mail:logout') + expect(mail.accountEmail).toBe('') + expect(mail.folder).toBe('inbox') + expect(mail.search).toBe('') + expect(mail.items).toEqual([]) + expect(mail.hasMore).toBe(false) + }) +}) diff --git a/frontend/src/stores/mail.ts b/frontend/src/stores/mail.ts new file mode 100644 index 0000000..14ea9cc --- /dev/null +++ b/frontend/src/stores/mail.ts @@ -0,0 +1,185 @@ +import { defineStore } from 'pinia' +import { ref } from 'vue' + +import type { + MailComposeDraft, + MailCounts, + MailDraft, + MailFolder, + MailListItem, + MailListResponse, + MailMessage, + MailSession, +} from '@/types/mail' +import { nuiCall } from '@/utils/nui' + +const emptyCounts = (): MailCounts => ({ + drafts: 0, + inbox: 0, + sent: 0, + trash: 0, + unread: 0, +}) + +export const useMailStore = defineStore('mail', () => { + const accountEmail = ref('') + const counts = ref(emptyCounts()) + const folder = ref('inbox') + const hasMore = ref(false) + const items = ref([]) + const loading = ref(false) + const search = ref('') + + function applySession(session: MailSession): void { + accountEmail.value = session.email + counts.value = session.counts + } + + async function login(email: string, password: string) { + const response = await nuiCall('mail:login', { + email, + password, + }) + if (response.success && response.data) applySession(response.data) + return response + } + + async function register(email: string, password: string) { + const response = await nuiCall('mail:register', { + email, + password, + }) + if (response.success && response.data) applySession(response.data) + return response + } + + async function logout(): Promise { + if (accountEmail.value) await nuiCall('mail:logout') + accountEmail.value = '' + counts.value = emptyCounts() + items.value = [] + hasMore.value = false + folder.value = 'inbox' + search.value = '' + } + + async function loadFolder( + nextFolder: MailFolder, + nextSearch = '', + append = false, + ): Promise { + loading.value = true + const offset = append ? items.value.length : 0 + const response = await nuiCall('mail:list', { + folder: nextFolder, + offset, + search: nextSearch, + }) + loading.value = false + if (!response.success || !response.data) return false + + folder.value = nextFolder + search.value = nextSearch + items.value = append + ? [...items.value, ...response.data.items] + : response.data.items + hasMore.value = response.data.hasMore + return true + } + + async function refreshCounts(): Promise { + const response = await nuiCall('mail:counts') + if (response.success && response.data) counts.value = response.data + } + + async function openMessage(id: number): Promise { + const response = await nuiCall('mail:get', { id }) + if (!response.success || !response.data) return null + await refreshCounts() + return response.data + } + + async function openDraft(id: string): Promise { + const response = await nuiCall('mail:get-draft', { id }) + return response.success && response.data ? response.data : null + } + + async function saveDraft(draft: MailComposeDraft): Promise { + const response = await nuiCall<{ id: string }>('mail:save-draft', { + body: draft.body, + id: draft.id, + recipients: draft.recipients, + subject: draft.subject, + }) + if (response.success && response.data) { + await refreshCounts() + return response.data.id + } + return null + } + + async function deleteDraft(id: string): Promise { + const response = await nuiCall('mail:delete-draft', { id }) + if (response.success) await refreshCounts() + return response.success + } + + async function send(draft: MailComposeDraft) { + const response = await nuiCall<{ id: string }>('mail:send', { + body: draft.body, + draftId: draft.id, + recipients: draft.recipients, + subject: draft.subject, + }) + if (response.success) await refreshCounts() + return response + } + + async function mutateEntry( + endpoint: string, + id: number, + extra: Record = {}, + ): Promise { + const response = await nuiCall(endpoint, { id, ...extra }) + if (response.success) { + await Promise.all([ + refreshCounts(), + loadFolder(folder.value, search.value), + ]) + } + return response.success + } + + async function emptyTrash(): Promise { + const response = await nuiCall('mail:empty-trash') + if (response.success) { + await Promise.all([refreshCounts(), loadFolder('trash', search.value)]) + } + return response.success + } + + return { + accountEmail, + counts, + deleteDraft, + emptyTrash, + folder, + hasMore, + items, + loadFolder, + loading, + login, + logout, + mutateEntry, + openDraft, + openMessage, + refreshCounts, + register, + saveDraft, + search, + send, + setCounts(nextCounts: MailCounts) { + counts.value = nextCounts + }, + } +}) diff --git a/frontend/src/stores/phone.ts b/frontend/src/stores/phone.ts index fe34e67..fd6c5df 100644 --- a/frontend/src/stores/phone.ts +++ b/frontend/src/stores/phone.ts @@ -136,6 +136,69 @@ const defaultLocales: LocaleTree = { ringing: 'Timer', }, }, + mail: { + name: 'Mail', + login: 'Sign In', + register: 'Create Account', + registerLink: 'Register', + loginTitle: 'iFruit Mail', + loginBody: 'Sign in to use your shared iFruit mailbox.', + registerBody: 'Choose your new @ifruit.com address.', + localPart: 'Email address', + email: 'Email', + password: 'Password', + confirmPassword: 'Confirm password', + passwordWarning: + 'Use an in-character password. Do not reuse a real-world password.', + mailboxes: 'Mailboxes', + inbox: 'Inbox', + sent: 'Sent', + drafts: 'Drafts', + trash: 'Trash', + compose: 'New Message', + recipients: 'To', + recipientHint: 'Separate up to 10 addresses with commas.', + subject: 'Subject', + body: 'Message', + search: 'Search mail', + noMail: 'No Mail', + noMailBody: 'Messages in this mailbox will appear here.', + noResults: 'No Results', + noResultsBody: 'Try a different search.', + untitled: '(no subject)', + loadMore: 'Load More', + logout: 'Sign Out', + from: 'From', + to: 'To', + reply: 'Reply', + replyAll: 'Reply All', + forward: 'Forward', + markUnread: 'Mark as Unread', + moveToTrash: 'Move to Trash', + restore: 'Restore', + deleteForever: 'Delete Forever', + emptyTrash: 'Empty Trash', + emptyTrashTitle: 'Empty Trash?', + emptyTrashBody: 'Every message in Trash will be permanently deleted.', + deleteDraft: 'Delete Draft', + sentSuccess: 'Message sent.', + newMessage: 'New mail from {sender}', + passwordsMismatch: 'Passwords do not match.', + errors: { + invalid_email: 'Choose a valid 3–32 character iFruit address.', + invalid_password: 'Password must be 6–64 characters.', + invalid_credentials: 'Email or password is incorrect.', + email_taken: 'That iFruit address is already registered.', + rate_limited: 'Too many attempts. Try again in a minute.', + invalid_message: 'Add a valid recipient and a subject or message.', + recipient_not_found: 'One or more recipients do not exist.', + invalid_draft: 'This draft contains invalid content.', + not_authenticated: 'Your mail session has ended. Sign in again.', + request_failed: 'Mail is temporarily unavailable.', + invalid_request: 'The mail request was invalid.', + default: 'The mail request failed.', + }, + }, notes: { name: 'Notes', note: 'Note', @@ -157,8 +220,6 @@ const defaultLocales: LocaleTree = { pin: 'Pin note', unpin: 'Unpin note', deleteNote: 'Delete note', - deleteTitle: 'Delete Note?', - deleteBody: 'This note will be permanently deleted.', }, photos: { name: 'Photos', @@ -275,6 +336,7 @@ const defaultLocales: LocaleTree = { add: 'Add', cancel: 'Cancel', close: 'Close', + back: 'Back', delete: 'Delete', done: 'Done', edit: 'Edit', @@ -283,8 +345,10 @@ const defaultLocales: LocaleTree = { phone: 'Phone', phoneStatus: 'Phone status', reset: 'Reset', + loading: 'Loading', search: 'Search', save: 'Save', + send: 'Send', start: 'Start', stop: 'Stop', }, diff --git a/frontend/src/types/apps.ts b/frontend/src/types/apps.ts index 377fd57..a199781 100644 --- a/frontend/src/types/apps.ts +++ b/frontend/src/types/apps.ts @@ -4,6 +4,7 @@ export type PhoneAppId = | 'calculator' | 'camera' | 'clock' + | 'mail' | 'notes' | 'photos' | 'app-store' diff --git a/frontend/src/types/mail.ts b/frontend/src/types/mail.ts new file mode 100644 index 0000000..2815d2a --- /dev/null +++ b/frontend/src/types/mail.ts @@ -0,0 +1,59 @@ +export type MailFolder = 'inbox' | 'sent' | 'drafts' | 'trash' + +export type MailCounts = { + drafts: number + inbox: number + sent: number + trash: number + unread: number +} + +export type MailListItem = { + body?: string + created_at: string + folder?: 'inbox' | 'sent' + id: number | string + is_read?: boolean + message_id?: string + preview: string + recipients: string[] + sender?: string + subject: string + trashed_at?: string | null + updated_at?: string +} + +export type MailMessage = MailListItem & { + body: string + folder: 'inbox' | 'sent' + id: number + message_id: string + sender: string +} + +export type MailDraft = { + body: string + created_at: string + id: string + recipients: string[] + subject: string + updated_at: string +} + +export type MailSession = { + counts: MailCounts + email: string +} + +export type MailListResponse = { + hasMore: boolean + items: MailListItem[] + offset: number +} + +export type MailComposeDraft = { + body: string + id?: string + recipients: string[] + subject: string +} diff --git a/frontend/src/utils/mail.test.ts b/frontend/src/utils/mail.test.ts new file mode 100644 index 0000000..33745bf --- /dev/null +++ b/frontend/src/utils/mail.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from 'vitest' + +import type { MailMessage } from '@/types/mail' +import { + buildForwardDraft, + buildReplyDraft, + normalizeMailAddress, + parseMailRecipients, +} from '@/utils/mail' + +const message: MailMessage = { + body: 'Meet at Legion Square.', + created_at: '2026-08-04 10:00:00', + folder: 'inbox', + id: 1, + is_read: true, + message_id: '00000000-0000-0000-0000-000000000001', + preview: 'Meet at Legion Square.', + recipients: ['alex@ifruit.com', 'jamie@ifruit.com'], + sender: 'morgan@ifruit.com', + subject: 'Plans', +} + +describe('mail addresses', () => { + it('normalizes local parts and the iFruit domain', () => { + expect(normalizeMailAddress(' Sky.User ')).toBe('sky.user@ifruit.com') + expect(normalizeMailAddress('sky.user@ifruit.com')).toBe( + 'sky.user@ifruit.com', + ) + }) + + it('rejects invalid addresses and deduplicates recipients', () => { + expect(normalizeMailAddress('ab')).toBeNull() + expect(normalizeMailAddress('.user')).toBeNull() + expect(normalizeMailAddress('user@example.com')).toBeNull() + expect(parseMailRecipients('Alex, alex@ifruit.com; Jamie')).toEqual([ + 'alex@ifruit.com', + 'jamie@ifruit.com', + ]) + }) +}) + +describe('mail compose helpers', () => { + it('builds reply and reply-all drafts without the current account', () => { + expect(buildReplyDraft(message, 'alex@ifruit.com').recipients).toEqual([ + 'morgan@ifruit.com', + ]) + expect( + buildReplyDraft(message, 'alex@ifruit.com', true).recipients, + ).toEqual(['morgan@ifruit.com', 'jamie@ifruit.com']) + }) + + it('builds forward content and avoids duplicate subject prefixes', () => { + const forwarded = buildForwardDraft(message) + expect(forwarded.recipients).toEqual([]) + expect(forwarded.subject).toBe('Fwd: Plans') + expect(forwarded.body).toContain('From: morgan@ifruit.com') + }) +}) diff --git a/frontend/src/utils/mail.ts b/frontend/src/utils/mail.ts new file mode 100644 index 0000000..fb044de --- /dev/null +++ b/frontend/src/utils/mail.ts @@ -0,0 +1,77 @@ +import type { MailComposeDraft, MailMessage } from '@/types/mail' + +export const MAIL_DOMAIN = 'ifruit.com' +export const MAIL_MAX_RECIPIENTS = 10 + +export function normalizeMailAddress(value: string): string | null { + const normalized = value.trim().toLocaleLowerCase('en-US') + const localPart = normalized.includes('@') + ? normalized.endsWith(`@${MAIL_DOMAIN}`) + ? normalized.slice(0, -MAIL_DOMAIN.length - 1) + : '' + : normalized + + if ( + localPart.length < 3 || + localPart.length > 32 || + !/^[a-z0-9][a-z0-9._-]*[a-z0-9]$/.test(localPart) || + localPart.includes('..') + ) { + return null + } + + return `${localPart}@${MAIL_DOMAIN}` +} + +export function parseMailRecipients(value: string): string[] | null { + const parts = value + .split(/[;,]/) + .map((part) => part.trim()) + .filter(Boolean) + if (!parts.length || parts.length > MAIL_MAX_RECIPIENTS) return null + + const recipients: string[] = [] + const seen = new Set() + for (const part of parts) { + const email = normalizeMailAddress(part) + if (!email) return null + if (!seen.has(email)) { + seen.add(email) + recipients.push(email) + } + } + return recipients +} + +function prefixedSubject(prefix: 'Re' | 'Fwd', subject: string): string { + const clean = subject.trim() + if (new RegExp(`^${prefix}:`, 'i').test(clean)) return clean + return `${prefix}: ${clean || '(no subject)'}` +} + +export function buildReplyDraft( + message: MailMessage, + ownEmail: string, + replyAll = false, +): MailComposeDraft { + const candidates = replyAll + ? [message.sender, ...message.recipients] + : [message.sender] + const recipients = [...new Set(candidates)].filter( + (address) => address !== ownEmail, + ) + + return { + body: `\n--- ${message.sender} ---\n${message.body}`, + recipients, + subject: prefixedSubject('Re', message.subject), + } +} + +export function buildForwardDraft(message: MailMessage): MailComposeDraft { + return { + body: `\n--- Forwarded message ---\nFrom: ${message.sender}\nTo: ${message.recipients.join(', ')}\n\n${message.body}`, + recipients: [], + subject: prefixedSubject('Fwd', message.subject), + } +} diff --git a/frontend/src/utils/nui.ts b/frontend/src/utils/nui.ts index 5b03a5d..d189ddd 100644 --- a/frontend/src/utils/nui.ts +++ b/frontend/src/utils/nui.ts @@ -10,8 +10,11 @@ export async function nuiCall( endpoint: string, data: Record = {}, ): Promise> { + const developmentPort = new URLSearchParams(window.location.search).get( + 'apiPort', + ) const baseUrl = import.meta.env.DEV - ? 'http://localhost:3001/api' + ? `http://localhost:${developmentPort ?? '3001'}/api` : `https://${resourceName}` try { diff --git a/frontend/src/utils/preferences.test.ts b/frontend/src/utils/preferences.test.ts index e67e753..bf46e55 100644 --- a/frontend/src/utils/preferences.test.ts +++ b/frontend/src/utils/preferences.test.ts @@ -31,6 +31,10 @@ describe('preferences', () => { sounds: false, }) expect(value.settings.notifications.clock.enabled).toBe(true) + expect(value.settings.notifications.mail).toEqual({ + enabled: true, + sounds: true, + }) expect(value.settings.phoneScale).toBe(110) expect(value.settings.wallpaper).toBe('ember') }) diff --git a/frontend/src/utils/preferences.ts b/frontend/src/utils/preferences.ts index e929633..ac29aa6 100644 --- a/frontend/src/utils/preferences.ts +++ b/frontend/src/utils/preferences.ts @@ -50,6 +50,7 @@ const DEFAULT_APP_NOTIFICATIONS: Record< calculator: { enabled: true, sounds: true }, camera: { enabled: true, sounds: true }, clock: { enabled: true, sounds: true }, + mail: { enabled: true, sounds: true }, notes: { enabled: true, sounds: true }, photos: { enabled: true, sounds: true }, settings: { enabled: true, sounds: true }, diff --git a/frontend/src/views/apps/MailApp.vue b/frontend/src/views/apps/MailApp.vue new file mode 100644 index 0000000..cbaa8f7 --- /dev/null +++ b/frontend/src/views/apps/MailApp.vue @@ -0,0 +1,733 @@ + + + diff --git a/frontend/src/views/apps/NotesApp.vue b/frontend/src/views/apps/NotesApp.vue index 8e382e2..be37898 100644 --- a/frontend/src/views/apps/NotesApp.vue +++ b/frontend/src/views/apps/NotesApp.vue @@ -1,12 +1,7 @@