Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel

feat: Frontend, cobalt command and more!

+3 -1
README.md
··· 1 - # Aethel Bot 1 + # Aethel Monorepo 2 + 3 + This monorepo includes both the frontend and backend of the Aethel Discord bot. 2 4 3 5 [![Node.js](https://img.shields.io/badge/node-%3E=16.9.0-green?logo=node.js)](https://nodejs.org/) 4 6
+42
locales/en-US.json
··· 197 197 "done_title": "Done", 198 198 "none": "—" 199 199 }, 200 + "cobalt": { 201 + "name": "cobalt", 202 + "description": "Download a video or audio from a given URL", 203 + "options": { 204 + "url": { 205 + "name": "url", 206 + "description": "The URL of the video to download" 207 + }, 208 + "video_quality": { 209 + "name": "video-quality", 210 + "description": "The video quality to download" 211 + }, 212 + "audio_only": { 213 + "name": "audio-only", 214 + "description": "Download audio only" 215 + }, 216 + "mute_audio": { 217 + "name": "mute-audio", 218 + "description": "Mute audio" 219 + }, 220 + "twitter_gif": { 221 + "name": "twitter-gif", 222 + "description": "Download as Twitter GIF" 223 + }, 224 + "tiktok_original_audio": { 225 + "name": "tiktok-original-audio", 226 + "description": "Include TikTok original audio" 227 + }, 228 + "audio_format": { 229 + "name": "audio-format", 230 + "description": "Format for audio (requires audio-only to be true)" 231 + } 232 + }, 233 + "button_label": "Download", 234 + "success": "✅ **Download ready!**\n\n🔗 {url}\n📥 **Click the button below to download your file.**", 235 + "error": "❌ **Error:** {error}", 236 + "unknown_error": "An unknown error occurred.", 237 + "rate_limit": "⏳ **Rate limit exceeded.** Please try again later.", 238 + "unknown_response": "❓ **Unknown response from the API.** Please try again.", 239 + "multiple_items": "📋 **Multiple items found for this URL.** Please provide a more specific URL.", 240 + "local_processing_not_supported": "⚠️ **Local processing is not supported.** Please try a different URL or option." 241 + }, 200 242 "time": { 201 243 "name": "time", 202 244 "description": "Get the current time for a timezone",
+8 -2
package.json
··· 19 19 }, 20 20 "dependencies": { 21 21 "@discordjs/rest": "^2.5.1", 22 + "axios": "^1.10.0", 22 23 "city-timezones": "^1.3.1", 23 24 "cors": "^2.8.5", 24 25 "discord.js": "^14.21.0", 25 26 "dotenv": "^16.6.1", 26 27 "eslint-plugin-prettier": "^5.5.1", 27 - "express": "^5.1.0", 28 + "express": "^4.21.2", 28 29 "express-rate-limit": "^7.5.1", 30 + "express-validator": "^7.2.1", 29 31 "helmet": "^8.1.0", 32 + "jsonwebtoken": "^9.0.2", 30 33 "moment-timezone": "^0.6.0", 31 34 "node-fetch": "^3.3.2", 32 35 "pg": "^8.16.3", 36 + "uuid": "^11.1.0", 33 37 "validator": "^13.15.15", 34 38 "whois-json": "^2.0.4", 35 39 "winston": "^3.17.0" ··· 37 41 "devDependencies": { 38 42 "@eslint/js": "^9.30.0", 39 43 "@types/cors": "^2.8.19", 40 - "@types/express": "^5.0.3", 44 + "@types/express": "^4.17.23", 45 + "@types/jsonwebtoken": "^9.0.10", 41 46 "@types/node": "^24.0.7", 42 47 "@types/pg": "^8.15.4", 48 + "@types/uuid": "^10.0.0", 43 49 "@types/validator": "^13.15.2", 44 50 "@types/whois-json": "^2.0.4", 45 51 "eslint": "^9.30.0",
+395 -157
pnpm-lock.yaml
··· 11 11 '@discordjs/rest': 12 12 specifier: ^2.5.1 13 13 version: 2.5.1 14 + axios: 15 + specifier: ^1.10.0 16 + version: 1.10.0 14 17 city-timezones: 15 18 specifier: ^1.3.1 16 19 version: 1.3.1 ··· 27 30 specifier: ^5.5.1 28 31 version: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1))(eslint@9.30.1)(prettier@3.6.2) 29 32 express: 30 - specifier: ^5.1.0 31 - version: 5.1.0 33 + specifier: ^4.21.2 34 + version: 4.21.2 32 35 express-rate-limit: 33 36 specifier: ^7.5.1 34 - version: 7.5.1(express@5.1.0) 37 + version: 7.5.1(express@4.21.2) 38 + express-validator: 39 + specifier: ^7.2.1 40 + version: 7.2.1 35 41 helmet: 36 42 specifier: ^8.1.0 37 43 version: 8.1.0 44 + jsonwebtoken: 45 + specifier: ^9.0.2 46 + version: 9.0.2 38 47 moment-timezone: 39 48 specifier: ^0.6.0 40 49 version: 0.6.0 ··· 44 53 pg: 45 54 specifier: ^8.16.3 46 55 version: 8.16.3 56 + uuid: 57 + specifier: ^11.1.0 58 + version: 11.1.0 47 59 validator: 48 60 specifier: ^13.15.15 49 61 version: 13.15.15 ··· 61 73 specifier: ^2.8.19 62 74 version: 2.8.19 63 75 '@types/express': 64 - specifier: ^5.0.3 65 - version: 5.0.3 76 + specifier: ^4.17.23 77 + version: 4.17.23 78 + '@types/jsonwebtoken': 79 + specifier: ^9.0.10 80 + version: 9.0.10 66 81 '@types/node': 67 82 specifier: ^24.0.7 68 83 version: 24.0.12 69 84 '@types/pg': 70 85 specifier: ^8.15.4 71 86 version: 8.15.4 87 + '@types/uuid': 88 + specifier: ^10.0.0 89 + version: 10.0.0 72 90 '@types/validator': 73 91 specifier: ^13.15.2 74 92 version: 13.15.2 ··· 405 423 '@types/estree@1.0.8': 406 424 resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} 407 425 408 - '@types/express-serve-static-core@5.0.7': 409 - resolution: {integrity: sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==} 426 + '@types/express-serve-static-core@4.19.6': 427 + resolution: {integrity: sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==} 410 428 411 - '@types/express@5.0.3': 412 - resolution: {integrity: sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==} 429 + '@types/express@4.17.23': 430 + resolution: {integrity: sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==} 413 431 414 432 '@types/http-errors@2.0.5': 415 433 resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} ··· 417 435 '@types/json-schema@7.0.15': 418 436 resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 419 437 438 + '@types/jsonwebtoken@9.0.10': 439 + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} 440 + 420 441 '@types/mime@1.3.5': 421 442 resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} 443 + 444 + '@types/ms@2.1.0': 445 + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} 422 446 423 447 '@types/node@24.0.12': 424 448 resolution: {integrity: sha512-LtOrbvDf5ndC9Xi+4QZjVL0woFymF/xSTKZKPgrrl7H7XoeDvnD+E2IclKVDyaK9UM756W/3BXqSU+JEHopA9g==} ··· 440 464 441 465 '@types/triple-beam@1.3.5': 442 466 resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} 467 + 468 + '@types/uuid@10.0.0': 469 + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} 443 470 444 471 '@types/validator@13.15.2': 445 472 resolution: {integrity: sha512-y7pa/oEJJ4iGYBxOpfAKn5b9+xuihvzDVnC/OSvlVnGxVg0pOqmjiMafiJ1KVNQEaPZf9HsEp5icEwGg8uIe5Q==} ··· 513 540 resolution: {integrity: sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==} 514 541 engines: {node: '>=v14.0.0', npm: '>=7.0.0'} 515 542 516 - accepts@2.0.0: 517 - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} 543 + accepts@1.3.8: 544 + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} 518 545 engines: {node: '>= 0.6'} 519 546 520 547 acorn-jsx@5.3.2: ··· 545 572 argparse@2.0.1: 546 573 resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 547 574 575 + array-flatten@1.1.1: 576 + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} 577 + 548 578 array-union@2.1.0: 549 579 resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 550 580 engines: {node: '>=8'} 551 581 552 582 async@3.2.6: 553 583 resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} 584 + 585 + asynckit@0.4.0: 586 + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 587 + 588 + axios@1.10.0: 589 + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} 554 590 555 591 balanced-match@1.0.2: 556 592 resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} ··· 559 595 resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 560 596 engines: {node: '>=8'} 561 597 562 - body-parser@2.2.0: 563 - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} 564 - engines: {node: '>=18'} 598 + body-parser@1.20.3: 599 + resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} 600 + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 565 601 566 602 brace-expansion@1.1.12: 567 603 resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} ··· 572 608 braces@3.0.3: 573 609 resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 574 610 engines: {node: '>=8'} 611 + 612 + buffer-equal-constant-time@1.0.1: 613 + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} 575 614 576 615 bytes@3.1.2: 577 616 resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} ··· 635 674 colorspace@1.1.4: 636 675 resolution: {integrity: sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==} 637 676 677 + combined-stream@1.0.8: 678 + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 679 + engines: {node: '>= 0.8'} 680 + 638 681 commander@9.5.0: 639 682 resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} 640 683 engines: {node: ^12.20.0 || >=14} ··· 645 688 constant-case@2.0.0: 646 689 resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==} 647 690 648 - content-disposition@1.0.0: 649 - resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} 691 + content-disposition@0.5.4: 692 + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} 650 693 engines: {node: '>= 0.6'} 651 694 652 695 content-type@1.0.5: 653 696 resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} 654 697 engines: {node: '>= 0.6'} 655 698 656 - cookie-signature@1.2.2: 657 - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} 658 - engines: {node: '>=6.6.0'} 699 + cookie-signature@1.0.6: 700 + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} 659 701 660 - cookie@0.7.2: 661 - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} 702 + cookie@0.7.1: 703 + resolution: {integrity: sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==} 662 704 engines: {node: '>= 0.6'} 663 705 664 706 cors@2.8.5: ··· 673 715 resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} 674 716 engines: {node: '>= 12'} 675 717 718 + debug@2.6.9: 719 + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} 720 + peerDependencies: 721 + supports-color: '*' 722 + peerDependenciesMeta: 723 + supports-color: 724 + optional: true 725 + 676 726 debug@4.4.1: 677 727 resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} 678 728 engines: {node: '>=6.0'} ··· 691 741 692 742 deep-is@0.1.4: 693 743 resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 744 + 745 + delayed-stream@1.0.0: 746 + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 747 + engines: {node: '>=0.4.0'} 694 748 695 749 depd@2.0.0: 696 750 resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} 697 751 engines: {node: '>= 0.8'} 698 752 753 + destroy@1.2.0: 754 + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} 755 + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} 756 + 699 757 dir-glob@3.0.1: 700 758 resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 701 759 engines: {node: '>=8'} ··· 718 776 resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 719 777 engines: {node: '>= 0.4'} 720 778 779 + ecdsa-sig-formatter@1.0.11: 780 + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} 781 + 721 782 ee-first@1.1.1: 722 783 resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} 723 784 ··· 727 788 enabled@2.0.0: 728 789 resolution: {integrity: sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==} 729 790 791 + encodeurl@1.0.2: 792 + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} 793 + engines: {node: '>= 0.8'} 794 + 730 795 encodeurl@2.0.0: 731 796 resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} 732 797 engines: {node: '>= 0.8'} ··· 741 806 742 807 es-object-atoms@1.1.1: 743 808 resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 809 + engines: {node: '>= 0.4'} 810 + 811 + es-set-tostringtag@2.1.0: 812 + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} 744 813 engines: {node: '>= 0.4'} 745 814 746 815 esbuild@0.25.6: ··· 827 896 peerDependencies: 828 897 express: '>= 4.11' 829 898 830 - express@5.1.0: 831 - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} 832 - engines: {node: '>= 18'} 899 + express-validator@7.2.1: 900 + resolution: {integrity: sha512-CjNE6aakfpuwGaHQZ3m8ltCG2Qvivd7RHtVMS/6nVxOM7xVGqr4bhflsm4+N5FP5zI7Zxp+Hae+9RE+o8e3ZOQ==} 901 + engines: {node: '>= 8.0.0'} 902 + 903 + express@4.21.2: 904 + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} 905 + engines: {node: '>= 0.10.0'} 833 906 834 907 fast-deep-equal@3.1.3: 835 908 resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} ··· 865 938 resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 866 939 engines: {node: '>=8'} 867 940 868 - finalhandler@2.1.0: 869 - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} 941 + finalhandler@1.3.1: 942 + resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} 870 943 engines: {node: '>= 0.8'} 871 944 872 945 find-up@4.1.0: ··· 886 959 887 960 fn.name@1.1.0: 888 961 resolution: {integrity: sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==} 962 + 963 + follow-redirects@1.15.9: 964 + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} 965 + engines: {node: '>=4.0'} 966 + peerDependencies: 967 + debug: '*' 968 + peerDependenciesMeta: 969 + debug: 970 + optional: true 971 + 972 + form-data@4.0.4: 973 + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} 974 + engines: {node: '>= 6'} 889 975 890 976 formdata-polyfill@4.0.10: 891 977 resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} ··· 895 981 resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 896 982 engines: {node: '>= 0.6'} 897 983 898 - fresh@2.0.0: 899 - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} 900 - engines: {node: '>= 0.8'} 984 + fresh@0.5.2: 985 + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} 986 + engines: {node: '>= 0.6'} 901 987 902 988 fsevents@2.3.3: 903 989 resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} ··· 961 1047 resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 962 1048 engines: {node: '>= 0.4'} 963 1049 1050 + has-tostringtag@1.0.2: 1051 + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} 1052 + engines: {node: '>= 0.4'} 1053 + 964 1054 hasown@2.0.2: 965 1055 resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 966 1056 engines: {node: '>= 0.4'} ··· 979 1069 resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} 980 1070 engines: {node: '>= 0.8'} 981 1071 982 - iconv-lite@0.6.3: 983 - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} 1072 + iconv-lite@0.4.24: 1073 + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} 984 1074 engines: {node: '>=0.10.0'} 985 1075 986 1076 ignore-by-default@1.0.1: ··· 1039 1129 resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 1040 1130 engines: {node: '>=0.12.0'} 1041 1131 1042 - is-promise@4.0.0: 1043 - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} 1044 - 1045 1132 is-stream@2.0.1: 1046 1133 resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 1047 1134 engines: {node: '>=8'} ··· 1073 1160 engines: {node: '>=6'} 1074 1161 hasBin: true 1075 1162 1163 + jsonwebtoken@9.0.2: 1164 + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} 1165 + engines: {node: '>=12', npm: '>=6'} 1166 + 1167 + jwa@1.4.2: 1168 + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} 1169 + 1170 + jws@3.2.2: 1171 + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} 1172 + 1076 1173 keyv@4.5.4: 1077 1174 resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 1078 1175 ··· 1091 1188 resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 1092 1189 engines: {node: '>=10'} 1093 1190 1191 + lodash.includes@4.3.0: 1192 + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} 1193 + 1194 + lodash.isboolean@3.0.3: 1195 + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} 1196 + 1197 + lodash.isinteger@4.0.4: 1198 + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} 1199 + 1200 + lodash.isnumber@3.0.3: 1201 + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} 1202 + 1203 + lodash.isplainobject@4.0.6: 1204 + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} 1205 + 1206 + lodash.isstring@4.0.1: 1207 + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} 1208 + 1094 1209 lodash.merge@4.6.2: 1095 1210 resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 1211 + 1212 + lodash.once@4.1.1: 1213 + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} 1096 1214 1097 1215 lodash.snakecase@4.1.1: 1098 1216 resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} ··· 1117 1235 resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 1118 1236 engines: {node: '>= 0.4'} 1119 1237 1120 - media-typer@1.1.0: 1121 - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} 1122 - engines: {node: '>= 0.8'} 1238 + media-typer@0.3.0: 1239 + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} 1240 + engines: {node: '>= 0.6'} 1123 1241 1124 - merge-descriptors@2.0.0: 1125 - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} 1126 - engines: {node: '>=18'} 1242 + merge-descriptors@1.0.3: 1243 + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} 1127 1244 1128 1245 merge2@1.4.1: 1129 1246 resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 1130 1247 engines: {node: '>= 8'} 1131 1248 1249 + methods@1.1.2: 1250 + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} 1251 + engines: {node: '>= 0.6'} 1252 + 1132 1253 micromatch@4.0.8: 1133 1254 resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 1134 1255 engines: {node: '>=8.6'} 1135 1256 1136 - mime-db@1.54.0: 1137 - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} 1257 + mime-db@1.52.0: 1258 + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 1138 1259 engines: {node: '>= 0.6'} 1139 1260 1140 - mime-types@3.0.1: 1141 - resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} 1261 + mime-types@2.1.35: 1262 + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 1142 1263 engines: {node: '>= 0.6'} 1264 + 1265 + mime@1.6.0: 1266 + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} 1267 + engines: {node: '>=4'} 1268 + hasBin: true 1143 1269 1144 1270 minimatch@3.1.2: 1145 1271 resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} ··· 1157 1283 moment@2.30.1: 1158 1284 resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} 1159 1285 1286 + ms@2.0.0: 1287 + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} 1288 + 1160 1289 ms@2.1.3: 1161 1290 resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1162 1291 ··· 1167 1296 natural-compare@1.4.0: 1168 1297 resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 1169 1298 1170 - negotiator@1.0.0: 1171 - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} 1299 + negotiator@0.6.3: 1300 + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} 1172 1301 engines: {node: '>= 0.6'} 1173 1302 1174 1303 no-case@2.3.2: ··· 1203 1332 on-finished@2.4.1: 1204 1333 resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} 1205 1334 engines: {node: '>= 0.8'} 1206 - 1207 - once@1.4.0: 1208 - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1209 1335 1210 1336 one-time@1.0.0: 1211 1337 resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} ··· 1259 1385 resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1260 1386 engines: {node: '>=8'} 1261 1387 1262 - path-to-regexp@8.2.0: 1263 - resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} 1264 - engines: {node: '>=16'} 1388 + path-to-regexp@0.1.12: 1389 + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} 1265 1390 1266 1391 path-type@4.0.0: 1267 1392 resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} ··· 1342 1467 resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} 1343 1468 engines: {node: '>= 0.10'} 1344 1469 1470 + proxy-from-env@1.1.0: 1471 + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 1472 + 1345 1473 pstree.remy@1.1.8: 1346 1474 resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} 1347 1475 ··· 1349 1477 resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1350 1478 engines: {node: '>=6'} 1351 1479 1352 - qs@6.14.0: 1353 - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} 1480 + qs@6.13.0: 1481 + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} 1354 1482 engines: {node: '>=0.6'} 1355 1483 1356 1484 queue-lit@1.5.2: ··· 1364 1492 resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} 1365 1493 engines: {node: '>= 0.6'} 1366 1494 1367 - raw-body@3.0.0: 1368 - resolution: {integrity: sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==} 1495 + raw-body@2.5.2: 1496 + resolution: {integrity: sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==} 1369 1497 engines: {node: '>= 0.8'} 1370 1498 1371 1499 readable-stream@3.6.2: ··· 1394 1522 resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 1395 1523 engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1396 1524 1397 - router@2.2.0: 1398 - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} 1399 - engines: {node: '>= 18'} 1400 - 1401 1525 run-parallel@1.2.0: 1402 1526 resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 1403 1527 ··· 1416 1540 engines: {node: '>=10'} 1417 1541 hasBin: true 1418 1542 1419 - send@1.2.0: 1420 - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} 1421 - engines: {node: '>= 18'} 1543 + send@0.19.0: 1544 + resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==} 1545 + engines: {node: '>= 0.8.0'} 1422 1546 1423 1547 sentence-case@2.1.1: 1424 1548 resolution: {integrity: sha512-ENl7cYHaK/Ktwk5OTD+aDbQ3uC8IByu/6Bkg+HDv8Mm+XnBnppVNalcfJTNsp1ibstKh030/JKQQWglDvtKwEQ==} 1425 1549 1426 - serve-static@2.2.0: 1427 - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} 1428 - engines: {node: '>= 18'} 1550 + serve-static@1.16.2: 1551 + resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} 1552 + engines: {node: '>= 0.8.0'} 1429 1553 1430 1554 set-blocking@2.0.0: 1431 1555 resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} ··· 1493 1617 resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} 1494 1618 engines: {node: '>= 0.8'} 1495 1619 1496 - statuses@2.0.2: 1497 - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} 1498 - engines: {node: '>= 0.8'} 1499 - 1500 1620 string-width@4.2.3: 1501 1621 resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1502 1622 engines: {node: '>=8'} ··· 1583 1703 resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 1584 1704 engines: {node: '>= 0.8.0'} 1585 1705 1586 - type-is@2.0.1: 1587 - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} 1706 + type-is@1.6.18: 1707 + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} 1588 1708 engines: {node: '>= 0.6'} 1589 1709 1590 1710 typescript-eslint@8.36.0: ··· 1627 1747 1628 1748 util-deprecate@1.0.2: 1629 1749 resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1750 + 1751 + utils-merge@1.0.1: 1752 + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} 1753 + engines: {node: '>= 0.4.0'} 1754 + 1755 + uuid@11.1.0: 1756 + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} 1757 + hasBin: true 1758 + 1759 + validator@13.12.0: 1760 + resolution: {integrity: sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==} 1761 + engines: {node: '>= 0.10'} 1630 1762 1631 1763 validator@13.15.15: 1632 1764 resolution: {integrity: sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==} ··· 1671 1803 resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} 1672 1804 engines: {node: '>=8'} 1673 1805 1674 - wrappy@1.0.2: 1675 - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1676 - 1677 1806 ws@8.18.3: 1678 1807 resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} 1679 1808 engines: {node: '>=10.0.0'} ··· 1941 2070 1942 2071 '@types/estree@1.0.8': {} 1943 2072 1944 - '@types/express-serve-static-core@5.0.7': 2073 + '@types/express-serve-static-core@4.19.6': 1945 2074 dependencies: 1946 2075 '@types/node': 24.0.12 1947 2076 '@types/qs': 6.14.0 1948 2077 '@types/range-parser': 1.2.7 1949 2078 '@types/send': 0.17.5 1950 2079 1951 - '@types/express@5.0.3': 2080 + '@types/express@4.17.23': 1952 2081 dependencies: 1953 2082 '@types/body-parser': 1.19.6 1954 - '@types/express-serve-static-core': 5.0.7 2083 + '@types/express-serve-static-core': 4.19.6 2084 + '@types/qs': 6.14.0 1955 2085 '@types/serve-static': 1.15.8 1956 2086 1957 2087 '@types/http-errors@2.0.5': {} 1958 2088 1959 2089 '@types/json-schema@7.0.15': {} 1960 2090 2091 + '@types/jsonwebtoken@9.0.10': 2092 + dependencies: 2093 + '@types/ms': 2.1.0 2094 + '@types/node': 24.0.12 2095 + 1961 2096 '@types/mime@1.3.5': {} 2097 + 2098 + '@types/ms@2.1.0': {} 1962 2099 1963 2100 '@types/node@24.0.12': 1964 2101 dependencies: ··· 1986 2123 '@types/send': 0.17.5 1987 2124 1988 2125 '@types/triple-beam@1.3.5': {} 2126 + 2127 + '@types/uuid@10.0.0': {} 1989 2128 1990 2129 '@types/validator@13.15.2': {} 1991 2130 ··· 2089 2228 2090 2229 '@vladfrangu/async_event_emitter@2.4.6': {} 2091 2230 2092 - accepts@2.0.0: 2231 + accepts@1.3.8: 2093 2232 dependencies: 2094 - mime-types: 3.0.1 2095 - negotiator: 1.0.0 2233 + mime-types: 2.1.35 2234 + negotiator: 0.6.3 2096 2235 2097 2236 acorn-jsx@5.3.2(acorn@8.15.0): 2098 2237 dependencies: ··· 2120 2259 2121 2260 argparse@2.0.1: {} 2122 2261 2262 + array-flatten@1.1.1: {} 2263 + 2123 2264 array-union@2.1.0: {} 2124 2265 2125 2266 async@3.2.6: {} 2126 2267 2268 + asynckit@0.4.0: {} 2269 + 2270 + axios@1.10.0: 2271 + dependencies: 2272 + follow-redirects: 1.15.9 2273 + form-data: 4.0.4 2274 + proxy-from-env: 1.1.0 2275 + transitivePeerDependencies: 2276 + - debug 2277 + 2127 2278 balanced-match@1.0.2: {} 2128 2279 2129 2280 binary-extensions@2.3.0: {} 2130 2281 2131 - body-parser@2.2.0: 2282 + body-parser@1.20.3: 2132 2283 dependencies: 2133 2284 bytes: 3.1.2 2134 2285 content-type: 1.0.5 2135 - debug: 4.4.1(supports-color@5.5.0) 2286 + debug: 2.6.9 2287 + depd: 2.0.0 2288 + destroy: 1.2.0 2136 2289 http-errors: 2.0.0 2137 - iconv-lite: 0.6.3 2290 + iconv-lite: 0.4.24 2138 2291 on-finished: 2.4.1 2139 - qs: 6.14.0 2140 - raw-body: 3.0.0 2141 - type-is: 2.0.1 2292 + qs: 6.13.0 2293 + raw-body: 2.5.2 2294 + type-is: 1.6.18 2295 + unpipe: 1.0.0 2142 2296 transitivePeerDependencies: 2143 2297 - supports-color 2144 2298 ··· 2154 2308 braces@3.0.3: 2155 2309 dependencies: 2156 2310 fill-range: 7.1.1 2311 + 2312 + buffer-equal-constant-time@1.0.1: {} 2157 2313 2158 2314 bytes@3.1.2: {} 2159 2315 ··· 2251 2407 color: 3.2.1 2252 2408 text-hex: 1.0.0 2253 2409 2410 + combined-stream@1.0.8: 2411 + dependencies: 2412 + delayed-stream: 1.0.0 2413 + 2254 2414 commander@9.5.0: {} 2255 2415 2256 2416 concat-map@0.0.1: {} ··· 2260 2420 snake-case: 2.1.0 2261 2421 upper-case: 1.1.3 2262 2422 2263 - content-disposition@1.0.0: 2423 + content-disposition@0.5.4: 2264 2424 dependencies: 2265 2425 safe-buffer: 5.2.1 2266 2426 2267 2427 content-type@1.0.5: {} 2268 2428 2269 - cookie-signature@1.2.2: {} 2429 + cookie-signature@1.0.6: {} 2270 2430 2271 - cookie@0.7.2: {} 2431 + cookie@0.7.1: {} 2272 2432 2273 2433 cors@2.8.5: 2274 2434 dependencies: ··· 2283 2443 2284 2444 data-uri-to-buffer@4.0.1: {} 2285 2445 2446 + debug@2.6.9: 2447 + dependencies: 2448 + ms: 2.0.0 2449 + 2286 2450 debug@4.4.1(supports-color@5.5.0): 2287 2451 dependencies: 2288 2452 ms: 2.1.3 ··· 2295 2459 2296 2460 deep-is@0.1.4: {} 2297 2461 2462 + delayed-stream@1.0.0: {} 2463 + 2298 2464 depd@2.0.0: {} 2465 + 2466 + destroy@1.2.0: {} 2299 2467 2300 2468 dir-glob@3.0.1: 2301 2469 dependencies: ··· 2334 2502 es-errors: 1.3.0 2335 2503 gopd: 1.2.0 2336 2504 2505 + ecdsa-sig-formatter@1.0.11: 2506 + dependencies: 2507 + safe-buffer: 5.2.1 2508 + 2337 2509 ee-first@1.1.1: {} 2338 2510 2339 2511 emoji-regex@8.0.0: {} 2340 2512 2341 2513 enabled@2.0.0: {} 2342 2514 2515 + encodeurl@1.0.2: {} 2516 + 2343 2517 encodeurl@2.0.0: {} 2344 2518 2345 2519 es-define-property@1.0.1: {} ··· 2349 2523 es-object-atoms@1.1.1: 2350 2524 dependencies: 2351 2525 es-errors: 1.3.0 2526 + 2527 + es-set-tostringtag@2.1.0: 2528 + dependencies: 2529 + es-errors: 1.3.0 2530 + get-intrinsic: 1.3.0 2531 + has-tostringtag: 1.0.2 2532 + hasown: 2.0.2 2352 2533 2353 2534 esbuild@0.25.6: 2354 2535 optionalDependencies: ··· 2465 2646 2466 2647 etag@1.8.1: {} 2467 2648 2468 - express-rate-limit@7.5.1(express@5.1.0): 2649 + express-rate-limit@7.5.1(express@4.21.2): 2469 2650 dependencies: 2470 - express: 5.1.0 2651 + express: 4.21.2 2471 2652 2472 - express@5.1.0: 2653 + express-validator@7.2.1: 2654 + dependencies: 2655 + lodash: 4.17.21 2656 + validator: 13.12.0 2657 + 2658 + express@4.21.2: 2473 2659 dependencies: 2474 - accepts: 2.0.0 2475 - body-parser: 2.2.0 2476 - content-disposition: 1.0.0 2660 + accepts: 1.3.8 2661 + array-flatten: 1.1.1 2662 + body-parser: 1.20.3 2663 + content-disposition: 0.5.4 2477 2664 content-type: 1.0.5 2478 - cookie: 0.7.2 2479 - cookie-signature: 1.2.2 2480 - debug: 4.4.1(supports-color@5.5.0) 2665 + cookie: 0.7.1 2666 + cookie-signature: 1.0.6 2667 + debug: 2.6.9 2668 + depd: 2.0.0 2481 2669 encodeurl: 2.0.0 2482 2670 escape-html: 1.0.3 2483 2671 etag: 1.8.1 2484 - finalhandler: 2.1.0 2485 - fresh: 2.0.0 2672 + finalhandler: 1.3.1 2673 + fresh: 0.5.2 2486 2674 http-errors: 2.0.0 2487 - merge-descriptors: 2.0.0 2488 - mime-types: 3.0.1 2675 + merge-descriptors: 1.0.3 2676 + methods: 1.1.2 2489 2677 on-finished: 2.4.1 2490 - once: 1.4.0 2491 2678 parseurl: 1.3.3 2679 + path-to-regexp: 0.1.12 2492 2680 proxy-addr: 2.0.7 2493 - qs: 6.14.0 2681 + qs: 6.13.0 2494 2682 range-parser: 1.2.1 2495 - router: 2.2.0 2496 - send: 1.2.0 2497 - serve-static: 2.2.0 2498 - statuses: 2.0.2 2499 - type-is: 2.0.1 2683 + safe-buffer: 5.2.1 2684 + send: 0.19.0 2685 + serve-static: 1.16.2 2686 + setprototypeof: 1.2.0 2687 + statuses: 2.0.1 2688 + type-is: 1.6.18 2689 + utils-merge: 1.0.1 2500 2690 vary: 1.1.2 2501 2691 transitivePeerDependencies: 2502 2692 - supports-color ··· 2536 2726 dependencies: 2537 2727 to-regex-range: 5.0.1 2538 2728 2539 - finalhandler@2.1.0: 2729 + finalhandler@1.3.1: 2540 2730 dependencies: 2541 - debug: 4.4.1(supports-color@5.5.0) 2731 + debug: 2.6.9 2542 2732 encodeurl: 2.0.0 2543 2733 escape-html: 1.0.3 2544 2734 on-finished: 2.4.1 2545 2735 parseurl: 1.3.3 2546 - statuses: 2.0.2 2736 + statuses: 2.0.1 2737 + unpipe: 1.0.0 2547 2738 transitivePeerDependencies: 2548 2739 - supports-color 2549 2740 ··· 2566 2757 2567 2758 fn.name@1.1.0: {} 2568 2759 2760 + follow-redirects@1.15.9: {} 2761 + 2762 + form-data@4.0.4: 2763 + dependencies: 2764 + asynckit: 0.4.0 2765 + combined-stream: 1.0.8 2766 + es-set-tostringtag: 2.1.0 2767 + hasown: 2.0.2 2768 + mime-types: 2.1.35 2769 + 2569 2770 formdata-polyfill@4.0.10: 2570 2771 dependencies: 2571 2772 fetch-blob: 3.2.0 2572 2773 2573 2774 forwarded@0.2.0: {} 2574 2775 2575 - fresh@2.0.0: {} 2776 + fresh@0.5.2: {} 2576 2777 2577 2778 fsevents@2.3.3: 2578 2779 optional: true ··· 2634 2835 2635 2836 has-symbols@1.1.0: {} 2636 2837 2838 + has-tostringtag@1.0.2: 2839 + dependencies: 2840 + has-symbols: 1.1.0 2841 + 2637 2842 hasown@2.0.2: 2638 2843 dependencies: 2639 2844 function-bind: 1.1.2 ··· 2655 2860 statuses: 2.0.1 2656 2861 toidentifier: 1.0.1 2657 2862 2658 - iconv-lite@0.6.3: 2863 + iconv-lite@0.4.24: 2659 2864 dependencies: 2660 2865 safer-buffer: 2.1.2 2661 2866 ··· 2700 2905 lower-case: 1.1.4 2701 2906 2702 2907 is-number@7.0.0: {} 2703 - 2704 - is-promise@4.0.0: {} 2705 2908 2706 2909 is-stream@2.0.1: {} 2707 2910 ··· 2725 2928 2726 2929 json5@2.2.3: {} 2727 2930 2931 + jsonwebtoken@9.0.2: 2932 + dependencies: 2933 + jws: 3.2.2 2934 + lodash.includes: 4.3.0 2935 + lodash.isboolean: 3.0.3 2936 + lodash.isinteger: 4.0.4 2937 + lodash.isnumber: 3.0.3 2938 + lodash.isplainobject: 4.0.6 2939 + lodash.isstring: 4.0.1 2940 + lodash.once: 4.1.1 2941 + ms: 2.1.3 2942 + semver: 7.7.2 2943 + 2944 + jwa@1.4.2: 2945 + dependencies: 2946 + buffer-equal-constant-time: 1.0.1 2947 + ecdsa-sig-formatter: 1.0.11 2948 + safe-buffer: 5.2.1 2949 + 2950 + jws@3.2.2: 2951 + dependencies: 2952 + jwa: 1.4.2 2953 + safe-buffer: 5.2.1 2954 + 2728 2955 keyv@4.5.4: 2729 2956 dependencies: 2730 2957 json-buffer: 3.0.1 ··· 2744 2971 dependencies: 2745 2972 p-locate: 5.0.0 2746 2973 2974 + lodash.includes@4.3.0: {} 2975 + 2976 + lodash.isboolean@3.0.3: {} 2977 + 2978 + lodash.isinteger@4.0.4: {} 2979 + 2980 + lodash.isnumber@3.0.3: {} 2981 + 2982 + lodash.isplainobject@4.0.6: {} 2983 + 2984 + lodash.isstring@4.0.1: {} 2985 + 2747 2986 lodash.merge@4.6.2: {} 2987 + 2988 + lodash.once@4.1.1: {} 2748 2989 2749 2990 lodash.snakecase@4.1.1: {} 2750 2991 ··· 2769 3010 2770 3011 math-intrinsics@1.1.0: {} 2771 3012 2772 - media-typer@1.1.0: {} 3013 + media-typer@0.3.0: {} 2773 3014 2774 - merge-descriptors@2.0.0: {} 3015 + merge-descriptors@1.0.3: {} 2775 3016 2776 3017 merge2@1.4.1: {} 2777 3018 3019 + methods@1.1.2: {} 3020 + 2778 3021 micromatch@4.0.8: 2779 3022 dependencies: 2780 3023 braces: 3.0.3 2781 3024 picomatch: 2.3.1 2782 3025 2783 - mime-db@1.54.0: {} 3026 + mime-db@1.52.0: {} 2784 3027 2785 - mime-types@3.0.1: 3028 + mime-types@2.1.35: 2786 3029 dependencies: 2787 - mime-db: 1.54.0 3030 + mime-db: 1.52.0 3031 + 3032 + mime@1.6.0: {} 2788 3033 2789 3034 minimatch@3.1.2: 2790 3035 dependencies: ··· 2802 3047 2803 3048 moment@2.30.1: {} 2804 3049 3050 + ms@2.0.0: {} 3051 + 2805 3052 ms@2.1.3: {} 2806 3053 2807 3054 mylas@2.1.13: {} 2808 3055 2809 3056 natural-compare@1.4.0: {} 2810 3057 2811 - negotiator@1.0.0: {} 3058 + negotiator@0.6.3: {} 2812 3059 2813 3060 no-case@2.3.2: 2814 3061 dependencies: ··· 2844 3091 on-finished@2.4.1: 2845 3092 dependencies: 2846 3093 ee-first: 1.1.1 2847 - 2848 - once@1.4.0: 2849 - dependencies: 2850 - wrappy: 1.0.2 2851 3094 2852 3095 one-time@1.0.0: 2853 3096 dependencies: ··· 2903 3146 2904 3147 path-key@3.1.1: {} 2905 3148 2906 - path-to-regexp@8.2.0: {} 3149 + path-to-regexp@0.1.12: {} 2907 3150 2908 3151 path-type@4.0.0: {} 2909 3152 ··· 2970 3213 dependencies: 2971 3214 forwarded: 0.2.0 2972 3215 ipaddr.js: 1.9.1 3216 + 3217 + proxy-from-env@1.1.0: {} 2973 3218 2974 3219 pstree.remy@1.1.8: {} 2975 3220 2976 3221 punycode@2.3.1: {} 2977 3222 2978 - qs@6.14.0: 3223 + qs@6.13.0: 2979 3224 dependencies: 2980 3225 side-channel: 1.1.0 2981 3226 ··· 2985 3230 2986 3231 range-parser@1.2.1: {} 2987 3232 2988 - raw-body@3.0.0: 3233 + raw-body@2.5.2: 2989 3234 dependencies: 2990 3235 bytes: 3.1.2 2991 3236 http-errors: 2.0.0 2992 - iconv-lite: 0.6.3 3237 + iconv-lite: 0.4.24 2993 3238 unpipe: 1.0.0 2994 3239 2995 3240 readable-stream@3.6.2: ··· 3012 3257 3013 3258 reusify@1.1.0: {} 3014 3259 3015 - router@2.2.0: 3016 - dependencies: 3017 - debug: 4.4.1(supports-color@5.5.0) 3018 - depd: 2.0.0 3019 - is-promise: 4.0.0 3020 - parseurl: 1.3.3 3021 - path-to-regexp: 8.2.0 3022 - transitivePeerDependencies: 3023 - - supports-color 3024 - 3025 3260 run-parallel@1.2.0: 3026 3261 dependencies: 3027 3262 queue-microtask: 1.2.3 ··· 3034 3269 3035 3270 semver@7.7.2: {} 3036 3271 3037 - send@1.2.0: 3272 + send@0.19.0: 3038 3273 dependencies: 3039 - debug: 4.4.1(supports-color@5.5.0) 3040 - encodeurl: 2.0.0 3274 + debug: 2.6.9 3275 + depd: 2.0.0 3276 + destroy: 1.2.0 3277 + encodeurl: 1.0.2 3041 3278 escape-html: 1.0.3 3042 3279 etag: 1.8.1 3043 - fresh: 2.0.0 3280 + fresh: 0.5.2 3044 3281 http-errors: 2.0.0 3045 - mime-types: 3.0.1 3282 + mime: 1.6.0 3046 3283 ms: 2.1.3 3047 3284 on-finished: 2.4.1 3048 3285 range-parser: 1.2.1 3049 - statuses: 2.0.2 3286 + statuses: 2.0.1 3050 3287 transitivePeerDependencies: 3051 3288 - supports-color 3052 3289 ··· 3055 3292 no-case: 2.3.2 3056 3293 upper-case-first: 1.1.2 3057 3294 3058 - serve-static@2.2.0: 3295 + serve-static@1.16.2: 3059 3296 dependencies: 3060 3297 encodeurl: 2.0.0 3061 3298 escape-html: 1.0.3 3062 3299 parseurl: 1.3.3 3063 - send: 1.2.0 3300 + send: 0.19.0 3064 3301 transitivePeerDependencies: 3065 3302 - supports-color 3066 3303 ··· 3131 3368 3132 3369 statuses@2.0.1: {} 3133 3370 3134 - statuses@2.0.2: {} 3135 - 3136 3371 string-width@4.2.3: 3137 3372 dependencies: 3138 3373 emoji-regex: 8.0.0 ··· 3220 3455 dependencies: 3221 3456 prelude-ls: 1.2.1 3222 3457 3223 - type-is@2.0.1: 3458 + type-is@1.6.18: 3224 3459 dependencies: 3225 - content-type: 1.0.5 3226 - media-typer: 1.1.0 3227 - mime-types: 3.0.1 3460 + media-typer: 0.3.0 3461 + mime-types: 2.1.35 3228 3462 3229 3463 typescript-eslint@8.36.0(eslint@9.30.1)(typescript@5.8.3): 3230 3464 dependencies: ··· 3260 3494 3261 3495 util-deprecate@1.0.2: {} 3262 3496 3497 + utils-merge@1.0.1: {} 3498 + 3499 + uuid@11.1.0: {} 3500 + 3501 + validator@13.12.0: {} 3502 + 3263 3503 validator@13.15.15: {} 3264 3504 3265 3505 vary@1.1.2: {} ··· 3313 3553 ansi-styles: 4.3.0 3314 3554 string-width: 4.2.3 3315 3555 strip-ansi: 6.0.1 3316 - 3317 - wrappy@1.0.2: {} 3318 3556 3319 3557 ws@8.18.3: {} 3320 3558
+3 -1
src/commands/fun/joke.ts
··· 140 140 ); 141 141 embed.setFooter({ text: 'Ba dum tss! 🥁' }); 142 142 await interaction.editReply({ embeds: [embed] }); 143 - } catch {} 143 + } catch (error) { 144 + console.error('Error showing punchline:', error); 145 + } 144 146 }, 3000); 145 147 } catch (error) { 146 148 await errorHandler({
+1 -1
src/commands/utilities/ai.ts
··· 482 482 if (reset) { 483 483 userConversations.delete(userId); 484 484 await interaction.reply({ 485 - content: await client.getLocaleText('commands.reset', interaction.locale), 485 + content: await client.getLocaleText('commands.ai.reset', interaction.locale), 486 486 ephemeral: true, 487 487 }); 488 488 pendingRequests.delete(userId);
+298
src/commands/utilities/cobalt.ts
··· 1 + import { 2 + SlashCommandBuilder, 3 + ActionRowBuilder, 4 + ButtonBuilder, 5 + ButtonStyle, 6 + ChatInputCommandInteraction, 7 + ApplicationIntegrationType, 8 + InteractionContextType, 9 + } from 'discord.js'; 10 + import axios from 'axios'; 11 + import { SlashCommandProps } from '../../types/command'; 12 + import BotClient from '../../services/Client'; 13 + import { createCommandLogger } from '../../utils/commandLogger'; 14 + import { createErrorHandler } from '../../utils/errorHandler'; 15 + 16 + interface CobaltResponse { 17 + status: string; 18 + url?: string; 19 + filename?: string; 20 + text?: string; 21 + error?: { 22 + code: string; 23 + message: string; 24 + }; 25 + } 26 + 27 + const commandLogger = createCommandLogger('cobalt'); 28 + const errorHandler = createErrorHandler('cobalt'); 29 + 30 + export default { 31 + data: new SlashCommandBuilder() 32 + .setName('cobalt') 33 + .setNameLocalizations({ 34 + 'es-ES': 'cobalt', 35 + 'es-419': 'cobalt', 36 + 'en-US': 'cobalt', 37 + }) 38 + .setDescription('Download a video or audio from a given URL') 39 + .setDescriptionLocalizations({ 40 + 'es-ES': 'Descarga un video o audio desde una URL', 41 + 'es-419': 'Descarga un video o audio desde una URL', 42 + 'en-US': 'Download a video or audio from a given URL', 43 + }) 44 + .addStringOption((option) => 45 + option 46 + .setName('url') 47 + .setNameLocalizations({ 48 + 'es-ES': 'url', 49 + 'es-419': 'url', 50 + 'en-US': 'url', 51 + }) 52 + .setDescription('The URL of the video to download') 53 + .setDescriptionLocalizations({ 54 + 'es-ES': 'La URL del video a descargar', 55 + 'es-419': 'La URL del video a descargar', 56 + 'en-US': 'The URL of the video to download', 57 + }) 58 + .setRequired(true) 59 + ) 60 + .addStringOption((option) => 61 + option 62 + .setName('video-quality') 63 + .setNameLocalizations({ 64 + 'es-ES': 'calidad-video', 65 + 'es-419': 'calidad-video', 66 + 'en-US': 'video-quality', 67 + }) 68 + .setDescription('The video quality to download') 69 + .setDescriptionLocalizations({ 70 + 'es-ES': 'La calidad de video a descargar', 71 + 'es-419': 'La calidad de video a descargar', 72 + 'en-US': 'The video quality to download', 73 + }) 74 + .addChoices( 75 + { name: '144p', value: '144' }, 76 + { name: '240p', value: '240' }, 77 + { name: '360p', value: '360' }, 78 + { name: '480p', value: '480' }, 79 + { name: '720p', value: '720' }, 80 + { name: '1440p', value: '1440' }, 81 + { name: '2160p (4K)', value: '2160' }, 82 + { name: '4320p (8K)', value: '4320' }, 83 + { name: 'Max', value: 'max' } 84 + ) 85 + ) 86 + .addBooleanOption((option) => 87 + option 88 + .setName('audio-only') 89 + .setNameLocalizations({ 90 + 'es-ES': 'solo-audio', 91 + 'es-419': 'solo-audio', 92 + 'en-US': 'audio-only', 93 + }) 94 + .setDescription('Download audio only') 95 + .setDescriptionLocalizations({ 96 + 'es-ES': 'Descargar solo audio', 97 + 'es-419': 'Descargar solo audio', 98 + 'en-US': 'Download audio only', 99 + }) 100 + ) 101 + .addBooleanOption((option) => 102 + option 103 + .setName('mute-audio') 104 + .setNameLocalizations({ 105 + 'es-ES': 'silenciar-audio', 106 + 'es-419': 'silenciar-audio', 107 + 'en-US': 'mute-audio', 108 + }) 109 + .setDescription('Mute audio') 110 + .setDescriptionLocalizations({ 111 + 'es-ES': 'Silenciar audio', 112 + 'es-419': 'Silenciar audio', 113 + 'en-US': 'Mute audio', 114 + }) 115 + ) 116 + .addBooleanOption((option) => 117 + option 118 + .setName('twitter-gif') 119 + .setNameLocalizations({ 120 + 'es-ES': 'gif-twitter', 121 + 'es-419': 'gif-twitter', 122 + 'en-US': 'twitter-gif', 123 + }) 124 + .setDescription('Download as Twitter GIF') 125 + .setDescriptionLocalizations({ 126 + 'es-ES': 'Descargar como GIF de Twitter', 127 + 'es-419': 'Descargar como GIF de Twitter', 128 + 'en-US': 'Download as Twitter GIF', 129 + }) 130 + ) 131 + .addBooleanOption((option) => 132 + option 133 + .setName('tiktok-original-audio') 134 + .setNameLocalizations({ 135 + 'es-ES': 'audio-original-tiktok', 136 + 'es-419': 'audio-original-tiktok', 137 + 'en-US': 'tiktok-original-audio', 138 + }) 139 + .setDescription('Include TikTok original audio') 140 + .setDescriptionLocalizations({ 141 + 'es-ES': 'Incluir audio original de TikTok', 142 + 'es-419': 'Incluir audio original de TikTok', 143 + 'en-US': 'Include TikTok original audio', 144 + }) 145 + ) 146 + .addStringOption((option) => 147 + option 148 + .setName('audio-format') 149 + .setNameLocalizations({ 150 + 'es-ES': 'formato-audio', 151 + 'es-419': 'formato-audio', 152 + 'en-US': 'audio-format', 153 + }) 154 + .setDescription('Format for audio (requires audio-only to be true)') 155 + .setDescriptionLocalizations({ 156 + 'es-ES': 'Formato para audio (requiere solo-audio activado)', 157 + 'es-419': 'Formato para audio (requiere solo-audio activado)', 158 + 'en-US': 'Format for audio (requires audio-only to be true)', 159 + }) 160 + .addChoices( 161 + { name: 'MP3', value: 'mp3' }, 162 + { name: 'OGG', value: 'ogg' }, 163 + { name: 'WAV', value: 'wav' }, 164 + { name: 'Best', value: 'best' } 165 + ) 166 + ) 167 + .setContexts([ 168 + InteractionContextType.BotDM, 169 + InteractionContextType.Guild, 170 + InteractionContextType.PrivateChannel, 171 + ]) 172 + .setIntegrationTypes(ApplicationIntegrationType.UserInstall), 173 + category: 'utilities', 174 + async execute(client: BotClient, interaction: ChatInputCommandInteraction): Promise<void> { 175 + try { 176 + commandLogger.logFromInteraction(interaction); 177 + 178 + const url = interaction.options.getString('url', true); 179 + const videoQuality = interaction.options.getString('video-quality'); 180 + const audioOnly = interaction.options.getBoolean('audio-only'); 181 + const muteAudio = interaction.options.getBoolean('mute-audio'); 182 + const twitterGif = interaction.options.getBoolean('twitter-gif'); 183 + const tiktokOriginalAudio = interaction.options.getBoolean('tiktok-original-audio'); 184 + const audioFormat = interaction.options.getString('audio-format'); 185 + 186 + await interaction.deferReply(); 187 + 188 + const requestBody = { 189 + url: url, 190 + videoQuality: videoQuality || 'max', 191 + audioFormat: audioFormat || 'mp3', 192 + downloadMode: audioOnly ? 'audio' : muteAudio ? 'mute' : 'auto', 193 + filenameStyle: 'basic', 194 + tiktokFullAudio: tiktokOriginalAudio || false, 195 + convertGif: twitterGif || false, 196 + }; 197 + 198 + const response = await axios.post('https://cobalt.aethel.xyz/', requestBody, { 199 + headers: { 200 + Accept: 'application/json', 201 + 'Content-Type': 'application/json', 202 + }, 203 + }); 204 + 205 + const data: CobaltResponse = response.data; 206 + 207 + if (data.status === 'tunnel' || data.status === 'redirect') { 208 + const downloadUrl = data.url; 209 + if (!downloadUrl) { 210 + throw new Error('No download URL received'); 211 + } 212 + 213 + const buttonLabel = await client.getLocaleText( 214 + 'commands.cobalt.button_label', 215 + interaction.locale 216 + ); 217 + const successMessage = await client.getLocaleText( 218 + 'commands.cobalt.success', 219 + interaction.locale, 220 + { 221 + url, 222 + filename: data.filename || 'download', 223 + } 224 + ); 225 + 226 + const row = new ActionRowBuilder<ButtonBuilder>().addComponents( 227 + new ButtonBuilder().setLabel(buttonLabel).setStyle(ButtonStyle.Link).setURL(downloadUrl) 228 + ); 229 + 230 + await interaction.editReply({ 231 + content: successMessage, 232 + components: [row], 233 + }); 234 + } else if (data.status === 'error') { 235 + const unknownError = await client.getLocaleText( 236 + 'commands.cobalt.unknown_error', 237 + interaction.locale 238 + ); 239 + let errorText = unknownError; 240 + 241 + if (data.error) { 242 + errorText = data.error.message || data.error.code || unknownError; 243 + } else if (data.text) { 244 + errorText = data.text; 245 + } 246 + 247 + const errorMessage = await client.getLocaleText( 248 + 'commands.cobalt.error', 249 + interaction.locale, 250 + { 251 + error: errorText, 252 + } 253 + ); 254 + 255 + await interaction.editReply({ 256 + content: errorMessage, 257 + }); 258 + } else if (data.status === 'picker') { 259 + const multipleItemsMessage = await client.getLocaleText( 260 + 'commands.cobalt.multiple_items', 261 + interaction.locale, 262 + { url } 263 + ); 264 + 265 + await interaction.editReply({ 266 + content: 267 + multipleItemsMessage || 'Multiple items found. Please provide a more specific URL.', 268 + }); 269 + } else if (data.status === 'local-processing') { 270 + const notSupportedMessage = await client.getLocaleText( 271 + 'commands.cobalt.local_processing_not_supported', 272 + interaction.locale 273 + ); 274 + 275 + await interaction.editReply({ 276 + content: 277 + notSupportedMessage || 278 + 'Local processing is not supported. Please try a different URL or option.', 279 + }); 280 + } else { 281 + const unknownResponseMessage = await client.getLocaleText( 282 + 'commands.cobalt.unknown_response', 283 + interaction.locale 284 + ); 285 + 286 + await interaction.editReply({ 287 + content: unknownResponseMessage, 288 + }); 289 + } 290 + } catch (error) { 291 + await errorHandler({ 292 + interaction, 293 + client, 294 + error: error as Error, 295 + }); 296 + } 297 + }, 298 + } as SlashCommandProps;
+79
src/commands/utilities/remind.ts
··· 24 24 completeReminder, 25 25 cleanupReminders, 26 26 ensureUserRegistered, 27 + getActiveReminders, 27 28 DatabaseError, 28 29 } from '@/utils/reminderDb'; 29 30 import { RemindCommandProps } from '@/types/command'; ··· 69 70 const activeReminders = new Map<string, ActiveReminder>(); 70 71 const commandLogger = createCommandLogger('remind'); 71 72 const errorHandler = createErrorHandler('remind'); 73 + 74 + export async function loadActiveReminders(client: BotClient) { 75 + try { 76 + const reminders = await getActiveReminders(); 77 + logger.info(`Loading ${reminders.length} active reminders from database`); 78 + 79 + for (const reminder of reminders) { 80 + const now = Date.now(); 81 + const expiresAt = new Date(reminder.expires_at).getTime(); 82 + const timeUntilExpiry = expiresAt - now; 83 + 84 + if (timeUntilExpiry <= 0) { 85 + logger.warn(`Skipping expired reminder ${reminder.reminder_id}`); 86 + continue; 87 + } 88 + 89 + const timeoutId = setTimeout( 90 + createReminderHandler(client, { 91 + ...reminder, 92 + created_at: reminder.created_at || new Date(reminder.expires_at), 93 + }), 94 + timeUntilExpiry 95 + ); 96 + 97 + activeReminders.set(reminder.reminder_id, { 98 + timeoutId, 99 + expiresAt, 100 + }); 101 + 102 + logger.debug( 103 + `Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}` 104 + ); 105 + } 106 + 107 + logger.info(`Successfully loaded ${activeReminders.size} active reminders`); 108 + } catch (error) { 109 + logger.error('Error loading active reminders:', error); 110 + } 111 + } 112 + 113 + export function scheduleReminder(client: BotClient, reminder: Reminder) { 114 + try { 115 + const now = Date.now(); 116 + const expiresAt = new Date(reminder.expires_at).getTime(); 117 + const timeUntilExpiry = expiresAt - now; 118 + 119 + if (timeUntilExpiry <= 0) { 120 + logger.warn(`Cannot schedule expired reminder ${reminder.reminder_id}`); 121 + return false; 122 + } 123 + 124 + const existingReminder = activeReminders.get(reminder.reminder_id); 125 + if (existingReminder) { 126 + clearTimeout(existingReminder.timeoutId); 127 + } 128 + 129 + const timeoutId = setTimeout( 130 + createReminderHandler(client, { 131 + ...reminder, 132 + created_at: reminder.created_at || new Date(), 133 + }), 134 + timeUntilExpiry 135 + ); 136 + 137 + activeReminders.set(reminder.reminder_id, { 138 + timeoutId, 139 + expiresAt, 140 + }); 141 + 142 + logger.info( 143 + `Scheduled reminder ${reminder.reminder_id} for ${new Date(expiresAt).toISOString()}` 144 + ); 145 + return true; 146 + } catch (error) { 147 + logger.error(`Error scheduling reminder ${reminder.reminder_id}:`, error); 148 + return false; 149 + } 150 + } 72 151 73 152 declare global { 74 153 var _reminders: Map<string, MessageInfo>;
+3
src/events/ready.ts
··· 1 1 import BotClient from '@/services/Client'; 2 2 import logger from '@/utils/logger'; 3 + import { loadActiveReminders } from '@/commands/utilities/remind'; 3 4 4 5 export default class ReadyEvent { 5 6 constructor(c: BotClient) { ··· 10 11 try { 11 12 logger.info(`Logged in as ${client.user?.username}`); 12 13 await client.application?.commands.fetch({ withLocalizations: true }); 14 + 15 + await loadActiveReminders(client); 13 16 } catch (error) { 14 17 logger.error('Error during ready event:', error); 15 18 }
+40 -10
src/index.ts
··· 8 8 import rateLimit from 'express-rate-limit'; 9 9 import authenticateApiKey from './middlewares/verifyApiKey'; 10 10 import status from './routes/status'; 11 + import authRoutes from './routes/auth'; 12 + import todosRoutes from './routes/todos'; 13 + import apiKeysRoutes from './routes/apiKeys'; 14 + import remindersRoutes from './routes/reminders'; 11 15 import { resetOldStrikes } from './utils/userStrikes'; 12 16 13 17 config(); ··· 26 30 app.use(helmet()); 27 31 app.use( 28 32 cors({ 29 - origin: ALLOWED_ORIGINS ? ALLOWED_ORIGINS.split(',') : '*', 30 - methods: ['GET'], 31 - allowedHeaders: ['Content-Type', 'X-API-Key'], 33 + origin: ALLOWED_ORIGINS 34 + ? ALLOWED_ORIGINS.split(',') 35 + : ['http://localhost:3000', 'http://localhost:8080'], 36 + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], 37 + allowedHeaders: ['Content-Type', 'X-API-Key', 'Authorization'], 38 + credentials: true, 32 39 maxAge: 86400, 33 40 }) 34 41 ); ··· 43 50 }) 44 51 ); 45 52 53 + app.use(e.json({ limit: '10mb' })); 54 + app.use(e.urlencoded({ extended: true, limit: '10mb' })); 55 + 46 56 app.use((req, res, next) => { 47 57 res.setHeader('X-Content-Type-Options', 'nosniff'); 48 58 res.setHeader('X-Frame-Options', 'DENY'); 49 - res.setHeader('Content-Security-Policy', "default-src 'none'"); 50 - res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); 51 - res.setHeader('Pragma', 'no-cache'); 52 - res.setHeader('Expires', '0'); 53 - res.setHeader('Surrogate-Control', 'no-store'); 59 + if (req.path.startsWith('/api/') || req.path.startsWith('/status')) { 60 + res.setHeader('Content-Security-Policy', "default-src 'none'"); 61 + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate'); 62 + res.setHeader('Pragma', 'no-cache'); 63 + res.setHeader('Expires', '0'); 64 + res.setHeader('Surrogate-Control', 'no-store'); 65 + } else { 66 + res.setHeader( 67 + 'Content-Security-Policy', 68 + "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' data: https://fonts.gstatic.com; connect-src 'self' https:" 69 + ); 70 + } 54 71 next(); 55 72 }); 56 73 57 74 const bot = new BotClient(); 58 75 bot.init(); 59 76 60 - app.use(authenticateApiKey); 61 - app.use('/status', status(bot)); 77 + app.use('/api/auth', authRoutes); 78 + app.use('/api/todos', todosRoutes); 79 + app.use('/api/user/api-keys', apiKeysRoutes); 80 + app.use('/api/reminders', remindersRoutes); 81 + 82 + app.use('/status', authenticateApiKey, status(bot)); 83 + 84 + app.use(e.static('web/dist')); 85 + 86 + app.get('*', (req, res) => { 87 + if (req.path.startsWith('/api/') || req.path.startsWith('/status')) { 88 + return res.status(404).json({ error: 'Not found' }); 89 + } 90 + res.sendFile('index.html', { root: 'web/dist' }); 91 + }); 62 92 63 93 setInterval( 64 94 () => {
+58
src/middlewares/auth.ts
··· 1 + import { Request, Response, NextFunction } from 'express'; 2 + import jwt from 'jsonwebtoken'; 3 + import logger from '../utils/logger'; 4 + 5 + const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; 6 + 7 + interface JwtPayload { 8 + userId: string; 9 + username: string; 10 + discriminator: string; 11 + avatar?: string; 12 + iat?: number; 13 + exp?: number; 14 + } 15 + 16 + export const authenticateToken = (req: Request, res: Response, next: NextFunction) => { 17 + const authHeader = req.headers['authorization']; 18 + const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN 19 + 20 + if (!token) { 21 + return res.status(401).json({ error: 'Access token required' }); 22 + } 23 + 24 + try { 25 + const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; 26 + req.user = decoded; 27 + next(); 28 + } catch (error) { 29 + if (error instanceof jwt.TokenExpiredError) { 30 + logger.debug('Expired JWT token used'); 31 + return res.status(401).json({ error: 'Token expired' }); 32 + } else if (error instanceof jwt.JsonWebTokenError) { 33 + logger.debug('Invalid JWT token used'); 34 + return res.status(401).json({ error: 'Invalid token' }); 35 + } else { 36 + logger.error('JWT verification error:', error); 37 + return res.status(500).json({ error: 'Token verification failed' }); 38 + } 39 + } 40 + }; 41 + 42 + export const optionalAuth = (req: Request, res: Response, next: NextFunction) => { 43 + const authHeader = req.headers['authorization']; 44 + const token = authHeader && authHeader.split(' ')[1]; 45 + 46 + if (!token) { 47 + return next(); 48 + } 49 + 50 + try { 51 + const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload; 52 + req.user = decoded; 53 + } catch (error) { 54 + logger.debug('Optional auth token verification failed:', error); 55 + } 56 + 57 + next(); 58 + };
+268
src/routes/apiKeys.ts
··· 1 + import { Router } from 'express'; 2 + import pool from '../utils/pgClient'; 3 + import logger from '../utils/logger'; 4 + import { authenticateToken } from '../middlewares/auth'; 5 + import { body, validationResult } from 'express-validator'; 6 + import { encrypt as encryptApiKey } from '../utils/encrypt'; 7 + 8 + const router = Router(); 9 + 10 + router.use(authenticateToken); 11 + 12 + router.get('/', async (req, res) => { 13 + try { 14 + const userId = req.user?.userId; 15 + if (!userId) { 16 + return res.status(401).json({ error: 'User not authenticated' }); 17 + } 18 + 19 + const query = ` 20 + SELECT custom_model, custom_api_url, 21 + CASE WHEN api_key_encrypted IS NOT NULL THEN TRUE ELSE FALSE END as has_api_key 22 + FROM users 23 + WHERE user_id = $1 24 + `; 25 + const result = await pool.query(query, [userId]); 26 + 27 + if (result.rows.length === 0) { 28 + return res.status(404).json({ error: 'User not found' }); 29 + } 30 + 31 + const user = result.rows[0]; 32 + res.json({ 33 + hasApiKey: user.has_api_key, 34 + model: user.custom_model, 35 + apiUrl: user.custom_api_url, 36 + }); 37 + } catch (error) { 38 + logger.error('Error fetching API key info:', error); 39 + res.status(500).json({ error: 'Internal server error' }); 40 + } 41 + }); 42 + 43 + router.post( 44 + '/', 45 + body('apiKey') 46 + .trim() 47 + .isLength({ min: 1, max: 1000 }) 48 + .withMessage('API key is required and must be less than 1000 characters'), 49 + body('model') 50 + .optional() 51 + .trim() 52 + .isLength({ max: 100 }) 53 + .withMessage('Model name must be less than 100 characters'), 54 + body('apiUrl') 55 + .optional() 56 + .trim() 57 + .isURL({ require_protocol: true }) 58 + .withMessage('API URL must be a valid URL with protocol'), 59 + async (req, res) => { 60 + try { 61 + const errors = validationResult(req); 62 + if (!errors.isEmpty()) { 63 + return res.status(400).json({ error: errors.array()[0].msg }); 64 + } 65 + 66 + const userId = req.user?.userId; 67 + if (!userId) { 68 + return res.status(401).json({ error: 'User not authenticated' }); 69 + } 70 + 71 + const { apiKey, model, apiUrl } = req.body; 72 + 73 + const encryptedApiKey = encryptApiKey(apiKey); 74 + 75 + const query = ` 76 + UPDATE users 77 + SET api_key_encrypted = $1, 78 + custom_model = $2, 79 + custom_api_url = $3 80 + WHERE user_id = $4 81 + RETURNING user_id 82 + `; 83 + const result = await pool.query(query, [ 84 + encryptedApiKey, 85 + model || null, 86 + apiUrl || null, 87 + userId, 88 + ]); 89 + 90 + if (result.rows.length === 0) { 91 + return res.status(404).json({ error: 'User not found' }); 92 + } 93 + 94 + logger.info(`API key updated for user ${userId}`); 95 + res.json({ message: 'API key updated successfully' }); 96 + } catch (error) { 97 + logger.error('Error updating API key:', error); 98 + res.status(500).json({ error: 'Internal server error' }); 99 + } 100 + } 101 + ); 102 + 103 + router.put( 104 + '/', 105 + body('apiKey') 106 + .trim() 107 + .isLength({ min: 1, max: 1000 }) 108 + .withMessage('API key is required and must be less than 1000 characters'), 109 + body('model') 110 + .optional() 111 + .trim() 112 + .isLength({ max: 100 }) 113 + .withMessage('Model name must be less than 100 characters'), 114 + body('apiUrl') 115 + .optional() 116 + .trim() 117 + .isURL({ require_protocol: true }) 118 + .withMessage('API URL must be a valid URL with protocol'), 119 + async (req, res) => { 120 + try { 121 + const errors = validationResult(req); 122 + if (!errors.isEmpty()) { 123 + return res.status(400).json({ error: errors.array()[0].msg }); 124 + } 125 + 126 + const userId = req.user?.userId; 127 + if (!userId) { 128 + return res.status(401).json({ error: 'User not authenticated' }); 129 + } 130 + 131 + const { apiKey, model, apiUrl } = req.body; 132 + 133 + const encryptedApiKey = encryptApiKey(apiKey); 134 + 135 + const query = ` 136 + UPDATE users 137 + SET api_key_encrypted = $1, 138 + custom_model = $2, 139 + custom_api_url = $3 140 + WHERE user_id = $4 141 + RETURNING user_id 142 + `; 143 + const result = await pool.query(query, [ 144 + encryptedApiKey, 145 + model || null, 146 + apiUrl || null, 147 + userId, 148 + ]); 149 + 150 + if (result.rows.length === 0) { 151 + return res.status(404).json({ error: 'User not found' }); 152 + } 153 + 154 + logger.info(`API key updated for user ${userId}`); 155 + res.json({ message: 'API key updated successfully' }); 156 + } catch (error) { 157 + logger.error('Error updating API key:', error); 158 + res.status(500).json({ error: 'Internal server error' }); 159 + } 160 + } 161 + ); 162 + 163 + router.delete('/', async (req, res) => { 164 + try { 165 + const userId = req.user?.userId; 166 + if (!userId) { 167 + return res.status(401).json({ error: 'User not authenticated' }); 168 + } 169 + 170 + const query = ` 171 + UPDATE users 172 + SET api_key_encrypted = NULL, 173 + custom_model = NULL, 174 + custom_api_url = NULL 175 + WHERE user_id = $1 176 + RETURNING user_id 177 + `; 178 + const result = await pool.query(query, [userId]); 179 + 180 + if (result.rows.length === 0) { 181 + return res.status(404).json({ error: 'User not found' }); 182 + } 183 + 184 + logger.info(`API key deleted for user ${userId}`); 185 + res.json({ message: 'API key deleted successfully' }); 186 + } catch (error) { 187 + logger.error('Error deleting API key:', error); 188 + res.status(500).json({ error: 'Internal server error' }); 189 + } 190 + }); 191 + 192 + router.post( 193 + '/test', 194 + body('apiKey').trim().isLength({ min: 1 }).withMessage('API key is required'), 195 + body('model').optional().trim(), 196 + body('apiUrl') 197 + .optional() 198 + .trim() 199 + .isURL({ require_protocol: true }) 200 + .withMessage('API URL must be a valid URL with protocol'), 201 + async (req, res) => { 202 + try { 203 + const errors = validationResult(req); 204 + if (!errors.isEmpty()) { 205 + return res.status(400).json({ error: errors.array()[0].msg }); 206 + } 207 + 208 + const { apiKey, model, apiUrl } = req.body; 209 + const userId = req.user?.userId; 210 + 211 + const fullApiUrl = apiUrl || 'https://api.openai.com/v1/chat/completions'; 212 + const testModel = model || 'gpt-3.5-turbo'; 213 + 214 + const testResponse = await fetch(fullApiUrl, { 215 + method: 'POST', 216 + headers: { 217 + Authorization: `Bearer ${apiKey}`, 218 + 'Content-Type': 'application/json', 219 + }, 220 + body: JSON.stringify({ 221 + model: testModel, 222 + messages: [ 223 + { 224 + role: 'user', 225 + content: 226 + 'Hello! This is a test message. Please respond with "API key test successful!"', 227 + }, 228 + ], 229 + max_tokens: 50, 230 + temperature: 0.1, 231 + }), 232 + }); 233 + 234 + if (!testResponse.ok) { 235 + const errorData = await testResponse.json().catch(() => ({})); 236 + const errorMessage = 237 + errorData.error?.message || `HTTP ${testResponse.status}: ${testResponse.statusText}`; 238 + 239 + logger.warn(`API key test failed for user ${userId}: ${errorMessage}`); 240 + return res.status(400).json({ 241 + error: `API key test failed: ${errorMessage}`, 242 + }); 243 + } 244 + 245 + const responseData = await testResponse.json(); 246 + const testMessage = responseData.choices?.[0]?.message?.content || 'Test completed'; 247 + 248 + logger.info(`API key test successful for user ${userId}`); 249 + res.json({ 250 + success: true, 251 + message: 'API key test successful!', 252 + testResponse: testMessage.substring(0, 100), 253 + }); 254 + } catch (error) { 255 + logger.error('Error testing API key:', error); 256 + 257 + if (error instanceof TypeError && error.message.includes('fetch')) { 258 + return res 259 + .status(400) 260 + .json({ error: 'Failed to connect to API endpoint. Please check the URL.' }); 261 + } 262 + 263 + res.status(500).json({ error: 'API key test failed due to server error' }); 264 + } 265 + } 266 + ); 267 + 268 + export default router;
+151
src/routes/auth.ts
··· 1 + import { Router } from 'express'; 2 + import jwt from 'jsonwebtoken'; 3 + import pool from '../utils/pgClient'; 4 + import logger from '../utils/logger'; 5 + import { authenticateToken } from '../middlewares/auth'; 6 + 7 + const router = Router(); 8 + 9 + const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; 10 + const DISCORD_CLIENT_SECRET = process.env.DISCORD_CLIENT_SECRET; 11 + const DISCORD_REDIRECT_URI = 12 + process.env.DISCORD_REDIRECT_URI || 'http://localhost:8080/api/auth/discord/callback'; 13 + const JWT_SECRET = process.env.JWT_SECRET || 'your-jwt-secret'; 14 + const FRONTEND_URL = process.env.FRONTEND_URL || 'http://localhost:3000'; 15 + 16 + interface DiscordUser { 17 + id: string; 18 + username: string; 19 + discriminator: string; 20 + avatar?: string; 21 + email?: string; 22 + } 23 + 24 + router.get('/discord', (req, res) => { 25 + const discordAuthUrl = `https://discord.com/api/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(DISCORD_REDIRECT_URI)}&response_type=code&scope=identify`; 26 + res.redirect(discordAuthUrl); 27 + }); 28 + 29 + router.get('/discord/callback', async (req, res) => { 30 + const { code, error } = req.query; 31 + 32 + if (error) { 33 + logger.error('Discord OAuth error:', error); 34 + return res.redirect(`${FRONTEND_URL}/login?error=oauth_error`); 35 + } 36 + 37 + if (!code) { 38 + logger.error('No authorization code received from Discord'); 39 + return res.redirect(`${FRONTEND_URL}/login?error=no_code`); 40 + } 41 + 42 + try { 43 + const tokenResponse = await fetch('https://discord.com/api/oauth2/token', { 44 + method: 'POST', 45 + headers: { 46 + 'Content-Type': 'application/x-www-form-urlencoded', 47 + }, 48 + body: new URLSearchParams({ 49 + client_id: DISCORD_CLIENT_ID!, 50 + client_secret: DISCORD_CLIENT_SECRET!, 51 + grant_type: 'authorization_code', 52 + code: code as string, 53 + redirect_uri: DISCORD_REDIRECT_URI, 54 + }), 55 + }); 56 + 57 + if (!tokenResponse.ok) { 58 + throw new Error('Failed to exchange code for token'); 59 + } 60 + 61 + const tokenData = await tokenResponse.json(); 62 + 63 + const userResponse = await fetch('https://discord.com/api/users/@me', { 64 + headers: { 65 + Authorization: `Bearer ${tokenData.access_token}`, 66 + }, 67 + }); 68 + 69 + if (!userResponse.ok) { 70 + throw new Error('Failed to fetch user information'); 71 + } 72 + 73 + const discordUser: DiscordUser = await userResponse.json(); 74 + 75 + const userQuery = 'SELECT user_id, created_at FROM users WHERE user_id = $1'; 76 + const userResult = await pool.query(userQuery, [discordUser.id]); 77 + 78 + if (userResult.rows.length === 0) { 79 + const insertQuery = 80 + 'INSERT INTO users (user_id, language, created_at) VALUES ($1, $2, NOW())'; 81 + await pool.query(insertQuery, [discordUser.id, 'en']); 82 + logger.info( 83 + `New user created: ${discordUser.username}#${discordUser.discriminator} (${discordUser.id})` 84 + ); 85 + } 86 + 87 + const jwtToken = jwt.sign( 88 + { 89 + userId: discordUser.id, 90 + username: discordUser.username, 91 + discriminator: discordUser.discriminator === '0' ? null : discordUser.discriminator, 92 + avatar: discordUser.avatar, 93 + }, 94 + JWT_SECRET, 95 + { expiresIn: '7d' } 96 + ); 97 + 98 + const redirectUrl = new URL(`${FRONTEND_URL}/login`); 99 + redirectUrl.searchParams.set('token', jwtToken); 100 + redirectUrl.searchParams.set('user_id', discordUser.id); 101 + redirectUrl.searchParams.set('username', discordUser.username); 102 + if (discordUser.discriminator && discordUser.discriminator !== '0') { 103 + redirectUrl.searchParams.set('discriminator', discordUser.discriminator); 104 + } 105 + if (discordUser.avatar) { 106 + redirectUrl.searchParams.set('avatar', discordUser.avatar); 107 + } 108 + 109 + res.redirect(redirectUrl.toString()); 110 + } catch (error) { 111 + logger.error('Discord OAuth callback error:', error); 112 + res.redirect(`${FRONTEND_URL}/login?error=auth_failed`); 113 + } 114 + }); 115 + 116 + router.get('/me', authenticateToken, async (req, res) => { 117 + try { 118 + const userId = req.user?.userId; 119 + if (!userId) { 120 + return res.status(401).json({ error: 'User not authenticated' }); 121 + } 122 + 123 + const userQuery = 'SELECT user_id, language, created_at FROM users WHERE user_id = $1'; 124 + const userResult = await pool.query(userQuery, [userId]); 125 + 126 + if (userResult.rows.length === 0) { 127 + return res.status(404).json({ error: 'User not found' }); 128 + } 129 + 130 + const user = userResult.rows[0]; 131 + res.json({ 132 + user: { 133 + id: user.user_id, 134 + username: req.user?.username, 135 + discriminator: req.user?.discriminator, 136 + avatar: req.user?.avatar, 137 + language: user.language, 138 + createdAt: user.created_at, 139 + }, 140 + }); 141 + } catch (error) { 142 + logger.error('Error fetching user info:', error); 143 + res.status(500).json({ error: 'Internal server error' }); 144 + } 145 + }); 146 + 147 + router.post('/logout', authenticateToken, (req, res) => { 148 + res.json({ message: 'Logged out successfully' }); 149 + }); 150 + 151 + export default router;
+273
src/routes/reminders.ts
··· 1 + import { Router, Request, Response } from 'express'; 2 + import { v4 as uuidv4 } from 'uuid'; 3 + import { 4 + saveReminder, 5 + getUserReminders, 6 + getReminder, 7 + completeReminder, 8 + getActiveReminders, 9 + clearCompletedReminders, 10 + } from '../utils/reminderDb'; 11 + import { authenticateToken } from '../middlewares/auth'; 12 + import logger from '../utils/logger'; 13 + import BotClient from '../services/Client'; 14 + import { EmbedBuilder } from 'discord.js'; 15 + import { formatTimeString } from '../utils/validation'; 16 + import { scheduleReminder } from '../commands/utilities/remind'; 17 + 18 + const router = Router(); 19 + 20 + router.get('/', authenticateToken, async (req: Request, res: Response) => { 21 + try { 22 + const userId = req.user?.userId; 23 + if (!userId) { 24 + return res.status(401).json({ error: 'User not authenticated' }); 25 + } 26 + 27 + const reminders = await getUserReminders(userId); 28 + res.json({ reminders }); 29 + } catch (error) { 30 + logger.error('Error fetching user reminders:', error); 31 + res.status(500).json({ error: 'Failed to fetch reminders' }); 32 + } 33 + }); 34 + 35 + router.post('/', authenticateToken, async (req: Request, res: Response) => { 36 + try { 37 + const userId = req.user?.userId; 38 + const userTag = req.user?.username || 'Unknown'; 39 + 40 + if (!userId) { 41 + return res.status(401).json({ error: 'User not authenticated' }); 42 + } 43 + 44 + const { message, expires_at } = req.body; 45 + 46 + if (!message || !expires_at) { 47 + return res.status(400).json({ error: 'Message and expiration date are required' }); 48 + } 49 + 50 + const reminderData = { 51 + reminder_id: uuidv4(), 52 + user_id: userId, 53 + user_tag: userTag, 54 + channel_id: 'web', 55 + guild_id: null, 56 + message, 57 + expires_at: new Date(expires_at), 58 + locale: 'en', 59 + metadata: { 60 + source: 'web', 61 + created_via: 'dashboard', 62 + }, 63 + }; 64 + 65 + const savedReminder = await saveReminder(reminderData); 66 + 67 + try { 68 + const client = BotClient.getInstance(); 69 + if (client) { 70 + const scheduled = scheduleReminder(client, { 71 + ...savedReminder, 72 + created_at: savedReminder.created_at || new Date(), 73 + }); 74 + 75 + if (scheduled) { 76 + logger.info( 77 + `Successfully scheduled reminder ${savedReminder.reminder_id} from dashboard` 78 + ); 79 + } else { 80 + logger.warn(`Failed to schedule reminder ${savedReminder.reminder_id} from dashboard`); 81 + } 82 + } else { 83 + logger.warn('Bot client not available, reminder saved but not scheduled'); 84 + } 85 + } catch (schedulingError) { 86 + logger.error(`Error scheduling reminder ${savedReminder.reminder_id}:`, schedulingError); 87 + } 88 + 89 + res.status(201).json({ reminder: savedReminder }); 90 + } catch (error) { 91 + logger.error('Error creating reminder:', error); 92 + res.status(500).json({ error: 'Failed to create reminder' }); 93 + } 94 + }); 95 + 96 + router.get('/:id', authenticateToken, async (req: Request, res: Response) => { 97 + try { 98 + const userId = req.user?.userId; 99 + const reminderId = req.params.id; 100 + 101 + if (!userId) { 102 + return res.status(401).json({ error: 'User not authenticated' }); 103 + } 104 + 105 + const reminder = await getReminder(reminderId); 106 + 107 + if (!reminder) { 108 + return res.status(404).json({ error: 'Reminder not found' }); 109 + } 110 + 111 + if (reminder.user_id !== userId) { 112 + return res.status(403).json({ error: 'Access denied' }); 113 + } 114 + 115 + res.json({ reminder }); 116 + } catch (error) { 117 + logger.error('Error fetching reminder:', error); 118 + res.status(500).json({ error: 'Failed to fetch reminder' }); 119 + } 120 + }); 121 + 122 + router.patch('/:id/complete', authenticateToken, async (req: Request, res: Response) => { 123 + try { 124 + const userId = req.user?.userId; 125 + const reminderId = req.params.id; 126 + 127 + if (!userId) { 128 + return res.status(401).json({ error: 'User not authenticated' }); 129 + } 130 + 131 + const reminder = await getReminder(reminderId); 132 + 133 + if (!reminder) { 134 + return res.status(404).json({ error: 'Reminder not found' }); 135 + } 136 + 137 + if (reminder.user_id !== userId) { 138 + return res.status(403).json({ error: 'Access denied' }); 139 + } 140 + 141 + try { 142 + const client = BotClient.getInstance(); 143 + if (client) { 144 + const user = await client.users.fetch(reminder.user_id); 145 + if (user) { 146 + const minutes = Math.floor( 147 + (new Date(reminder.expires_at).getTime() - new Date(reminder.created_at!).getTime()) / 148 + (60 * 1000) 149 + ); 150 + 151 + const reminderTitle = 152 + '⏰ ' + (await client.getLocaleText('commands.remind.reminder', reminder.locale)); 153 + const reminderDesc = await client.getLocaleText( 154 + 'commands.remind.remindyou', 155 + reminder.locale, 156 + { message: reminder.message } 157 + ); 158 + 159 + const timeElapsedText = 160 + '⏱️ ' + (await client.getLocaleText('commands.remind.timeelapsed', reminder.locale)); 161 + const originalTimeText = 162 + '📅 ' + (await client.getLocaleText('commands.remind.originaltime', reminder.locale)); 163 + 164 + const reminderEmbed = new EmbedBuilder() 165 + .setColor(0xfaa0a0) 166 + .setTitle(reminderTitle) 167 + .setDescription(reminderDesc) 168 + .addFields( 169 + { name: timeElapsedText, value: formatTimeString(minutes), inline: true }, 170 + { 171 + name: originalTimeText, 172 + value: `<t:${Math.floor(new Date(reminder.created_at!).getTime() / 1000)}:f>`, 173 + inline: true, 174 + } 175 + ) 176 + .setFooter({ text: `ID: ${reminder.reminder_id.slice(-6)}` }) 177 + .setTimestamp(); 178 + 179 + if (reminder.metadata?.message_url) { 180 + const originalMessageText = await client.getLocaleText( 181 + 'common.ogmessage', 182 + reminder.locale 183 + ); 184 + const jumpToMessageText = await client.getLocaleText( 185 + 'common.jumptomessage', 186 + reminder.locale 187 + ); 188 + 189 + reminderEmbed.addFields({ 190 + name: originalMessageText, 191 + value: `[${jumpToMessageText}](${reminder.metadata.message_url})`, 192 + inline: false, 193 + }); 194 + } 195 + 196 + if (reminder.message.includes('http') && !reminder.metadata?.message_url) { 197 + const messageLinkText = await client.getLocaleText( 198 + 'common.messagelink', 199 + reminder.locale 200 + ); 201 + reminderEmbed.addFields({ 202 + name: messageLinkText, 203 + value: reminder.message, 204 + inline: false, 205 + }); 206 + } 207 + 208 + await user.send({ 209 + embeds: [reminderEmbed], 210 + }); 211 + 212 + logger.info(`Successfully sent reminder to ${reminder.user_tag} (${reminder.user_id})`, { 213 + reminderId: reminder.reminder_id, 214 + }); 215 + } 216 + } 217 + } catch (notificationError) { 218 + logger.error( 219 + `Failed to send reminder notification to ${reminder.user_tag} (${reminder.user_id}): ${(notificationError as Error).message}`, 220 + { 221 + error: notificationError, 222 + reminderId: reminder.reminder_id, 223 + } 224 + ); 225 + } 226 + 227 + const completedReminder = await completeReminder(reminderId); 228 + res.json({ reminder: completedReminder }); 229 + } catch (error) { 230 + logger.error('Error completing reminder:', error); 231 + res.status(500).json({ error: 'Failed to complete reminder' }); 232 + } 233 + }); 234 + 235 + router.get('/active/all', authenticateToken, async (req: Request, res: Response) => { 236 + try { 237 + const userId = req.user?.userId; 238 + 239 + if (!userId) { 240 + return res.status(401).json({ error: 'User not authenticated' }); 241 + } 242 + 243 + const activeReminders = await getActiveReminders(); 244 + const userActiveReminders = activeReminders.filter((reminder) => reminder.user_id === userId); 245 + 246 + res.json({ reminders: userActiveReminders }); 247 + } catch (error) { 248 + logger.error('Error fetching active reminders:', error); 249 + res.status(500).json({ error: 'Failed to fetch active reminders' }); 250 + } 251 + }); 252 + 253 + router.delete('/completed', authenticateToken, async (req: Request, res: Response) => { 254 + try { 255 + const userId = req.user?.userId; 256 + 257 + if (!userId) { 258 + return res.status(401).json({ error: 'User not authenticated' }); 259 + } 260 + 261 + const deletedCount = await clearCompletedReminders(userId); 262 + 263 + res.json({ 264 + message: `Successfully cleared ${deletedCount} completed reminders`, 265 + deletedCount, 266 + }); 267 + } catch (error) { 268 + logger.error('Error clearing completed reminders:', error); 269 + res.status(500).json({ error: 'Failed to clear completed reminders' }); 270 + } 271 + }); 272 + 273 + export default router;
+190
src/routes/todos.ts
··· 1 + import { Router } from 'express'; 2 + import pool from '../utils/pgClient'; 3 + import logger from '../utils/logger'; 4 + import { authenticateToken } from '../middlewares/auth'; 5 + import { body, validationResult } from 'express-validator'; 6 + 7 + const router = Router(); 8 + 9 + router.use(authenticateToken); 10 + 11 + router.get('/', async (req, res) => { 12 + try { 13 + const userId = req.user?.userId; 14 + if (!userId) { 15 + return res.status(401).json({ error: 'User not authenticated' }); 16 + } 17 + 18 + const query = ` 19 + SELECT id, item, done, created_at, completed_at 20 + FROM todos 21 + WHERE user_id = $1 22 + ORDER BY created_at DESC 23 + `; 24 + const result = await pool.query(query, [userId]); 25 + 26 + res.json(result.rows); 27 + } catch (error) { 28 + logger.error('Error fetching todos:', error); 29 + res.status(500).json({ error: 'Internal server error' }); 30 + } 31 + }); 32 + 33 + router.post( 34 + '/', 35 + body('item') 36 + .trim() 37 + .isLength({ min: 1, max: 500 }) 38 + .withMessage('Todo item must be between 1 and 500 characters'), 39 + async (req, res) => { 40 + try { 41 + const errors = validationResult(req); 42 + if (!errors.isEmpty()) { 43 + return res.status(400).json({ error: errors.array()[0].msg }); 44 + } 45 + 46 + const userId = req.user?.userId; 47 + if (!userId) { 48 + return res.status(401).json({ error: 'User not authenticated' }); 49 + } 50 + 51 + const { item } = req.body; 52 + 53 + const query = ` 54 + INSERT INTO todos (user_id, item, done, created_at) 55 + VALUES ($1, $2, FALSE, NOW()) 56 + RETURNING id, item, done, created_at, completed_at 57 + `; 58 + const result = await pool.query(query, [userId, item]); 59 + 60 + logger.info(`Todo created for user ${userId}: ${item}`); 61 + res.status(201).json(result.rows[0]); 62 + } catch (error) { 63 + logger.error('Error creating todo:', error); 64 + res.status(500).json({ error: 'Internal server error' }); 65 + } 66 + } 67 + ); 68 + 69 + router.put( 70 + '/:id', 71 + body('done').isBoolean().withMessage('Done must be a boolean value'), 72 + async (req, res) => { 73 + try { 74 + const errors = validationResult(req); 75 + if (!errors.isEmpty()) { 76 + return res.status(400).json({ error: errors.array()[0].msg }); 77 + } 78 + 79 + const userId = req.user?.userId; 80 + if (!userId) { 81 + return res.status(401).json({ error: 'User not authenticated' }); 82 + } 83 + 84 + const { id } = req.params as { id: string }; 85 + const { done } = req.body; 86 + 87 + const checkQuery = 'SELECT id FROM todos WHERE id = $1 AND user_id = $2'; 88 + const checkResult = await pool.query(checkQuery, [id, userId]); 89 + 90 + if (checkResult.rows.length === 0) { 91 + return res.status(404).json({ error: 'Todo not found' }); 92 + } 93 + 94 + const query = ` 95 + UPDATE todos 96 + SET done = $1, completed_at = CASE WHEN $1 = TRUE THEN NOW() ELSE NULL END 97 + WHERE id = $2 AND user_id = $3 98 + RETURNING id, item, done, created_at, completed_at 99 + `; 100 + const result = await pool.query(query, [done, id, userId]); 101 + 102 + logger.info(`Todo ${done ? 'completed' : 'uncompleted'} for user ${userId}: ${id}`); 103 + res.json(result.rows[0]); 104 + } catch (error) { 105 + logger.error('Error updating todo:', error); 106 + res.status(500).json({ error: 'Internal server error' }); 107 + } 108 + } 109 + ); 110 + 111 + router.delete('/:id', async (req, res) => { 112 + try { 113 + const userId = req.user?.userId; 114 + if (!userId) { 115 + return res.status(401).json({ error: 'User not authenticated' }); 116 + } 117 + 118 + const { id } = req.params as { id: string }; 119 + 120 + const checkQuery = 'SELECT id, item FROM todos WHERE id = $1 AND user_id = $2'; 121 + const checkResult = await pool.query(checkQuery, [id, userId]); 122 + 123 + if (checkResult.rows.length === 0) { 124 + return res.status(404).json({ error: 'Todo not found' }); 125 + } 126 + 127 + const deleteQuery = 'DELETE FROM todos WHERE id = $1 AND user_id = $2'; 128 + await pool.query(deleteQuery, [id, userId]); 129 + 130 + logger.info(`Todo deleted for user ${userId}: ${checkResult.rows[0].item}`); 131 + res.json({ message: 'Todo deleted successfully' }); 132 + } catch (error) { 133 + logger.error('Error deleting todo:', error); 134 + res.status(500).json({ error: 'Internal server error' }); 135 + } 136 + }); 137 + 138 + router.delete('/', async (req, res) => { 139 + try { 140 + const userId = req.user?.userId; 141 + if (!userId) { 142 + return res.status(401).json({ error: 'User not authenticated' }); 143 + } 144 + 145 + const countQuery = 'SELECT COUNT(*) as count FROM todos WHERE user_id = $1'; 146 + const countResult = await pool.query(countQuery, [userId]); 147 + const todoCount = parseInt(countResult.rows[0].count); 148 + 149 + const deleteQuery = 'DELETE FROM todos WHERE user_id = $1'; 150 + await pool.query(deleteQuery, [userId]); 151 + 152 + logger.info(`All todos cleared for user ${userId}: ${todoCount} todos deleted`); 153 + res.json({ message: `${todoCount} todos cleared successfully` }); 154 + } catch (error) { 155 + logger.error('Error clearing todos:', error); 156 + res.status(500).json({ error: 'Internal server error' }); 157 + } 158 + }); 159 + 160 + router.get('/stats', async (req, res) => { 161 + try { 162 + const userId = req.user?.userId; 163 + if (!userId) { 164 + return res.status(401).json({ error: 'User not authenticated' }); 165 + } 166 + 167 + const query = ` 168 + SELECT 169 + COUNT(*) as total, 170 + COUNT(CASE WHEN done = TRUE THEN 1 END) as completed, 171 + COUNT(CASE WHEN done = FALSE THEN 1 END) as pending 172 + FROM todos 173 + WHERE user_id = $1 174 + `; 175 + const result = await pool.query(query, [userId]); 176 + 177 + const stats = { 178 + total: parseInt(result.rows[0].total), 179 + completed: parseInt(result.rows[0].completed), 180 + pending: parseInt(result.rows[0].pending), 181 + }; 182 + 183 + res.json(stats); 184 + } catch (error) { 185 + logger.error('Error fetching todo stats:', error); 186 + res.status(500).json({ error: 'Internal server error' }); 187 + } 188 + }); 189 + 190 + export default router;
+12
src/services/Client.ts
··· 13 13 export const srcDir = path.join(__dirname, '..'); 14 14 15 15 export default class BotClient extends Client { 16 + private static instance: BotClient | null = null; 16 17 public commands = new Collection<string, SlashCommandProps>(); 17 18 // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 19 public t = new Collection<string, any>(); 20 + 19 21 constructor() { 20 22 super({ 21 23 intents: [GatewayIntentBits.MessageContent], ··· 28 30 ], 29 31 }, 30 32 }); 33 + BotClient.instance = this; 34 + } 35 + 36 + public static getInstance(): BotClient | null { 37 + return BotClient.instance; 31 38 } 32 39 33 40 public async init() { ··· 76 83 } 77 84 public async getLocaleText(key: string, locale: string, replaces = {}): Promise<string> { 78 85 const fallbackLocale = 'en-US'; 86 + 87 + if (!locale) { 88 + locale = fallbackLocale; 89 + } 90 + 79 91 let langMap = this.t.get(locale); 80 92 if (!langMap) { 81 93 const langOnly = locale.split('-')[0];
+14
src/types/express.d.ts
··· 1 + /// <reference types="express" /> 2 + 3 + declare namespace Express { 4 + interface Request { 5 + user?: { 6 + userId: string; 7 + username: string; 8 + discriminator: string; 9 + avatar?: string; 10 + iat?: number; 11 + exp?: number; 12 + }; 13 + } 14 + }
+7 -2
src/utils/commandLogger.ts
··· 11 11 } 12 12 13 13 export function logUserAction(options: CommandLogOptions): void { 14 - const { commandName, userId, username, guildId, channelId } = options; 14 + const { commandName, userId, username, guildId, channelId, additionalInfo } = options; 15 15 16 16 let logMessage = `User ${username} (${userId}) used ${commandName} command`; 17 17 ··· 23 23 logMessage += ` in channel ${channelId}`; 24 24 } 25 25 26 + if (additionalInfo) { 27 + logMessage += ` with ${additionalInfo}`; 28 + } 29 + 26 30 logger.info(logMessage); 27 31 } 28 32 ··· 37 41 username: interaction.user.tag, 38 42 guildId: interaction.guildId || undefined, 39 43 channelId: interaction.channelId, 44 + additionalInfo, 40 45 }); 41 46 } 42 47 ··· 46 51 logUserAction({ ...options, commandName }); 47 52 }, 48 53 logFromInteraction: (interaction: CommandInteraction, additionalInfo?: string) => { 49 - logUserActionFromInteraction(interaction, commandName); 54 + logUserActionFromInteraction(interaction, commandName, additionalInfo); 50 55 }, 51 56 }; 52 57 }
+82
src/utils/encryption.ts
··· 1 + import crypto from 'crypto'; 2 + import { API_KEY_ENCRYPTION_SECRET } from '../config'; 3 + 4 + const ENCRYPTION_KEY = API_KEY_ENCRYPTION_SECRET; 5 + const ALGORITHM = 'aes-256-gcm'; 6 + 7 + const getEncryptionKey = (): Buffer => { 8 + if (ENCRYPTION_KEY.length !== 32) { 9 + throw new Error('ENCRYPTION_KEY must be exactly 32 characters long'); 10 + } 11 + return Buffer.from(ENCRYPTION_KEY, 'utf8'); 12 + }; 13 + 14 + /** 15 + * Encrypts a string using AES-256-GCM 16 + * @param text The text to encrypt 17 + * @returns Base64 encoded encrypted data with IV and auth tag 18 + */ 19 + export const encryptApiKey = (text: string): string => { 20 + try { 21 + const key = getEncryptionKey(); 22 + const iv = crypto.randomBytes(16); 23 + 24 + const cipher = crypto.createCipheriv(ALGORITHM, key, iv); 25 + cipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); 26 + 27 + let encrypted = cipher.update(text, 'utf8', 'hex'); 28 + encrypted += cipher.final('hex'); 29 + 30 + const authTag = cipher.getAuthTag(); 31 + 32 + const combined = Buffer.concat([iv, authTag, Buffer.from(encrypted, 'hex')]); 33 + 34 + return combined.toString('base64'); 35 + } catch { 36 + throw new Error('Failed to encrypt API key'); 37 + } 38 + }; 39 + 40 + /** 41 + * Decrypts a string that was encrypted with encryptApiKey 42 + * @param encryptedData Base64 encoded encrypted data 43 + * @returns The decrypted text 44 + */ 45 + export const decryptApiKey = (encryptedData: string): string => { 46 + try { 47 + const key = getEncryptionKey(); 48 + const combined = Buffer.from(encryptedData, 'base64'); 49 + 50 + const extractedIv = combined.subarray(0, 16); 51 + const authTag = combined.subarray(16, 32); 52 + const encrypted = combined.subarray(32); 53 + 54 + const decipher = crypto.createDecipheriv(ALGORITHM, key, extractedIv); 55 + decipher.setAAD(Buffer.from('aethel-api-key', 'utf8')); 56 + decipher.setAuthTag(authTag); 57 + 58 + let decrypted = decipher.update(encrypted, undefined, 'utf8'); 59 + decrypted += decipher.final('utf8'); 60 + 61 + return decrypted; 62 + } catch { 63 + throw new Error('Failed to decrypt API key'); 64 + } 65 + }; 66 + 67 + /** 68 + * Generates a secure random encryption key 69 + * @returns A 32-character random string suitable for use as ENCRYPTION_KEY 70 + */ 71 + export const generateEncryptionKey = (): string => { 72 + return crypto.randomBytes(32).toString('base64').substring(0, 32); 73 + }; 74 + 75 + /** 76 + * Validates that an encryption key is properly formatted 77 + * @param key The key to validate 78 + * @returns True if the key is valid 79 + */ 80 + export const validateEncryptionKey = (key: string): boolean => { 81 + return typeof key === 'string' && key.length === 32; 82 + };
+17
src/utils/reminderDb.ts
··· 197 197 } 198 198 } 199 199 200 + async function clearCompletedReminders(userId: string) { 201 + const query = ` 202 + DELETE FROM reminders 203 + WHERE user_id = $1 204 + AND is_completed = TRUE 205 + RETURNING * 206 + `; 207 + 208 + try { 209 + const result = await pool.query(query, [userId]); 210 + return result.rowCount; 211 + } catch (error) { 212 + throw createDatabaseError(error, 'clearing completed reminders'); 213 + } 214 + } 215 + 200 216 export { 201 217 saveReminder, 202 218 completeReminder, ··· 204 220 getReminder, 205 221 getUserReminders, 206 222 cleanupReminders, 223 + clearCompletedReminders, 207 224 ensureUserRegistered, 208 225 DatabaseError, 209 226 };
+1 -1
tsconfig.json
··· 105 105 // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 106 106 "skipLibCheck": true /* Skip type checking all .d.ts files. */ 107 107 }, 108 - "include": ["src/**/*"], 108 + "include": ["src/**/*", "environment.d.ts"], 109 109 "exclude": ["express-server/*", "node_modules", "dist"], 110 110 "ts-node": { 111 111 "pretty": true,
+16
web/index.html
··· 1 + <!doctype html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="icon" type="image/svg+xml" href="/vite.svg" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>Aethel Dashboard</title> 8 + <link rel="preconnect" href="https://fonts.googleapis.com"> 9 + <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 10 + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet"> 11 + </head> 12 + <body> 13 + <div id="root"></div> 14 + <script type="module" src="/src/main.tsx"></script> 15 + </body> 16 + </html>
+36
web/package.json
··· 1 + { 2 + "name": "aethel-dashboard", 3 + "version": "1.0.0", 4 + "type": "module", 5 + "scripts": { 6 + "dev": "vite", 7 + "build": "tsc && vite build", 8 + "preview": "vite preview", 9 + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" 10 + }, 11 + "dependencies": { 12 + "@tanstack/react-query": "^5.83.0", 13 + "axios": "^1.10.0", 14 + "lucide-react": "^0.294.0", 15 + "react": "^18.3.1", 16 + "react-dom": "^18.3.1", 17 + "react-router-dom": "^6.30.1", 18 + "sonner": "^1.7.4", 19 + "zustand": "^4.5.7" 20 + }, 21 + "devDependencies": { 22 + "@types/react": "^18.3.23", 23 + "@types/react-dom": "^18.3.7", 24 + "@typescript-eslint/eslint-plugin": "^6.21.0", 25 + "@typescript-eslint/parser": "^6.21.0", 26 + "@vitejs/plugin-react": "^4.7.0", 27 + "autoprefixer": "^10.4.21", 28 + "eslint": "^8.57.1", 29 + "eslint-plugin-react-hooks": "^4.6.2", 30 + "eslint-plugin-react-refresh": "^0.4.20", 31 + "postcss": "^8.5.6", 32 + "tailwindcss": "^3.4.17", 33 + "typescript": "^5.8.3", 34 + "vite": "^4.5.14" 35 + } 36 + }
+2724
web/pnpm-lock.yaml
··· 1 + lockfileVersion: '9.0' 2 + 3 + settings: 4 + autoInstallPeers: true 5 + excludeLinksFromLockfile: false 6 + 7 + importers: 8 + 9 + .: 10 + dependencies: 11 + '@tanstack/react-query': 12 + specifier: ^5.83.0 13 + version: 5.83.0(react@18.3.1) 14 + axios: 15 + specifier: ^1.10.0 16 + version: 1.10.0 17 + lucide-react: 18 + specifier: ^0.294.0 19 + version: 0.294.0(react@18.3.1) 20 + react: 21 + specifier: ^18.3.1 22 + version: 18.3.1 23 + react-dom: 24 + specifier: ^18.3.1 25 + version: 18.3.1(react@18.3.1) 26 + react-router-dom: 27 + specifier: ^6.30.1 28 + version: 6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 29 + sonner: 30 + specifier: ^1.7.4 31 + version: 1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 32 + zustand: 33 + specifier: ^4.5.7 34 + version: 4.5.7(@types/react@18.3.23)(react@18.3.1) 35 + devDependencies: 36 + '@types/react': 37 + specifier: ^18.3.23 38 + version: 18.3.23 39 + '@types/react-dom': 40 + specifier: ^18.3.7 41 + version: 18.3.7(@types/react@18.3.23) 42 + '@typescript-eslint/eslint-plugin': 43 + specifier: ^6.21.0 44 + version: 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3) 45 + '@typescript-eslint/parser': 46 + specifier: ^6.21.0 47 + version: 6.21.0(eslint@8.57.1)(typescript@5.8.3) 48 + '@vitejs/plugin-react': 49 + specifier: ^4.7.0 50 + version: 4.7.0(vite@4.5.14) 51 + autoprefixer: 52 + specifier: ^10.4.21 53 + version: 10.4.21(postcss@8.5.6) 54 + eslint: 55 + specifier: ^8.57.1 56 + version: 8.57.1 57 + eslint-plugin-react-hooks: 58 + specifier: ^4.6.2 59 + version: 4.6.2(eslint@8.57.1) 60 + eslint-plugin-react-refresh: 61 + specifier: ^0.4.20 62 + version: 0.4.20(eslint@8.57.1) 63 + postcss: 64 + specifier: ^8.5.6 65 + version: 8.5.6 66 + tailwindcss: 67 + specifier: ^3.4.17 68 + version: 3.4.17 69 + typescript: 70 + specifier: ^5.8.3 71 + version: 5.8.3 72 + vite: 73 + specifier: ^4.5.14 74 + version: 4.5.14 75 + 76 + packages: 77 + 78 + '@alloc/quick-lru@5.2.0': 79 + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 80 + engines: {node: '>=10'} 81 + 82 + '@ampproject/remapping@2.3.0': 83 + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} 84 + engines: {node: '>=6.0.0'} 85 + 86 + '@babel/code-frame@7.27.1': 87 + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} 88 + engines: {node: '>=6.9.0'} 89 + 90 + '@babel/compat-data@7.28.0': 91 + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} 92 + engines: {node: '>=6.9.0'} 93 + 94 + '@babel/core@7.28.0': 95 + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} 96 + engines: {node: '>=6.9.0'} 97 + 98 + '@babel/generator@7.28.0': 99 + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} 100 + engines: {node: '>=6.9.0'} 101 + 102 + '@babel/helper-compilation-targets@7.27.2': 103 + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} 104 + engines: {node: '>=6.9.0'} 105 + 106 + '@babel/helper-globals@7.28.0': 107 + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} 108 + engines: {node: '>=6.9.0'} 109 + 110 + '@babel/helper-module-imports@7.27.1': 111 + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} 112 + engines: {node: '>=6.9.0'} 113 + 114 + '@babel/helper-module-transforms@7.27.3': 115 + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} 116 + engines: {node: '>=6.9.0'} 117 + peerDependencies: 118 + '@babel/core': ^7.0.0 119 + 120 + '@babel/helper-plugin-utils@7.27.1': 121 + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} 122 + engines: {node: '>=6.9.0'} 123 + 124 + '@babel/helper-string-parser@7.27.1': 125 + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} 126 + engines: {node: '>=6.9.0'} 127 + 128 + '@babel/helper-validator-identifier@7.27.1': 129 + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} 130 + engines: {node: '>=6.9.0'} 131 + 132 + '@babel/helper-validator-option@7.27.1': 133 + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} 134 + engines: {node: '>=6.9.0'} 135 + 136 + '@babel/helpers@7.27.6': 137 + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} 138 + engines: {node: '>=6.9.0'} 139 + 140 + '@babel/parser@7.28.0': 141 + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} 142 + engines: {node: '>=6.0.0'} 143 + hasBin: true 144 + 145 + '@babel/plugin-transform-react-jsx-self@7.27.1': 146 + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} 147 + engines: {node: '>=6.9.0'} 148 + peerDependencies: 149 + '@babel/core': ^7.0.0-0 150 + 151 + '@babel/plugin-transform-react-jsx-source@7.27.1': 152 + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} 153 + engines: {node: '>=6.9.0'} 154 + peerDependencies: 155 + '@babel/core': ^7.0.0-0 156 + 157 + '@babel/template@7.27.2': 158 + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} 159 + engines: {node: '>=6.9.0'} 160 + 161 + '@babel/traverse@7.28.0': 162 + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} 163 + engines: {node: '>=6.9.0'} 164 + 165 + '@babel/types@7.28.1': 166 + resolution: {integrity: sha512-x0LvFTekgSX+83TI28Y9wYPUfzrnl2aT5+5QLnO6v7mSJYtEEevuDRN0F0uSHRk1G1IWZC43o00Y0xDDrpBGPQ==} 167 + engines: {node: '>=6.9.0'} 168 + 169 + '@esbuild/android-arm64@0.18.20': 170 + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} 171 + engines: {node: '>=12'} 172 + cpu: [arm64] 173 + os: [android] 174 + 175 + '@esbuild/android-arm@0.18.20': 176 + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} 177 + engines: {node: '>=12'} 178 + cpu: [arm] 179 + os: [android] 180 + 181 + '@esbuild/android-x64@0.18.20': 182 + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} 183 + engines: {node: '>=12'} 184 + cpu: [x64] 185 + os: [android] 186 + 187 + '@esbuild/darwin-arm64@0.18.20': 188 + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} 189 + engines: {node: '>=12'} 190 + cpu: [arm64] 191 + os: [darwin] 192 + 193 + '@esbuild/darwin-x64@0.18.20': 194 + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} 195 + engines: {node: '>=12'} 196 + cpu: [x64] 197 + os: [darwin] 198 + 199 + '@esbuild/freebsd-arm64@0.18.20': 200 + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} 201 + engines: {node: '>=12'} 202 + cpu: [arm64] 203 + os: [freebsd] 204 + 205 + '@esbuild/freebsd-x64@0.18.20': 206 + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} 207 + engines: {node: '>=12'} 208 + cpu: [x64] 209 + os: [freebsd] 210 + 211 + '@esbuild/linux-arm64@0.18.20': 212 + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} 213 + engines: {node: '>=12'} 214 + cpu: [arm64] 215 + os: [linux] 216 + 217 + '@esbuild/linux-arm@0.18.20': 218 + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} 219 + engines: {node: '>=12'} 220 + cpu: [arm] 221 + os: [linux] 222 + 223 + '@esbuild/linux-ia32@0.18.20': 224 + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} 225 + engines: {node: '>=12'} 226 + cpu: [ia32] 227 + os: [linux] 228 + 229 + '@esbuild/linux-loong64@0.18.20': 230 + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} 231 + engines: {node: '>=12'} 232 + cpu: [loong64] 233 + os: [linux] 234 + 235 + '@esbuild/linux-mips64el@0.18.20': 236 + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} 237 + engines: {node: '>=12'} 238 + cpu: [mips64el] 239 + os: [linux] 240 + 241 + '@esbuild/linux-ppc64@0.18.20': 242 + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} 243 + engines: {node: '>=12'} 244 + cpu: [ppc64] 245 + os: [linux] 246 + 247 + '@esbuild/linux-riscv64@0.18.20': 248 + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} 249 + engines: {node: '>=12'} 250 + cpu: [riscv64] 251 + os: [linux] 252 + 253 + '@esbuild/linux-s390x@0.18.20': 254 + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} 255 + engines: {node: '>=12'} 256 + cpu: [s390x] 257 + os: [linux] 258 + 259 + '@esbuild/linux-x64@0.18.20': 260 + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} 261 + engines: {node: '>=12'} 262 + cpu: [x64] 263 + os: [linux] 264 + 265 + '@esbuild/netbsd-x64@0.18.20': 266 + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} 267 + engines: {node: '>=12'} 268 + cpu: [x64] 269 + os: [netbsd] 270 + 271 + '@esbuild/openbsd-x64@0.18.20': 272 + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} 273 + engines: {node: '>=12'} 274 + cpu: [x64] 275 + os: [openbsd] 276 + 277 + '@esbuild/sunos-x64@0.18.20': 278 + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} 279 + engines: {node: '>=12'} 280 + cpu: [x64] 281 + os: [sunos] 282 + 283 + '@esbuild/win32-arm64@0.18.20': 284 + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} 285 + engines: {node: '>=12'} 286 + cpu: [arm64] 287 + os: [win32] 288 + 289 + '@esbuild/win32-ia32@0.18.20': 290 + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} 291 + engines: {node: '>=12'} 292 + cpu: [ia32] 293 + os: [win32] 294 + 295 + '@esbuild/win32-x64@0.18.20': 296 + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} 297 + engines: {node: '>=12'} 298 + cpu: [x64] 299 + os: [win32] 300 + 301 + '@eslint-community/eslint-utils@4.7.0': 302 + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} 303 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 304 + peerDependencies: 305 + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 306 + 307 + '@eslint-community/regexpp@4.12.1': 308 + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} 309 + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 310 + 311 + '@eslint/eslintrc@2.1.4': 312 + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} 313 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 314 + 315 + '@eslint/js@8.57.1': 316 + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} 317 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 318 + 319 + '@humanwhocodes/config-array@0.13.0': 320 + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} 321 + engines: {node: '>=10.10.0'} 322 + deprecated: Use @eslint/config-array instead 323 + 324 + '@humanwhocodes/module-importer@1.0.1': 325 + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} 326 + engines: {node: '>=12.22'} 327 + 328 + '@humanwhocodes/object-schema@2.0.3': 329 + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} 330 + deprecated: Use @eslint/object-schema instead 331 + 332 + '@isaacs/cliui@8.0.2': 333 + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 334 + engines: {node: '>=12'} 335 + 336 + '@jridgewell/gen-mapping@0.3.12': 337 + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} 338 + 339 + '@jridgewell/resolve-uri@3.1.2': 340 + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 341 + engines: {node: '>=6.0.0'} 342 + 343 + '@jridgewell/sourcemap-codec@1.5.4': 344 + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} 345 + 346 + '@jridgewell/trace-mapping@0.3.29': 347 + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} 348 + 349 + '@nodelib/fs.scandir@2.1.5': 350 + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 351 + engines: {node: '>= 8'} 352 + 353 + '@nodelib/fs.stat@2.0.5': 354 + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 355 + engines: {node: '>= 8'} 356 + 357 + '@nodelib/fs.walk@1.2.8': 358 + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 359 + engines: {node: '>= 8'} 360 + 361 + '@pkgjs/parseargs@0.11.0': 362 + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 363 + engines: {node: '>=14'} 364 + 365 + '@remix-run/router@1.23.0': 366 + resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} 367 + engines: {node: '>=14.0.0'} 368 + 369 + '@rolldown/pluginutils@1.0.0-beta.27': 370 + resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} 371 + 372 + '@tanstack/query-core@5.83.0': 373 + resolution: {integrity: sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==} 374 + 375 + '@tanstack/react-query@5.83.0': 376 + resolution: {integrity: sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==} 377 + peerDependencies: 378 + react: ^18 || ^19 379 + 380 + '@types/babel__core@7.20.5': 381 + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} 382 + 383 + '@types/babel__generator@7.27.0': 384 + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} 385 + 386 + '@types/babel__template@7.4.4': 387 + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} 388 + 389 + '@types/babel__traverse@7.20.7': 390 + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} 391 + 392 + '@types/json-schema@7.0.15': 393 + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} 394 + 395 + '@types/prop-types@15.7.15': 396 + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} 397 + 398 + '@types/react-dom@18.3.7': 399 + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} 400 + peerDependencies: 401 + '@types/react': ^18.0.0 402 + 403 + '@types/react@18.3.23': 404 + resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} 405 + 406 + '@types/semver@7.7.0': 407 + resolution: {integrity: sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==} 408 + 409 + '@typescript-eslint/eslint-plugin@6.21.0': 410 + resolution: {integrity: sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==} 411 + engines: {node: ^16.0.0 || >=18.0.0} 412 + peerDependencies: 413 + '@typescript-eslint/parser': ^6.0.0 || ^6.0.0-alpha 414 + eslint: ^7.0.0 || ^8.0.0 415 + typescript: '*' 416 + peerDependenciesMeta: 417 + typescript: 418 + optional: true 419 + 420 + '@typescript-eslint/parser@6.21.0': 421 + resolution: {integrity: sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==} 422 + engines: {node: ^16.0.0 || >=18.0.0} 423 + peerDependencies: 424 + eslint: ^7.0.0 || ^8.0.0 425 + typescript: '*' 426 + peerDependenciesMeta: 427 + typescript: 428 + optional: true 429 + 430 + '@typescript-eslint/scope-manager@6.21.0': 431 + resolution: {integrity: sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==} 432 + engines: {node: ^16.0.0 || >=18.0.0} 433 + 434 + '@typescript-eslint/type-utils@6.21.0': 435 + resolution: {integrity: sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==} 436 + engines: {node: ^16.0.0 || >=18.0.0} 437 + peerDependencies: 438 + eslint: ^7.0.0 || ^8.0.0 439 + typescript: '*' 440 + peerDependenciesMeta: 441 + typescript: 442 + optional: true 443 + 444 + '@typescript-eslint/types@6.21.0': 445 + resolution: {integrity: sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==} 446 + engines: {node: ^16.0.0 || >=18.0.0} 447 + 448 + '@typescript-eslint/typescript-estree@6.21.0': 449 + resolution: {integrity: sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==} 450 + engines: {node: ^16.0.0 || >=18.0.0} 451 + peerDependencies: 452 + typescript: '*' 453 + peerDependenciesMeta: 454 + typescript: 455 + optional: true 456 + 457 + '@typescript-eslint/utils@6.21.0': 458 + resolution: {integrity: sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==} 459 + engines: {node: ^16.0.0 || >=18.0.0} 460 + peerDependencies: 461 + eslint: ^7.0.0 || ^8.0.0 462 + 463 + '@typescript-eslint/visitor-keys@6.21.0': 464 + resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} 465 + engines: {node: ^16.0.0 || >=18.0.0} 466 + 467 + '@ungap/structured-clone@1.3.0': 468 + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} 469 + 470 + '@vitejs/plugin-react@4.7.0': 471 + resolution: {integrity: sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==} 472 + engines: {node: ^14.18.0 || >=16.0.0} 473 + peerDependencies: 474 + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 475 + 476 + acorn-jsx@5.3.2: 477 + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} 478 + peerDependencies: 479 + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 480 + 481 + acorn@8.15.0: 482 + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} 483 + engines: {node: '>=0.4.0'} 484 + hasBin: true 485 + 486 + ajv@6.12.6: 487 + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} 488 + 489 + ansi-regex@5.0.1: 490 + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 491 + engines: {node: '>=8'} 492 + 493 + ansi-regex@6.1.0: 494 + resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} 495 + engines: {node: '>=12'} 496 + 497 + ansi-styles@4.3.0: 498 + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 499 + engines: {node: '>=8'} 500 + 501 + ansi-styles@6.2.1: 502 + resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 503 + engines: {node: '>=12'} 504 + 505 + any-promise@1.3.0: 506 + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 507 + 508 + anymatch@3.1.3: 509 + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 510 + engines: {node: '>= 8'} 511 + 512 + arg@5.0.2: 513 + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 514 + 515 + argparse@2.0.1: 516 + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} 517 + 518 + array-union@2.1.0: 519 + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} 520 + engines: {node: '>=8'} 521 + 522 + asynckit@0.4.0: 523 + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} 524 + 525 + autoprefixer@10.4.21: 526 + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} 527 + engines: {node: ^10 || ^12 || >=14} 528 + hasBin: true 529 + peerDependencies: 530 + postcss: ^8.1.0 531 + 532 + axios@1.10.0: 533 + resolution: {integrity: sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==} 534 + 535 + balanced-match@1.0.2: 536 + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 537 + 538 + binary-extensions@2.3.0: 539 + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 540 + engines: {node: '>=8'} 541 + 542 + brace-expansion@1.1.12: 543 + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} 544 + 545 + brace-expansion@2.0.2: 546 + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} 547 + 548 + braces@3.0.3: 549 + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 550 + engines: {node: '>=8'} 551 + 552 + browserslist@4.25.1: 553 + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} 554 + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 555 + hasBin: true 556 + 557 + call-bind-apply-helpers@1.0.2: 558 + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} 559 + engines: {node: '>= 0.4'} 560 + 561 + callsites@3.1.0: 562 + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} 563 + engines: {node: '>=6'} 564 + 565 + camelcase-css@2.0.1: 566 + resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 567 + engines: {node: '>= 6'} 568 + 569 + caniuse-lite@1.0.30001727: 570 + resolution: {integrity: sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==} 571 + 572 + chalk@4.1.2: 573 + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} 574 + engines: {node: '>=10'} 575 + 576 + chokidar@3.6.0: 577 + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 578 + engines: {node: '>= 8.10.0'} 579 + 580 + color-convert@2.0.1: 581 + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 582 + engines: {node: '>=7.0.0'} 583 + 584 + color-name@1.1.4: 585 + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 586 + 587 + combined-stream@1.0.8: 588 + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} 589 + engines: {node: '>= 0.8'} 590 + 591 + commander@4.1.1: 592 + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 593 + engines: {node: '>= 6'} 594 + 595 + concat-map@0.0.1: 596 + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 597 + 598 + convert-source-map@2.0.0: 599 + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} 600 + 601 + cross-spawn@7.0.6: 602 + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} 603 + engines: {node: '>= 8'} 604 + 605 + cssesc@3.0.0: 606 + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 607 + engines: {node: '>=4'} 608 + hasBin: true 609 + 610 + csstype@3.1.3: 611 + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 612 + 613 + debug@4.4.1: 614 + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} 615 + engines: {node: '>=6.0'} 616 + peerDependencies: 617 + supports-color: '*' 618 + peerDependenciesMeta: 619 + supports-color: 620 + optional: true 621 + 622 + deep-is@0.1.4: 623 + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 624 + 625 + delayed-stream@1.0.0: 626 + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} 627 + engines: {node: '>=0.4.0'} 628 + 629 + didyoumean@1.2.2: 630 + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 631 + 632 + dir-glob@3.0.1: 633 + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 634 + engines: {node: '>=8'} 635 + 636 + dlv@1.1.3: 637 + resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 638 + 639 + doctrine@3.0.0: 640 + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} 641 + engines: {node: '>=6.0.0'} 642 + 643 + dunder-proto@1.0.1: 644 + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} 645 + engines: {node: '>= 0.4'} 646 + 647 + eastasianwidth@0.2.0: 648 + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 649 + 650 + electron-to-chromium@1.5.189: 651 + resolution: {integrity: sha512-y9D1ntS1ruO/pZ/V2FtLE+JXLQe28XoRpZ7QCCo0T8LdQladzdcOVQZH/IWLVJvCw12OGMb6hYOeOAjntCmJRQ==} 652 + 653 + emoji-regex@8.0.0: 654 + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 655 + 656 + emoji-regex@9.2.2: 657 + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 658 + 659 + es-define-property@1.0.1: 660 + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} 661 + engines: {node: '>= 0.4'} 662 + 663 + es-errors@1.3.0: 664 + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} 665 + engines: {node: '>= 0.4'} 666 + 667 + es-object-atoms@1.1.1: 668 + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} 669 + engines: {node: '>= 0.4'} 670 + 671 + es-set-tostringtag@2.1.0: 672 + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} 673 + engines: {node: '>= 0.4'} 674 + 675 + esbuild@0.18.20: 676 + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} 677 + engines: {node: '>=12'} 678 + hasBin: true 679 + 680 + escalade@3.2.0: 681 + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} 682 + engines: {node: '>=6'} 683 + 684 + escape-string-regexp@4.0.0: 685 + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} 686 + engines: {node: '>=10'} 687 + 688 + eslint-plugin-react-hooks@4.6.2: 689 + resolution: {integrity: sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==} 690 + engines: {node: '>=10'} 691 + peerDependencies: 692 + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 693 + 694 + eslint-plugin-react-refresh@0.4.20: 695 + resolution: {integrity: sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==} 696 + peerDependencies: 697 + eslint: '>=8.40' 698 + 699 + eslint-scope@7.2.2: 700 + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} 701 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 702 + 703 + eslint-visitor-keys@3.4.3: 704 + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} 705 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 706 + 707 + eslint@8.57.1: 708 + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} 709 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 710 + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. 711 + hasBin: true 712 + 713 + espree@9.6.1: 714 + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} 715 + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} 716 + 717 + esquery@1.6.0: 718 + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} 719 + engines: {node: '>=0.10'} 720 + 721 + esrecurse@4.3.0: 722 + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} 723 + engines: {node: '>=4.0'} 724 + 725 + estraverse@5.3.0: 726 + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} 727 + engines: {node: '>=4.0'} 728 + 729 + esutils@2.0.3: 730 + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 731 + engines: {node: '>=0.10.0'} 732 + 733 + fast-deep-equal@3.1.3: 734 + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} 735 + 736 + fast-glob@3.3.3: 737 + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} 738 + engines: {node: '>=8.6.0'} 739 + 740 + fast-json-stable-stringify@2.1.0: 741 + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} 742 + 743 + fast-levenshtein@2.0.6: 744 + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 745 + 746 + fastq@1.19.1: 747 + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} 748 + 749 + file-entry-cache@6.0.1: 750 + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} 751 + engines: {node: ^10.12.0 || >=12.0.0} 752 + 753 + fill-range@7.1.1: 754 + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 755 + engines: {node: '>=8'} 756 + 757 + find-up@5.0.0: 758 + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 759 + engines: {node: '>=10'} 760 + 761 + flat-cache@3.2.0: 762 + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} 763 + engines: {node: ^10.12.0 || >=12.0.0} 764 + 765 + flatted@3.3.3: 766 + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} 767 + 768 + follow-redirects@1.15.9: 769 + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} 770 + engines: {node: '>=4.0'} 771 + peerDependencies: 772 + debug: '*' 773 + peerDependenciesMeta: 774 + debug: 775 + optional: true 776 + 777 + foreground-child@3.3.1: 778 + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} 779 + engines: {node: '>=14'} 780 + 781 + form-data@4.0.4: 782 + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} 783 + engines: {node: '>= 6'} 784 + 785 + fraction.js@4.3.7: 786 + resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} 787 + 788 + fs.realpath@1.0.0: 789 + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 790 + 791 + fsevents@2.3.3: 792 + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 793 + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 794 + os: [darwin] 795 + 796 + function-bind@1.1.2: 797 + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 798 + 799 + gensync@1.0.0-beta.2: 800 + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} 801 + engines: {node: '>=6.9.0'} 802 + 803 + get-intrinsic@1.3.0: 804 + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} 805 + engines: {node: '>= 0.4'} 806 + 807 + get-proto@1.0.1: 808 + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} 809 + engines: {node: '>= 0.4'} 810 + 811 + glob-parent@5.1.2: 812 + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 813 + engines: {node: '>= 6'} 814 + 815 + glob-parent@6.0.2: 816 + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 817 + engines: {node: '>=10.13.0'} 818 + 819 + glob@10.4.5: 820 + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 821 + hasBin: true 822 + 823 + glob@7.2.3: 824 + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 825 + deprecated: Glob versions prior to v9 are no longer supported 826 + 827 + globals@13.24.0: 828 + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} 829 + engines: {node: '>=8'} 830 + 831 + globby@11.1.0: 832 + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} 833 + engines: {node: '>=10'} 834 + 835 + gopd@1.2.0: 836 + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} 837 + engines: {node: '>= 0.4'} 838 + 839 + graphemer@1.4.0: 840 + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} 841 + 842 + has-flag@4.0.0: 843 + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 844 + engines: {node: '>=8'} 845 + 846 + has-symbols@1.1.0: 847 + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} 848 + engines: {node: '>= 0.4'} 849 + 850 + has-tostringtag@1.0.2: 851 + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} 852 + engines: {node: '>= 0.4'} 853 + 854 + hasown@2.0.2: 855 + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 856 + engines: {node: '>= 0.4'} 857 + 858 + ignore@5.3.2: 859 + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} 860 + engines: {node: '>= 4'} 861 + 862 + import-fresh@3.3.1: 863 + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} 864 + engines: {node: '>=6'} 865 + 866 + imurmurhash@0.1.4: 867 + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} 868 + engines: {node: '>=0.8.19'} 869 + 870 + inflight@1.0.6: 871 + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 872 + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. 873 + 874 + inherits@2.0.4: 875 + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 876 + 877 + is-binary-path@2.1.0: 878 + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 879 + engines: {node: '>=8'} 880 + 881 + is-core-module@2.16.1: 882 + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} 883 + engines: {node: '>= 0.4'} 884 + 885 + is-extglob@2.1.1: 886 + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 887 + engines: {node: '>=0.10.0'} 888 + 889 + is-fullwidth-code-point@3.0.0: 890 + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 891 + engines: {node: '>=8'} 892 + 893 + is-glob@4.0.3: 894 + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 895 + engines: {node: '>=0.10.0'} 896 + 897 + is-number@7.0.0: 898 + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 899 + engines: {node: '>=0.12.0'} 900 + 901 + is-path-inside@3.0.3: 902 + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} 903 + engines: {node: '>=8'} 904 + 905 + isexe@2.0.0: 906 + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 907 + 908 + jackspeak@3.4.3: 909 + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 910 + 911 + jiti@1.21.7: 912 + resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==} 913 + hasBin: true 914 + 915 + js-tokens@4.0.0: 916 + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 917 + 918 + js-yaml@4.1.0: 919 + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} 920 + hasBin: true 921 + 922 + jsesc@3.1.0: 923 + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} 924 + engines: {node: '>=6'} 925 + hasBin: true 926 + 927 + json-buffer@3.0.1: 928 + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} 929 + 930 + json-schema-traverse@0.4.1: 931 + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} 932 + 933 + json-stable-stringify-without-jsonify@1.0.1: 934 + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} 935 + 936 + json5@2.2.3: 937 + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} 938 + engines: {node: '>=6'} 939 + hasBin: true 940 + 941 + keyv@4.5.4: 942 + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} 943 + 944 + levn@0.4.1: 945 + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 946 + engines: {node: '>= 0.8.0'} 947 + 948 + lilconfig@3.1.3: 949 + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} 950 + engines: {node: '>=14'} 951 + 952 + lines-and-columns@1.2.4: 953 + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 954 + 955 + locate-path@6.0.0: 956 + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 957 + engines: {node: '>=10'} 958 + 959 + lodash.merge@4.6.2: 960 + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} 961 + 962 + loose-envify@1.4.0: 963 + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 964 + hasBin: true 965 + 966 + lru-cache@10.4.3: 967 + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 968 + 969 + lru-cache@5.1.1: 970 + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} 971 + 972 + lucide-react@0.294.0: 973 + resolution: {integrity: sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA==} 974 + peerDependencies: 975 + react: ^16.5.1 || ^17.0.0 || ^18.0.0 976 + 977 + math-intrinsics@1.1.0: 978 + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} 979 + engines: {node: '>= 0.4'} 980 + 981 + merge2@1.4.1: 982 + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 983 + engines: {node: '>= 8'} 984 + 985 + micromatch@4.0.8: 986 + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 987 + engines: {node: '>=8.6'} 988 + 989 + mime-db@1.52.0: 990 + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} 991 + engines: {node: '>= 0.6'} 992 + 993 + mime-types@2.1.35: 994 + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} 995 + engines: {node: '>= 0.6'} 996 + 997 + minimatch@3.1.2: 998 + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 999 + 1000 + minimatch@9.0.3: 1001 + resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} 1002 + engines: {node: '>=16 || 14 >=14.17'} 1003 + 1004 + minimatch@9.0.5: 1005 + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 1006 + engines: {node: '>=16 || 14 >=14.17'} 1007 + 1008 + minipass@7.1.2: 1009 + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 1010 + engines: {node: '>=16 || 14 >=14.17'} 1011 + 1012 + ms@2.1.3: 1013 + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} 1014 + 1015 + mz@2.7.0: 1016 + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 1017 + 1018 + nanoid@3.3.11: 1019 + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} 1020 + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 1021 + hasBin: true 1022 + 1023 + natural-compare@1.4.0: 1024 + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} 1025 + 1026 + node-releases@2.0.19: 1027 + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} 1028 + 1029 + normalize-path@3.0.0: 1030 + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 1031 + engines: {node: '>=0.10.0'} 1032 + 1033 + normalize-range@0.1.2: 1034 + resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} 1035 + engines: {node: '>=0.10.0'} 1036 + 1037 + object-assign@4.1.1: 1038 + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 1039 + engines: {node: '>=0.10.0'} 1040 + 1041 + object-hash@3.0.0: 1042 + resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 1043 + engines: {node: '>= 6'} 1044 + 1045 + once@1.4.0: 1046 + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 1047 + 1048 + optionator@0.9.4: 1049 + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} 1050 + engines: {node: '>= 0.8.0'} 1051 + 1052 + p-limit@3.1.0: 1053 + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 1054 + engines: {node: '>=10'} 1055 + 1056 + p-locate@5.0.0: 1057 + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 1058 + engines: {node: '>=10'} 1059 + 1060 + package-json-from-dist@1.0.1: 1061 + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} 1062 + 1063 + parent-module@1.0.1: 1064 + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} 1065 + engines: {node: '>=6'} 1066 + 1067 + path-exists@4.0.0: 1068 + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 1069 + engines: {node: '>=8'} 1070 + 1071 + path-is-absolute@1.0.1: 1072 + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 1073 + engines: {node: '>=0.10.0'} 1074 + 1075 + path-key@3.1.1: 1076 + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 1077 + engines: {node: '>=8'} 1078 + 1079 + path-parse@1.0.7: 1080 + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 1081 + 1082 + path-scurry@1.11.1: 1083 + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 1084 + engines: {node: '>=16 || 14 >=14.18'} 1085 + 1086 + path-type@4.0.0: 1087 + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 1088 + engines: {node: '>=8'} 1089 + 1090 + picocolors@1.1.1: 1091 + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} 1092 + 1093 + picomatch@2.3.1: 1094 + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 1095 + engines: {node: '>=8.6'} 1096 + 1097 + pify@2.3.0: 1098 + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 1099 + engines: {node: '>=0.10.0'} 1100 + 1101 + pirates@4.0.7: 1102 + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} 1103 + engines: {node: '>= 6'} 1104 + 1105 + postcss-import@15.1.0: 1106 + resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 1107 + engines: {node: '>=14.0.0'} 1108 + peerDependencies: 1109 + postcss: ^8.0.0 1110 + 1111 + postcss-js@4.0.1: 1112 + resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 1113 + engines: {node: ^12 || ^14 || >= 16} 1114 + peerDependencies: 1115 + postcss: ^8.4.21 1116 + 1117 + postcss-load-config@4.0.2: 1118 + resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 1119 + engines: {node: '>= 14'} 1120 + peerDependencies: 1121 + postcss: '>=8.0.9' 1122 + ts-node: '>=9.0.0' 1123 + peerDependenciesMeta: 1124 + postcss: 1125 + optional: true 1126 + ts-node: 1127 + optional: true 1128 + 1129 + postcss-nested@6.2.0: 1130 + resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} 1131 + engines: {node: '>=12.0'} 1132 + peerDependencies: 1133 + postcss: ^8.2.14 1134 + 1135 + postcss-selector-parser@6.1.2: 1136 + resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} 1137 + engines: {node: '>=4'} 1138 + 1139 + postcss-value-parser@4.2.0: 1140 + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 1141 + 1142 + postcss@8.5.6: 1143 + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} 1144 + engines: {node: ^10 || ^12 || >=14} 1145 + 1146 + prelude-ls@1.2.1: 1147 + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 1148 + engines: {node: '>= 0.8.0'} 1149 + 1150 + proxy-from-env@1.1.0: 1151 + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} 1152 + 1153 + punycode@2.3.1: 1154 + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} 1155 + engines: {node: '>=6'} 1156 + 1157 + queue-microtask@1.2.3: 1158 + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 1159 + 1160 + react-dom@18.3.1: 1161 + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} 1162 + peerDependencies: 1163 + react: ^18.3.1 1164 + 1165 + react-refresh@0.17.0: 1166 + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} 1167 + engines: {node: '>=0.10.0'} 1168 + 1169 + react-router-dom@6.30.1: 1170 + resolution: {integrity: sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==} 1171 + engines: {node: '>=14.0.0'} 1172 + peerDependencies: 1173 + react: '>=16.8' 1174 + react-dom: '>=16.8' 1175 + 1176 + react-router@6.30.1: 1177 + resolution: {integrity: sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==} 1178 + engines: {node: '>=14.0.0'} 1179 + peerDependencies: 1180 + react: '>=16.8' 1181 + 1182 + react@18.3.1: 1183 + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 1184 + engines: {node: '>=0.10.0'} 1185 + 1186 + read-cache@1.0.0: 1187 + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 1188 + 1189 + readdirp@3.6.0: 1190 + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 1191 + engines: {node: '>=8.10.0'} 1192 + 1193 + resolve-from@4.0.0: 1194 + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} 1195 + engines: {node: '>=4'} 1196 + 1197 + resolve@1.22.10: 1198 + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} 1199 + engines: {node: '>= 0.4'} 1200 + hasBin: true 1201 + 1202 + reusify@1.1.0: 1203 + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} 1204 + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 1205 + 1206 + rimraf@3.0.2: 1207 + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 1208 + deprecated: Rimraf versions prior to v4 are no longer supported 1209 + hasBin: true 1210 + 1211 + rollup@3.29.5: 1212 + resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==} 1213 + engines: {node: '>=14.18.0', npm: '>=8.0.0'} 1214 + hasBin: true 1215 + 1216 + run-parallel@1.2.0: 1217 + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 1218 + 1219 + scheduler@0.23.2: 1220 + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} 1221 + 1222 + semver@6.3.1: 1223 + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} 1224 + hasBin: true 1225 + 1226 + semver@7.7.2: 1227 + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} 1228 + engines: {node: '>=10'} 1229 + hasBin: true 1230 + 1231 + shebang-command@2.0.0: 1232 + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 1233 + engines: {node: '>=8'} 1234 + 1235 + shebang-regex@3.0.0: 1236 + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 1237 + engines: {node: '>=8'} 1238 + 1239 + signal-exit@4.1.0: 1240 + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 1241 + engines: {node: '>=14'} 1242 + 1243 + slash@3.0.0: 1244 + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} 1245 + engines: {node: '>=8'} 1246 + 1247 + sonner@1.7.4: 1248 + resolution: {integrity: sha512-DIS8z4PfJRbIyfVFDVnK9rO3eYDtse4Omcm6bt0oEr5/jtLgysmjuBl1frJ9E/EQZrFmKx2A8m/s5s9CRXIzhw==} 1249 + peerDependencies: 1250 + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc 1251 + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc 1252 + 1253 + source-map-js@1.2.1: 1254 + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} 1255 + engines: {node: '>=0.10.0'} 1256 + 1257 + string-width@4.2.3: 1258 + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 1259 + engines: {node: '>=8'} 1260 + 1261 + string-width@5.1.2: 1262 + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 1263 + engines: {node: '>=12'} 1264 + 1265 + strip-ansi@6.0.1: 1266 + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 1267 + engines: {node: '>=8'} 1268 + 1269 + strip-ansi@7.1.0: 1270 + resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 1271 + engines: {node: '>=12'} 1272 + 1273 + strip-json-comments@3.1.1: 1274 + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} 1275 + engines: {node: '>=8'} 1276 + 1277 + sucrase@3.35.0: 1278 + resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 1279 + engines: {node: '>=16 || 14 >=14.17'} 1280 + hasBin: true 1281 + 1282 + supports-color@7.2.0: 1283 + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 1284 + engines: {node: '>=8'} 1285 + 1286 + supports-preserve-symlinks-flag@1.0.0: 1287 + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 1288 + engines: {node: '>= 0.4'} 1289 + 1290 + tailwindcss@3.4.17: 1291 + resolution: {integrity: sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==} 1292 + engines: {node: '>=14.0.0'} 1293 + hasBin: true 1294 + 1295 + text-table@0.2.0: 1296 + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} 1297 + 1298 + thenify-all@1.6.0: 1299 + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 1300 + engines: {node: '>=0.8'} 1301 + 1302 + thenify@3.3.1: 1303 + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 1304 + 1305 + to-regex-range@5.0.1: 1306 + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 1307 + engines: {node: '>=8.0'} 1308 + 1309 + ts-api-utils@1.4.3: 1310 + resolution: {integrity: sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==} 1311 + engines: {node: '>=16'} 1312 + peerDependencies: 1313 + typescript: '>=4.2.0' 1314 + 1315 + ts-interface-checker@0.1.13: 1316 + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 1317 + 1318 + type-check@0.4.0: 1319 + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 1320 + engines: {node: '>= 0.8.0'} 1321 + 1322 + type-fest@0.20.2: 1323 + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} 1324 + engines: {node: '>=10'} 1325 + 1326 + typescript@5.8.3: 1327 + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} 1328 + engines: {node: '>=14.17'} 1329 + hasBin: true 1330 + 1331 + update-browserslist-db@1.1.3: 1332 + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} 1333 + hasBin: true 1334 + peerDependencies: 1335 + browserslist: '>= 4.21.0' 1336 + 1337 + uri-js@4.4.1: 1338 + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} 1339 + 1340 + use-sync-external-store@1.5.0: 1341 + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} 1342 + peerDependencies: 1343 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 1344 + 1345 + util-deprecate@1.0.2: 1346 + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 1347 + 1348 + vite@4.5.14: 1349 + resolution: {integrity: sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==} 1350 + engines: {node: ^14.18.0 || >=16.0.0} 1351 + hasBin: true 1352 + peerDependencies: 1353 + '@types/node': '>= 14' 1354 + less: '*' 1355 + lightningcss: ^1.21.0 1356 + sass: '*' 1357 + stylus: '*' 1358 + sugarss: '*' 1359 + terser: ^5.4.0 1360 + peerDependenciesMeta: 1361 + '@types/node': 1362 + optional: true 1363 + less: 1364 + optional: true 1365 + lightningcss: 1366 + optional: true 1367 + sass: 1368 + optional: true 1369 + stylus: 1370 + optional: true 1371 + sugarss: 1372 + optional: true 1373 + terser: 1374 + optional: true 1375 + 1376 + which@2.0.2: 1377 + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 1378 + engines: {node: '>= 8'} 1379 + hasBin: true 1380 + 1381 + word-wrap@1.2.5: 1382 + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} 1383 + engines: {node: '>=0.10.0'} 1384 + 1385 + wrap-ansi@7.0.0: 1386 + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 1387 + engines: {node: '>=10'} 1388 + 1389 + wrap-ansi@8.1.0: 1390 + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 1391 + engines: {node: '>=12'} 1392 + 1393 + wrappy@1.0.2: 1394 + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 1395 + 1396 + yallist@3.1.1: 1397 + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} 1398 + 1399 + yaml@2.8.0: 1400 + resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==} 1401 + engines: {node: '>= 14.6'} 1402 + hasBin: true 1403 + 1404 + yocto-queue@0.1.0: 1405 + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 1406 + engines: {node: '>=10'} 1407 + 1408 + zustand@4.5.7: 1409 + resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} 1410 + engines: {node: '>=12.7.0'} 1411 + peerDependencies: 1412 + '@types/react': '>=16.8' 1413 + immer: '>=9.0.6' 1414 + react: '>=16.8' 1415 + peerDependenciesMeta: 1416 + '@types/react': 1417 + optional: true 1418 + immer: 1419 + optional: true 1420 + react: 1421 + optional: true 1422 + 1423 + snapshots: 1424 + 1425 + '@alloc/quick-lru@5.2.0': {} 1426 + 1427 + '@ampproject/remapping@2.3.0': 1428 + dependencies: 1429 + '@jridgewell/gen-mapping': 0.3.12 1430 + '@jridgewell/trace-mapping': 0.3.29 1431 + 1432 + '@babel/code-frame@7.27.1': 1433 + dependencies: 1434 + '@babel/helper-validator-identifier': 7.27.1 1435 + js-tokens: 4.0.0 1436 + picocolors: 1.1.1 1437 + 1438 + '@babel/compat-data@7.28.0': {} 1439 + 1440 + '@babel/core@7.28.0': 1441 + dependencies: 1442 + '@ampproject/remapping': 2.3.0 1443 + '@babel/code-frame': 7.27.1 1444 + '@babel/generator': 7.28.0 1445 + '@babel/helper-compilation-targets': 7.27.2 1446 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) 1447 + '@babel/helpers': 7.27.6 1448 + '@babel/parser': 7.28.0 1449 + '@babel/template': 7.27.2 1450 + '@babel/traverse': 7.28.0 1451 + '@babel/types': 7.28.1 1452 + convert-source-map: 2.0.0 1453 + debug: 4.4.1 1454 + gensync: 1.0.0-beta.2 1455 + json5: 2.2.3 1456 + semver: 6.3.1 1457 + transitivePeerDependencies: 1458 + - supports-color 1459 + 1460 + '@babel/generator@7.28.0': 1461 + dependencies: 1462 + '@babel/parser': 7.28.0 1463 + '@babel/types': 7.28.1 1464 + '@jridgewell/gen-mapping': 0.3.12 1465 + '@jridgewell/trace-mapping': 0.3.29 1466 + jsesc: 3.1.0 1467 + 1468 + '@babel/helper-compilation-targets@7.27.2': 1469 + dependencies: 1470 + '@babel/compat-data': 7.28.0 1471 + '@babel/helper-validator-option': 7.27.1 1472 + browserslist: 4.25.1 1473 + lru-cache: 5.1.1 1474 + semver: 6.3.1 1475 + 1476 + '@babel/helper-globals@7.28.0': {} 1477 + 1478 + '@babel/helper-module-imports@7.27.1': 1479 + dependencies: 1480 + '@babel/traverse': 7.28.0 1481 + '@babel/types': 7.28.1 1482 + transitivePeerDependencies: 1483 + - supports-color 1484 + 1485 + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': 1486 + dependencies: 1487 + '@babel/core': 7.28.0 1488 + '@babel/helper-module-imports': 7.27.1 1489 + '@babel/helper-validator-identifier': 7.27.1 1490 + '@babel/traverse': 7.28.0 1491 + transitivePeerDependencies: 1492 + - supports-color 1493 + 1494 + '@babel/helper-plugin-utils@7.27.1': {} 1495 + 1496 + '@babel/helper-string-parser@7.27.1': {} 1497 + 1498 + '@babel/helper-validator-identifier@7.27.1': {} 1499 + 1500 + '@babel/helper-validator-option@7.27.1': {} 1501 + 1502 + '@babel/helpers@7.27.6': 1503 + dependencies: 1504 + '@babel/template': 7.27.2 1505 + '@babel/types': 7.28.1 1506 + 1507 + '@babel/parser@7.28.0': 1508 + dependencies: 1509 + '@babel/types': 7.28.1 1510 + 1511 + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': 1512 + dependencies: 1513 + '@babel/core': 7.28.0 1514 + '@babel/helper-plugin-utils': 7.27.1 1515 + 1516 + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': 1517 + dependencies: 1518 + '@babel/core': 7.28.0 1519 + '@babel/helper-plugin-utils': 7.27.1 1520 + 1521 + '@babel/template@7.27.2': 1522 + dependencies: 1523 + '@babel/code-frame': 7.27.1 1524 + '@babel/parser': 7.28.0 1525 + '@babel/types': 7.28.1 1526 + 1527 + '@babel/traverse@7.28.0': 1528 + dependencies: 1529 + '@babel/code-frame': 7.27.1 1530 + '@babel/generator': 7.28.0 1531 + '@babel/helper-globals': 7.28.0 1532 + '@babel/parser': 7.28.0 1533 + '@babel/template': 7.27.2 1534 + '@babel/types': 7.28.1 1535 + debug: 4.4.1 1536 + transitivePeerDependencies: 1537 + - supports-color 1538 + 1539 + '@babel/types@7.28.1': 1540 + dependencies: 1541 + '@babel/helper-string-parser': 7.27.1 1542 + '@babel/helper-validator-identifier': 7.27.1 1543 + 1544 + '@esbuild/android-arm64@0.18.20': 1545 + optional: true 1546 + 1547 + '@esbuild/android-arm@0.18.20': 1548 + optional: true 1549 + 1550 + '@esbuild/android-x64@0.18.20': 1551 + optional: true 1552 + 1553 + '@esbuild/darwin-arm64@0.18.20': 1554 + optional: true 1555 + 1556 + '@esbuild/darwin-x64@0.18.20': 1557 + optional: true 1558 + 1559 + '@esbuild/freebsd-arm64@0.18.20': 1560 + optional: true 1561 + 1562 + '@esbuild/freebsd-x64@0.18.20': 1563 + optional: true 1564 + 1565 + '@esbuild/linux-arm64@0.18.20': 1566 + optional: true 1567 + 1568 + '@esbuild/linux-arm@0.18.20': 1569 + optional: true 1570 + 1571 + '@esbuild/linux-ia32@0.18.20': 1572 + optional: true 1573 + 1574 + '@esbuild/linux-loong64@0.18.20': 1575 + optional: true 1576 + 1577 + '@esbuild/linux-mips64el@0.18.20': 1578 + optional: true 1579 + 1580 + '@esbuild/linux-ppc64@0.18.20': 1581 + optional: true 1582 + 1583 + '@esbuild/linux-riscv64@0.18.20': 1584 + optional: true 1585 + 1586 + '@esbuild/linux-s390x@0.18.20': 1587 + optional: true 1588 + 1589 + '@esbuild/linux-x64@0.18.20': 1590 + optional: true 1591 + 1592 + '@esbuild/netbsd-x64@0.18.20': 1593 + optional: true 1594 + 1595 + '@esbuild/openbsd-x64@0.18.20': 1596 + optional: true 1597 + 1598 + '@esbuild/sunos-x64@0.18.20': 1599 + optional: true 1600 + 1601 + '@esbuild/win32-arm64@0.18.20': 1602 + optional: true 1603 + 1604 + '@esbuild/win32-ia32@0.18.20': 1605 + optional: true 1606 + 1607 + '@esbuild/win32-x64@0.18.20': 1608 + optional: true 1609 + 1610 + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': 1611 + dependencies: 1612 + eslint: 8.57.1 1613 + eslint-visitor-keys: 3.4.3 1614 + 1615 + '@eslint-community/regexpp@4.12.1': {} 1616 + 1617 + '@eslint/eslintrc@2.1.4': 1618 + dependencies: 1619 + ajv: 6.12.6 1620 + debug: 4.4.1 1621 + espree: 9.6.1 1622 + globals: 13.24.0 1623 + ignore: 5.3.2 1624 + import-fresh: 3.3.1 1625 + js-yaml: 4.1.0 1626 + minimatch: 3.1.2 1627 + strip-json-comments: 3.1.1 1628 + transitivePeerDependencies: 1629 + - supports-color 1630 + 1631 + '@eslint/js@8.57.1': {} 1632 + 1633 + '@humanwhocodes/config-array@0.13.0': 1634 + dependencies: 1635 + '@humanwhocodes/object-schema': 2.0.3 1636 + debug: 4.4.1 1637 + minimatch: 3.1.2 1638 + transitivePeerDependencies: 1639 + - supports-color 1640 + 1641 + '@humanwhocodes/module-importer@1.0.1': {} 1642 + 1643 + '@humanwhocodes/object-schema@2.0.3': {} 1644 + 1645 + '@isaacs/cliui@8.0.2': 1646 + dependencies: 1647 + string-width: 5.1.2 1648 + string-width-cjs: string-width@4.2.3 1649 + strip-ansi: 7.1.0 1650 + strip-ansi-cjs: strip-ansi@6.0.1 1651 + wrap-ansi: 8.1.0 1652 + wrap-ansi-cjs: wrap-ansi@7.0.0 1653 + 1654 + '@jridgewell/gen-mapping@0.3.12': 1655 + dependencies: 1656 + '@jridgewell/sourcemap-codec': 1.5.4 1657 + '@jridgewell/trace-mapping': 0.3.29 1658 + 1659 + '@jridgewell/resolve-uri@3.1.2': {} 1660 + 1661 + '@jridgewell/sourcemap-codec@1.5.4': {} 1662 + 1663 + '@jridgewell/trace-mapping@0.3.29': 1664 + dependencies: 1665 + '@jridgewell/resolve-uri': 3.1.2 1666 + '@jridgewell/sourcemap-codec': 1.5.4 1667 + 1668 + '@nodelib/fs.scandir@2.1.5': 1669 + dependencies: 1670 + '@nodelib/fs.stat': 2.0.5 1671 + run-parallel: 1.2.0 1672 + 1673 + '@nodelib/fs.stat@2.0.5': {} 1674 + 1675 + '@nodelib/fs.walk@1.2.8': 1676 + dependencies: 1677 + '@nodelib/fs.scandir': 2.1.5 1678 + fastq: 1.19.1 1679 + 1680 + '@pkgjs/parseargs@0.11.0': 1681 + optional: true 1682 + 1683 + '@remix-run/router@1.23.0': {} 1684 + 1685 + '@rolldown/pluginutils@1.0.0-beta.27': {} 1686 + 1687 + '@tanstack/query-core@5.83.0': {} 1688 + 1689 + '@tanstack/react-query@5.83.0(react@18.3.1)': 1690 + dependencies: 1691 + '@tanstack/query-core': 5.83.0 1692 + react: 18.3.1 1693 + 1694 + '@types/babel__core@7.20.5': 1695 + dependencies: 1696 + '@babel/parser': 7.28.0 1697 + '@babel/types': 7.28.1 1698 + '@types/babel__generator': 7.27.0 1699 + '@types/babel__template': 7.4.4 1700 + '@types/babel__traverse': 7.20.7 1701 + 1702 + '@types/babel__generator@7.27.0': 1703 + dependencies: 1704 + '@babel/types': 7.28.1 1705 + 1706 + '@types/babel__template@7.4.4': 1707 + dependencies: 1708 + '@babel/parser': 7.28.0 1709 + '@babel/types': 7.28.1 1710 + 1711 + '@types/babel__traverse@7.20.7': 1712 + dependencies: 1713 + '@babel/types': 7.28.1 1714 + 1715 + '@types/json-schema@7.0.15': {} 1716 + 1717 + '@types/prop-types@15.7.15': {} 1718 + 1719 + '@types/react-dom@18.3.7(@types/react@18.3.23)': 1720 + dependencies: 1721 + '@types/react': 18.3.23 1722 + 1723 + '@types/react@18.3.23': 1724 + dependencies: 1725 + '@types/prop-types': 15.7.15 1726 + csstype: 3.1.3 1727 + 1728 + '@types/semver@7.7.0': {} 1729 + 1730 + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3))(eslint@8.57.1)(typescript@5.8.3)': 1731 + dependencies: 1732 + '@eslint-community/regexpp': 4.12.1 1733 + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.3) 1734 + '@typescript-eslint/scope-manager': 6.21.0 1735 + '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) 1736 + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) 1737 + '@typescript-eslint/visitor-keys': 6.21.0 1738 + debug: 4.4.1 1739 + eslint: 8.57.1 1740 + graphemer: 1.4.0 1741 + ignore: 5.3.2 1742 + natural-compare: 1.4.0 1743 + semver: 7.7.2 1744 + ts-api-utils: 1.4.3(typescript@5.8.3) 1745 + optionalDependencies: 1746 + typescript: 5.8.3 1747 + transitivePeerDependencies: 1748 + - supports-color 1749 + 1750 + '@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.3)': 1751 + dependencies: 1752 + '@typescript-eslint/scope-manager': 6.21.0 1753 + '@typescript-eslint/types': 6.21.0 1754 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) 1755 + '@typescript-eslint/visitor-keys': 6.21.0 1756 + debug: 4.4.1 1757 + eslint: 8.57.1 1758 + optionalDependencies: 1759 + typescript: 5.8.3 1760 + transitivePeerDependencies: 1761 + - supports-color 1762 + 1763 + '@typescript-eslint/scope-manager@6.21.0': 1764 + dependencies: 1765 + '@typescript-eslint/types': 6.21.0 1766 + '@typescript-eslint/visitor-keys': 6.21.0 1767 + 1768 + '@typescript-eslint/type-utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)': 1769 + dependencies: 1770 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) 1771 + '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.3) 1772 + debug: 4.4.1 1773 + eslint: 8.57.1 1774 + ts-api-utils: 1.4.3(typescript@5.8.3) 1775 + optionalDependencies: 1776 + typescript: 5.8.3 1777 + transitivePeerDependencies: 1778 + - supports-color 1779 + 1780 + '@typescript-eslint/types@6.21.0': {} 1781 + 1782 + '@typescript-eslint/typescript-estree@6.21.0(typescript@5.8.3)': 1783 + dependencies: 1784 + '@typescript-eslint/types': 6.21.0 1785 + '@typescript-eslint/visitor-keys': 6.21.0 1786 + debug: 4.4.1 1787 + globby: 11.1.0 1788 + is-glob: 4.0.3 1789 + minimatch: 9.0.3 1790 + semver: 7.7.2 1791 + ts-api-utils: 1.4.3(typescript@5.8.3) 1792 + optionalDependencies: 1793 + typescript: 5.8.3 1794 + transitivePeerDependencies: 1795 + - supports-color 1796 + 1797 + '@typescript-eslint/utils@6.21.0(eslint@8.57.1)(typescript@5.8.3)': 1798 + dependencies: 1799 + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) 1800 + '@types/json-schema': 7.0.15 1801 + '@types/semver': 7.7.0 1802 + '@typescript-eslint/scope-manager': 6.21.0 1803 + '@typescript-eslint/types': 6.21.0 1804 + '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.3) 1805 + eslint: 8.57.1 1806 + semver: 7.7.2 1807 + transitivePeerDependencies: 1808 + - supports-color 1809 + - typescript 1810 + 1811 + '@typescript-eslint/visitor-keys@6.21.0': 1812 + dependencies: 1813 + '@typescript-eslint/types': 6.21.0 1814 + eslint-visitor-keys: 3.4.3 1815 + 1816 + '@ungap/structured-clone@1.3.0': {} 1817 + 1818 + '@vitejs/plugin-react@4.7.0(vite@4.5.14)': 1819 + dependencies: 1820 + '@babel/core': 7.28.0 1821 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) 1822 + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) 1823 + '@rolldown/pluginutils': 1.0.0-beta.27 1824 + '@types/babel__core': 7.20.5 1825 + react-refresh: 0.17.0 1826 + vite: 4.5.14 1827 + transitivePeerDependencies: 1828 + - supports-color 1829 + 1830 + acorn-jsx@5.3.2(acorn@8.15.0): 1831 + dependencies: 1832 + acorn: 8.15.0 1833 + 1834 + acorn@8.15.0: {} 1835 + 1836 + ajv@6.12.6: 1837 + dependencies: 1838 + fast-deep-equal: 3.1.3 1839 + fast-json-stable-stringify: 2.1.0 1840 + json-schema-traverse: 0.4.1 1841 + uri-js: 4.4.1 1842 + 1843 + ansi-regex@5.0.1: {} 1844 + 1845 + ansi-regex@6.1.0: {} 1846 + 1847 + ansi-styles@4.3.0: 1848 + dependencies: 1849 + color-convert: 2.0.1 1850 + 1851 + ansi-styles@6.2.1: {} 1852 + 1853 + any-promise@1.3.0: {} 1854 + 1855 + anymatch@3.1.3: 1856 + dependencies: 1857 + normalize-path: 3.0.0 1858 + picomatch: 2.3.1 1859 + 1860 + arg@5.0.2: {} 1861 + 1862 + argparse@2.0.1: {} 1863 + 1864 + array-union@2.1.0: {} 1865 + 1866 + asynckit@0.4.0: {} 1867 + 1868 + autoprefixer@10.4.21(postcss@8.5.6): 1869 + dependencies: 1870 + browserslist: 4.25.1 1871 + caniuse-lite: 1.0.30001727 1872 + fraction.js: 4.3.7 1873 + normalize-range: 0.1.2 1874 + picocolors: 1.1.1 1875 + postcss: 8.5.6 1876 + postcss-value-parser: 4.2.0 1877 + 1878 + axios@1.10.0: 1879 + dependencies: 1880 + follow-redirects: 1.15.9 1881 + form-data: 4.0.4 1882 + proxy-from-env: 1.1.0 1883 + transitivePeerDependencies: 1884 + - debug 1885 + 1886 + balanced-match@1.0.2: {} 1887 + 1888 + binary-extensions@2.3.0: {} 1889 + 1890 + brace-expansion@1.1.12: 1891 + dependencies: 1892 + balanced-match: 1.0.2 1893 + concat-map: 0.0.1 1894 + 1895 + brace-expansion@2.0.2: 1896 + dependencies: 1897 + balanced-match: 1.0.2 1898 + 1899 + braces@3.0.3: 1900 + dependencies: 1901 + fill-range: 7.1.1 1902 + 1903 + browserslist@4.25.1: 1904 + dependencies: 1905 + caniuse-lite: 1.0.30001727 1906 + electron-to-chromium: 1.5.189 1907 + node-releases: 2.0.19 1908 + update-browserslist-db: 1.1.3(browserslist@4.25.1) 1909 + 1910 + call-bind-apply-helpers@1.0.2: 1911 + dependencies: 1912 + es-errors: 1.3.0 1913 + function-bind: 1.1.2 1914 + 1915 + callsites@3.1.0: {} 1916 + 1917 + camelcase-css@2.0.1: {} 1918 + 1919 + caniuse-lite@1.0.30001727: {} 1920 + 1921 + chalk@4.1.2: 1922 + dependencies: 1923 + ansi-styles: 4.3.0 1924 + supports-color: 7.2.0 1925 + 1926 + chokidar@3.6.0: 1927 + dependencies: 1928 + anymatch: 3.1.3 1929 + braces: 3.0.3 1930 + glob-parent: 5.1.2 1931 + is-binary-path: 2.1.0 1932 + is-glob: 4.0.3 1933 + normalize-path: 3.0.0 1934 + readdirp: 3.6.0 1935 + optionalDependencies: 1936 + fsevents: 2.3.3 1937 + 1938 + color-convert@2.0.1: 1939 + dependencies: 1940 + color-name: 1.1.4 1941 + 1942 + color-name@1.1.4: {} 1943 + 1944 + combined-stream@1.0.8: 1945 + dependencies: 1946 + delayed-stream: 1.0.0 1947 + 1948 + commander@4.1.1: {} 1949 + 1950 + concat-map@0.0.1: {} 1951 + 1952 + convert-source-map@2.0.0: {} 1953 + 1954 + cross-spawn@7.0.6: 1955 + dependencies: 1956 + path-key: 3.1.1 1957 + shebang-command: 2.0.0 1958 + which: 2.0.2 1959 + 1960 + cssesc@3.0.0: {} 1961 + 1962 + csstype@3.1.3: {} 1963 + 1964 + debug@4.4.1: 1965 + dependencies: 1966 + ms: 2.1.3 1967 + 1968 + deep-is@0.1.4: {} 1969 + 1970 + delayed-stream@1.0.0: {} 1971 + 1972 + didyoumean@1.2.2: {} 1973 + 1974 + dir-glob@3.0.1: 1975 + dependencies: 1976 + path-type: 4.0.0 1977 + 1978 + dlv@1.1.3: {} 1979 + 1980 + doctrine@3.0.0: 1981 + dependencies: 1982 + esutils: 2.0.3 1983 + 1984 + dunder-proto@1.0.1: 1985 + dependencies: 1986 + call-bind-apply-helpers: 1.0.2 1987 + es-errors: 1.3.0 1988 + gopd: 1.2.0 1989 + 1990 + eastasianwidth@0.2.0: {} 1991 + 1992 + electron-to-chromium@1.5.189: {} 1993 + 1994 + emoji-regex@8.0.0: {} 1995 + 1996 + emoji-regex@9.2.2: {} 1997 + 1998 + es-define-property@1.0.1: {} 1999 + 2000 + es-errors@1.3.0: {} 2001 + 2002 + es-object-atoms@1.1.1: 2003 + dependencies: 2004 + es-errors: 1.3.0 2005 + 2006 + es-set-tostringtag@2.1.0: 2007 + dependencies: 2008 + es-errors: 1.3.0 2009 + get-intrinsic: 1.3.0 2010 + has-tostringtag: 1.0.2 2011 + hasown: 2.0.2 2012 + 2013 + esbuild@0.18.20: 2014 + optionalDependencies: 2015 + '@esbuild/android-arm': 0.18.20 2016 + '@esbuild/android-arm64': 0.18.20 2017 + '@esbuild/android-x64': 0.18.20 2018 + '@esbuild/darwin-arm64': 0.18.20 2019 + '@esbuild/darwin-x64': 0.18.20 2020 + '@esbuild/freebsd-arm64': 0.18.20 2021 + '@esbuild/freebsd-x64': 0.18.20 2022 + '@esbuild/linux-arm': 0.18.20 2023 + '@esbuild/linux-arm64': 0.18.20 2024 + '@esbuild/linux-ia32': 0.18.20 2025 + '@esbuild/linux-loong64': 0.18.20 2026 + '@esbuild/linux-mips64el': 0.18.20 2027 + '@esbuild/linux-ppc64': 0.18.20 2028 + '@esbuild/linux-riscv64': 0.18.20 2029 + '@esbuild/linux-s390x': 0.18.20 2030 + '@esbuild/linux-x64': 0.18.20 2031 + '@esbuild/netbsd-x64': 0.18.20 2032 + '@esbuild/openbsd-x64': 0.18.20 2033 + '@esbuild/sunos-x64': 0.18.20 2034 + '@esbuild/win32-arm64': 0.18.20 2035 + '@esbuild/win32-ia32': 0.18.20 2036 + '@esbuild/win32-x64': 0.18.20 2037 + 2038 + escalade@3.2.0: {} 2039 + 2040 + escape-string-regexp@4.0.0: {} 2041 + 2042 + eslint-plugin-react-hooks@4.6.2(eslint@8.57.1): 2043 + dependencies: 2044 + eslint: 8.57.1 2045 + 2046 + eslint-plugin-react-refresh@0.4.20(eslint@8.57.1): 2047 + dependencies: 2048 + eslint: 8.57.1 2049 + 2050 + eslint-scope@7.2.2: 2051 + dependencies: 2052 + esrecurse: 4.3.0 2053 + estraverse: 5.3.0 2054 + 2055 + eslint-visitor-keys@3.4.3: {} 2056 + 2057 + eslint@8.57.1: 2058 + dependencies: 2059 + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) 2060 + '@eslint-community/regexpp': 4.12.1 2061 + '@eslint/eslintrc': 2.1.4 2062 + '@eslint/js': 8.57.1 2063 + '@humanwhocodes/config-array': 0.13.0 2064 + '@humanwhocodes/module-importer': 1.0.1 2065 + '@nodelib/fs.walk': 1.2.8 2066 + '@ungap/structured-clone': 1.3.0 2067 + ajv: 6.12.6 2068 + chalk: 4.1.2 2069 + cross-spawn: 7.0.6 2070 + debug: 4.4.1 2071 + doctrine: 3.0.0 2072 + escape-string-regexp: 4.0.0 2073 + eslint-scope: 7.2.2 2074 + eslint-visitor-keys: 3.4.3 2075 + espree: 9.6.1 2076 + esquery: 1.6.0 2077 + esutils: 2.0.3 2078 + fast-deep-equal: 3.1.3 2079 + file-entry-cache: 6.0.1 2080 + find-up: 5.0.0 2081 + glob-parent: 6.0.2 2082 + globals: 13.24.0 2083 + graphemer: 1.4.0 2084 + ignore: 5.3.2 2085 + imurmurhash: 0.1.4 2086 + is-glob: 4.0.3 2087 + is-path-inside: 3.0.3 2088 + js-yaml: 4.1.0 2089 + json-stable-stringify-without-jsonify: 1.0.1 2090 + levn: 0.4.1 2091 + lodash.merge: 4.6.2 2092 + minimatch: 3.1.2 2093 + natural-compare: 1.4.0 2094 + optionator: 0.9.4 2095 + strip-ansi: 6.0.1 2096 + text-table: 0.2.0 2097 + transitivePeerDependencies: 2098 + - supports-color 2099 + 2100 + espree@9.6.1: 2101 + dependencies: 2102 + acorn: 8.15.0 2103 + acorn-jsx: 5.3.2(acorn@8.15.0) 2104 + eslint-visitor-keys: 3.4.3 2105 + 2106 + esquery@1.6.0: 2107 + dependencies: 2108 + estraverse: 5.3.0 2109 + 2110 + esrecurse@4.3.0: 2111 + dependencies: 2112 + estraverse: 5.3.0 2113 + 2114 + estraverse@5.3.0: {} 2115 + 2116 + esutils@2.0.3: {} 2117 + 2118 + fast-deep-equal@3.1.3: {} 2119 + 2120 + fast-glob@3.3.3: 2121 + dependencies: 2122 + '@nodelib/fs.stat': 2.0.5 2123 + '@nodelib/fs.walk': 1.2.8 2124 + glob-parent: 5.1.2 2125 + merge2: 1.4.1 2126 + micromatch: 4.0.8 2127 + 2128 + fast-json-stable-stringify@2.1.0: {} 2129 + 2130 + fast-levenshtein@2.0.6: {} 2131 + 2132 + fastq@1.19.1: 2133 + dependencies: 2134 + reusify: 1.1.0 2135 + 2136 + file-entry-cache@6.0.1: 2137 + dependencies: 2138 + flat-cache: 3.2.0 2139 + 2140 + fill-range@7.1.1: 2141 + dependencies: 2142 + to-regex-range: 5.0.1 2143 + 2144 + find-up@5.0.0: 2145 + dependencies: 2146 + locate-path: 6.0.0 2147 + path-exists: 4.0.0 2148 + 2149 + flat-cache@3.2.0: 2150 + dependencies: 2151 + flatted: 3.3.3 2152 + keyv: 4.5.4 2153 + rimraf: 3.0.2 2154 + 2155 + flatted@3.3.3: {} 2156 + 2157 + follow-redirects@1.15.9: {} 2158 + 2159 + foreground-child@3.3.1: 2160 + dependencies: 2161 + cross-spawn: 7.0.6 2162 + signal-exit: 4.1.0 2163 + 2164 + form-data@4.0.4: 2165 + dependencies: 2166 + asynckit: 0.4.0 2167 + combined-stream: 1.0.8 2168 + es-set-tostringtag: 2.1.0 2169 + hasown: 2.0.2 2170 + mime-types: 2.1.35 2171 + 2172 + fraction.js@4.3.7: {} 2173 + 2174 + fs.realpath@1.0.0: {} 2175 + 2176 + fsevents@2.3.3: 2177 + optional: true 2178 + 2179 + function-bind@1.1.2: {} 2180 + 2181 + gensync@1.0.0-beta.2: {} 2182 + 2183 + get-intrinsic@1.3.0: 2184 + dependencies: 2185 + call-bind-apply-helpers: 1.0.2 2186 + es-define-property: 1.0.1 2187 + es-errors: 1.3.0 2188 + es-object-atoms: 1.1.1 2189 + function-bind: 1.1.2 2190 + get-proto: 1.0.1 2191 + gopd: 1.2.0 2192 + has-symbols: 1.1.0 2193 + hasown: 2.0.2 2194 + math-intrinsics: 1.1.0 2195 + 2196 + get-proto@1.0.1: 2197 + dependencies: 2198 + dunder-proto: 1.0.1 2199 + es-object-atoms: 1.1.1 2200 + 2201 + glob-parent@5.1.2: 2202 + dependencies: 2203 + is-glob: 4.0.3 2204 + 2205 + glob-parent@6.0.2: 2206 + dependencies: 2207 + is-glob: 4.0.3 2208 + 2209 + glob@10.4.5: 2210 + dependencies: 2211 + foreground-child: 3.3.1 2212 + jackspeak: 3.4.3 2213 + minimatch: 9.0.5 2214 + minipass: 7.1.2 2215 + package-json-from-dist: 1.0.1 2216 + path-scurry: 1.11.1 2217 + 2218 + glob@7.2.3: 2219 + dependencies: 2220 + fs.realpath: 1.0.0 2221 + inflight: 1.0.6 2222 + inherits: 2.0.4 2223 + minimatch: 3.1.2 2224 + once: 1.4.0 2225 + path-is-absolute: 1.0.1 2226 + 2227 + globals@13.24.0: 2228 + dependencies: 2229 + type-fest: 0.20.2 2230 + 2231 + globby@11.1.0: 2232 + dependencies: 2233 + array-union: 2.1.0 2234 + dir-glob: 3.0.1 2235 + fast-glob: 3.3.3 2236 + ignore: 5.3.2 2237 + merge2: 1.4.1 2238 + slash: 3.0.0 2239 + 2240 + gopd@1.2.0: {} 2241 + 2242 + graphemer@1.4.0: {} 2243 + 2244 + has-flag@4.0.0: {} 2245 + 2246 + has-symbols@1.1.0: {} 2247 + 2248 + has-tostringtag@1.0.2: 2249 + dependencies: 2250 + has-symbols: 1.1.0 2251 + 2252 + hasown@2.0.2: 2253 + dependencies: 2254 + function-bind: 1.1.2 2255 + 2256 + ignore@5.3.2: {} 2257 + 2258 + import-fresh@3.3.1: 2259 + dependencies: 2260 + parent-module: 1.0.1 2261 + resolve-from: 4.0.0 2262 + 2263 + imurmurhash@0.1.4: {} 2264 + 2265 + inflight@1.0.6: 2266 + dependencies: 2267 + once: 1.4.0 2268 + wrappy: 1.0.2 2269 + 2270 + inherits@2.0.4: {} 2271 + 2272 + is-binary-path@2.1.0: 2273 + dependencies: 2274 + binary-extensions: 2.3.0 2275 + 2276 + is-core-module@2.16.1: 2277 + dependencies: 2278 + hasown: 2.0.2 2279 + 2280 + is-extglob@2.1.1: {} 2281 + 2282 + is-fullwidth-code-point@3.0.0: {} 2283 + 2284 + is-glob@4.0.3: 2285 + dependencies: 2286 + is-extglob: 2.1.1 2287 + 2288 + is-number@7.0.0: {} 2289 + 2290 + is-path-inside@3.0.3: {} 2291 + 2292 + isexe@2.0.0: {} 2293 + 2294 + jackspeak@3.4.3: 2295 + dependencies: 2296 + '@isaacs/cliui': 8.0.2 2297 + optionalDependencies: 2298 + '@pkgjs/parseargs': 0.11.0 2299 + 2300 + jiti@1.21.7: {} 2301 + 2302 + js-tokens@4.0.0: {} 2303 + 2304 + js-yaml@4.1.0: 2305 + dependencies: 2306 + argparse: 2.0.1 2307 + 2308 + jsesc@3.1.0: {} 2309 + 2310 + json-buffer@3.0.1: {} 2311 + 2312 + json-schema-traverse@0.4.1: {} 2313 + 2314 + json-stable-stringify-without-jsonify@1.0.1: {} 2315 + 2316 + json5@2.2.3: {} 2317 + 2318 + keyv@4.5.4: 2319 + dependencies: 2320 + json-buffer: 3.0.1 2321 + 2322 + levn@0.4.1: 2323 + dependencies: 2324 + prelude-ls: 1.2.1 2325 + type-check: 0.4.0 2326 + 2327 + lilconfig@3.1.3: {} 2328 + 2329 + lines-and-columns@1.2.4: {} 2330 + 2331 + locate-path@6.0.0: 2332 + dependencies: 2333 + p-locate: 5.0.0 2334 + 2335 + lodash.merge@4.6.2: {} 2336 + 2337 + loose-envify@1.4.0: 2338 + dependencies: 2339 + js-tokens: 4.0.0 2340 + 2341 + lru-cache@10.4.3: {} 2342 + 2343 + lru-cache@5.1.1: 2344 + dependencies: 2345 + yallist: 3.1.1 2346 + 2347 + lucide-react@0.294.0(react@18.3.1): 2348 + dependencies: 2349 + react: 18.3.1 2350 + 2351 + math-intrinsics@1.1.0: {} 2352 + 2353 + merge2@1.4.1: {} 2354 + 2355 + micromatch@4.0.8: 2356 + dependencies: 2357 + braces: 3.0.3 2358 + picomatch: 2.3.1 2359 + 2360 + mime-db@1.52.0: {} 2361 + 2362 + mime-types@2.1.35: 2363 + dependencies: 2364 + mime-db: 1.52.0 2365 + 2366 + minimatch@3.1.2: 2367 + dependencies: 2368 + brace-expansion: 1.1.12 2369 + 2370 + minimatch@9.0.3: 2371 + dependencies: 2372 + brace-expansion: 2.0.2 2373 + 2374 + minimatch@9.0.5: 2375 + dependencies: 2376 + brace-expansion: 2.0.2 2377 + 2378 + minipass@7.1.2: {} 2379 + 2380 + ms@2.1.3: {} 2381 + 2382 + mz@2.7.0: 2383 + dependencies: 2384 + any-promise: 1.3.0 2385 + object-assign: 4.1.1 2386 + thenify-all: 1.6.0 2387 + 2388 + nanoid@3.3.11: {} 2389 + 2390 + natural-compare@1.4.0: {} 2391 + 2392 + node-releases@2.0.19: {} 2393 + 2394 + normalize-path@3.0.0: {} 2395 + 2396 + normalize-range@0.1.2: {} 2397 + 2398 + object-assign@4.1.1: {} 2399 + 2400 + object-hash@3.0.0: {} 2401 + 2402 + once@1.4.0: 2403 + dependencies: 2404 + wrappy: 1.0.2 2405 + 2406 + optionator@0.9.4: 2407 + dependencies: 2408 + deep-is: 0.1.4 2409 + fast-levenshtein: 2.0.6 2410 + levn: 0.4.1 2411 + prelude-ls: 1.2.1 2412 + type-check: 0.4.0 2413 + word-wrap: 1.2.5 2414 + 2415 + p-limit@3.1.0: 2416 + dependencies: 2417 + yocto-queue: 0.1.0 2418 + 2419 + p-locate@5.0.0: 2420 + dependencies: 2421 + p-limit: 3.1.0 2422 + 2423 + package-json-from-dist@1.0.1: {} 2424 + 2425 + parent-module@1.0.1: 2426 + dependencies: 2427 + callsites: 3.1.0 2428 + 2429 + path-exists@4.0.0: {} 2430 + 2431 + path-is-absolute@1.0.1: {} 2432 + 2433 + path-key@3.1.1: {} 2434 + 2435 + path-parse@1.0.7: {} 2436 + 2437 + path-scurry@1.11.1: 2438 + dependencies: 2439 + lru-cache: 10.4.3 2440 + minipass: 7.1.2 2441 + 2442 + path-type@4.0.0: {} 2443 + 2444 + picocolors@1.1.1: {} 2445 + 2446 + picomatch@2.3.1: {} 2447 + 2448 + pify@2.3.0: {} 2449 + 2450 + pirates@4.0.7: {} 2451 + 2452 + postcss-import@15.1.0(postcss@8.5.6): 2453 + dependencies: 2454 + postcss: 8.5.6 2455 + postcss-value-parser: 4.2.0 2456 + read-cache: 1.0.0 2457 + resolve: 1.22.10 2458 + 2459 + postcss-js@4.0.1(postcss@8.5.6): 2460 + dependencies: 2461 + camelcase-css: 2.0.1 2462 + postcss: 8.5.6 2463 + 2464 + postcss-load-config@4.0.2(postcss@8.5.6): 2465 + dependencies: 2466 + lilconfig: 3.1.3 2467 + yaml: 2.8.0 2468 + optionalDependencies: 2469 + postcss: 8.5.6 2470 + 2471 + postcss-nested@6.2.0(postcss@8.5.6): 2472 + dependencies: 2473 + postcss: 8.5.6 2474 + postcss-selector-parser: 6.1.2 2475 + 2476 + postcss-selector-parser@6.1.2: 2477 + dependencies: 2478 + cssesc: 3.0.0 2479 + util-deprecate: 1.0.2 2480 + 2481 + postcss-value-parser@4.2.0: {} 2482 + 2483 + postcss@8.5.6: 2484 + dependencies: 2485 + nanoid: 3.3.11 2486 + picocolors: 1.1.1 2487 + source-map-js: 1.2.1 2488 + 2489 + prelude-ls@1.2.1: {} 2490 + 2491 + proxy-from-env@1.1.0: {} 2492 + 2493 + punycode@2.3.1: {} 2494 + 2495 + queue-microtask@1.2.3: {} 2496 + 2497 + react-dom@18.3.1(react@18.3.1): 2498 + dependencies: 2499 + loose-envify: 1.4.0 2500 + react: 18.3.1 2501 + scheduler: 0.23.2 2502 + 2503 + react-refresh@0.17.0: {} 2504 + 2505 + react-router-dom@6.30.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 2506 + dependencies: 2507 + '@remix-run/router': 1.23.0 2508 + react: 18.3.1 2509 + react-dom: 18.3.1(react@18.3.1) 2510 + react-router: 6.30.1(react@18.3.1) 2511 + 2512 + react-router@6.30.1(react@18.3.1): 2513 + dependencies: 2514 + '@remix-run/router': 1.23.0 2515 + react: 18.3.1 2516 + 2517 + react@18.3.1: 2518 + dependencies: 2519 + loose-envify: 1.4.0 2520 + 2521 + read-cache@1.0.0: 2522 + dependencies: 2523 + pify: 2.3.0 2524 + 2525 + readdirp@3.6.0: 2526 + dependencies: 2527 + picomatch: 2.3.1 2528 + 2529 + resolve-from@4.0.0: {} 2530 + 2531 + resolve@1.22.10: 2532 + dependencies: 2533 + is-core-module: 2.16.1 2534 + path-parse: 1.0.7 2535 + supports-preserve-symlinks-flag: 1.0.0 2536 + 2537 + reusify@1.1.0: {} 2538 + 2539 + rimraf@3.0.2: 2540 + dependencies: 2541 + glob: 7.2.3 2542 + 2543 + rollup@3.29.5: 2544 + optionalDependencies: 2545 + fsevents: 2.3.3 2546 + 2547 + run-parallel@1.2.0: 2548 + dependencies: 2549 + queue-microtask: 1.2.3 2550 + 2551 + scheduler@0.23.2: 2552 + dependencies: 2553 + loose-envify: 1.4.0 2554 + 2555 + semver@6.3.1: {} 2556 + 2557 + semver@7.7.2: {} 2558 + 2559 + shebang-command@2.0.0: 2560 + dependencies: 2561 + shebang-regex: 3.0.0 2562 + 2563 + shebang-regex@3.0.0: {} 2564 + 2565 + signal-exit@4.1.0: {} 2566 + 2567 + slash@3.0.0: {} 2568 + 2569 + sonner@1.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 2570 + dependencies: 2571 + react: 18.3.1 2572 + react-dom: 18.3.1(react@18.3.1) 2573 + 2574 + source-map-js@1.2.1: {} 2575 + 2576 + string-width@4.2.3: 2577 + dependencies: 2578 + emoji-regex: 8.0.0 2579 + is-fullwidth-code-point: 3.0.0 2580 + strip-ansi: 6.0.1 2581 + 2582 + string-width@5.1.2: 2583 + dependencies: 2584 + eastasianwidth: 0.2.0 2585 + emoji-regex: 9.2.2 2586 + strip-ansi: 7.1.0 2587 + 2588 + strip-ansi@6.0.1: 2589 + dependencies: 2590 + ansi-regex: 5.0.1 2591 + 2592 + strip-ansi@7.1.0: 2593 + dependencies: 2594 + ansi-regex: 6.1.0 2595 + 2596 + strip-json-comments@3.1.1: {} 2597 + 2598 + sucrase@3.35.0: 2599 + dependencies: 2600 + '@jridgewell/gen-mapping': 0.3.12 2601 + commander: 4.1.1 2602 + glob: 10.4.5 2603 + lines-and-columns: 1.2.4 2604 + mz: 2.7.0 2605 + pirates: 4.0.7 2606 + ts-interface-checker: 0.1.13 2607 + 2608 + supports-color@7.2.0: 2609 + dependencies: 2610 + has-flag: 4.0.0 2611 + 2612 + supports-preserve-symlinks-flag@1.0.0: {} 2613 + 2614 + tailwindcss@3.4.17: 2615 + dependencies: 2616 + '@alloc/quick-lru': 5.2.0 2617 + arg: 5.0.2 2618 + chokidar: 3.6.0 2619 + didyoumean: 1.2.2 2620 + dlv: 1.1.3 2621 + fast-glob: 3.3.3 2622 + glob-parent: 6.0.2 2623 + is-glob: 4.0.3 2624 + jiti: 1.21.7 2625 + lilconfig: 3.1.3 2626 + micromatch: 4.0.8 2627 + normalize-path: 3.0.0 2628 + object-hash: 3.0.0 2629 + picocolors: 1.1.1 2630 + postcss: 8.5.6 2631 + postcss-import: 15.1.0(postcss@8.5.6) 2632 + postcss-js: 4.0.1(postcss@8.5.6) 2633 + postcss-load-config: 4.0.2(postcss@8.5.6) 2634 + postcss-nested: 6.2.0(postcss@8.5.6) 2635 + postcss-selector-parser: 6.1.2 2636 + resolve: 1.22.10 2637 + sucrase: 3.35.0 2638 + transitivePeerDependencies: 2639 + - ts-node 2640 + 2641 + text-table@0.2.0: {} 2642 + 2643 + thenify-all@1.6.0: 2644 + dependencies: 2645 + thenify: 3.3.1 2646 + 2647 + thenify@3.3.1: 2648 + dependencies: 2649 + any-promise: 1.3.0 2650 + 2651 + to-regex-range@5.0.1: 2652 + dependencies: 2653 + is-number: 7.0.0 2654 + 2655 + ts-api-utils@1.4.3(typescript@5.8.3): 2656 + dependencies: 2657 + typescript: 5.8.3 2658 + 2659 + ts-interface-checker@0.1.13: {} 2660 + 2661 + type-check@0.4.0: 2662 + dependencies: 2663 + prelude-ls: 1.2.1 2664 + 2665 + type-fest@0.20.2: {} 2666 + 2667 + typescript@5.8.3: {} 2668 + 2669 + update-browserslist-db@1.1.3(browserslist@4.25.1): 2670 + dependencies: 2671 + browserslist: 4.25.1 2672 + escalade: 3.2.0 2673 + picocolors: 1.1.1 2674 + 2675 + uri-js@4.4.1: 2676 + dependencies: 2677 + punycode: 2.3.1 2678 + 2679 + use-sync-external-store@1.5.0(react@18.3.1): 2680 + dependencies: 2681 + react: 18.3.1 2682 + 2683 + util-deprecate@1.0.2: {} 2684 + 2685 + vite@4.5.14: 2686 + dependencies: 2687 + esbuild: 0.18.20 2688 + postcss: 8.5.6 2689 + rollup: 3.29.5 2690 + optionalDependencies: 2691 + fsevents: 2.3.3 2692 + 2693 + which@2.0.2: 2694 + dependencies: 2695 + isexe: 2.0.0 2696 + 2697 + word-wrap@1.2.5: {} 2698 + 2699 + wrap-ansi@7.0.0: 2700 + dependencies: 2701 + ansi-styles: 4.3.0 2702 + string-width: 4.2.3 2703 + strip-ansi: 6.0.1 2704 + 2705 + wrap-ansi@8.1.0: 2706 + dependencies: 2707 + ansi-styles: 6.2.1 2708 + string-width: 5.1.2 2709 + strip-ansi: 7.1.0 2710 + 2711 + wrappy@1.0.2: {} 2712 + 2713 + yallist@3.1.1: {} 2714 + 2715 + yaml@2.8.0: {} 2716 + 2717 + yocto-queue@0.1.0: {} 2718 + 2719 + zustand@4.5.7(@types/react@18.3.23)(react@18.3.1): 2720 + dependencies: 2721 + use-sync-external-store: 1.5.0(react@18.3.1) 2722 + optionalDependencies: 2723 + '@types/react': 18.3.23 2724 + react: 18.3.1
+6
web/postcss.config.js
··· 1 + export default { 2 + plugins: { 3 + tailwindcss: {}, 4 + autoprefixer: {}, 5 + }, 6 + };
+51
web/src/App.tsx
··· 1 + import { Routes, Route, Navigate } from 'react-router-dom' 2 + import { useAuthStore } from './stores/authStore' 3 + import LandingPage from './pages/LandingPage' 4 + import LoginPage from './pages/LoginPage' 5 + import StatusPage from './pages/StatusPage' 6 + import PrivacyPage from './pages/PrivacyPage' 7 + import TermsPage from './pages/TermsPage' 8 + import DashboardPage from './pages/DashboardPage' 9 + import TodosPage from './pages/TodosPage' 10 + import ApiKeysPage from './pages/ApiKeysPage' 11 + import RemindersPage from './pages/RemindersPage' 12 + import Layout from './components/Layout' 13 + import { useEffect } from 'react' 14 + 15 + function App() { 16 + const { isAuthenticated, checkAuth } = useAuthStore() 17 + 18 + useEffect(() => { 19 + checkAuth() 20 + }, []) 21 + 22 + return ( 23 + <Routes> 24 + {/* Public routes */} 25 + <Route path="/" element={<LandingPage />} /> 26 + <Route path="/login" element={isAuthenticated ? <Navigate to="/dashboard" replace /> : <LoginPage />} /> 27 + <Route path="/status" element={<StatusPage />} /> 28 + <Route path="/legal/privacy" element={<PrivacyPage />} /> 29 + <Route path="/legal/terms" element={<TermsPage />} /> 30 + 31 + {/* Protected routes */} 32 + {isAuthenticated ? ( 33 + <Route path="/*" element={ 34 + <Layout> 35 + <Routes> 36 + <Route path="/dashboard" element={<DashboardPage />} /> 37 + <Route path="/todos" element={<TodosPage />} /> 38 + <Route path="/reminders" element={<RemindersPage />} /> 39 + <Route path="/api-keys" element={<ApiKeysPage />} /> 40 + <Route path="*" element={<Navigate to="/dashboard" replace />} /> 41 + </Routes> 42 + </Layout> 43 + } /> 44 + ) : ( 45 + <Route path="*" element={<Navigate to="/" replace />} /> 46 + )} 47 + </Routes> 48 + ) 49 + } 50 + 51 + export default App
+167
web/src/components/Layout.tsx
··· 1 + import { ReactNode, useState } from 'react' 2 + import { Link, useLocation } from 'react-router-dom' 3 + import { 4 + LayoutDashboard, 5 + CheckSquare, 6 + Key, 7 + Bell, 8 + Menu, 9 + X, 10 + LogOut, 11 + User 12 + } from 'lucide-react' 13 + import { useAuthStore } from '../stores/authStore' 14 + import { toast } from 'sonner' 15 + 16 + interface LayoutProps { 17 + children: ReactNode 18 + } 19 + 20 + const Layout = ({ children }: LayoutProps) => { 21 + const [sidebarOpen, setSidebarOpen] = useState(false) 22 + const location = useLocation() 23 + const { user, logout } = useAuthStore() 24 + 25 + const navigation = [ 26 + { name: 'Dashboard', href: '/dashboard', icon: LayoutDashboard }, 27 + { name: 'Todos', href: '/todos', icon: CheckSquare }, 28 + { name: 'Reminders', href: '/reminders', icon: Bell }, 29 + { name: 'API Keys', href: '/api-keys', icon: Key }, 30 + ] 31 + 32 + const handleLogout = () => { 33 + logout() 34 + toast.success('Logged out successfully') 35 + } 36 + 37 + return ( 38 + <div className="min-h-screen bg-[#0A0A0A]"> 39 + {/* Mobile sidebar */} 40 + <div className={`fixed inset-0 z-50 lg:hidden ${ 41 + sidebarOpen ? 'block' : 'hidden' 42 + }`}> 43 + <div className="fixed inset-0 bg-gray-600 bg-opacity-75" onClick={() => setSidebarOpen(false)} /> 44 + <div className="fixed inset-y-0 left-0 flex w-64 flex-col bg-gray-900/50 border-r border-gray-700"> 45 + <div className="flex h-16 items-center justify-between px-4"> 46 + <h1 className="text-xl font-bold text-white">Aethel</h1> 47 + <button 48 + onClick={() => setSidebarOpen(false)} 49 + className="text-gray-400 hover:text-gray-600" 50 + > 51 + <X className="h-6 w-6" /> 52 + </button> 53 + </div> 54 + <nav className="flex-1 space-y-1 px-2 py-4"> 55 + {navigation.map((item) => { 56 + const Icon = item.icon 57 + const isActive = location.pathname === item.href 58 + return ( 59 + <Link 60 + key={item.name} 61 + to={item.href} 62 + onClick={() => setSidebarOpen(false)} 63 + className={`group flex items-center px-2 py-2 text-sm font-medium rounded-lg ${ 64 + isActive 65 + ? 'bg-gray-800 text-white' 66 + : 'text-gray-300 hover:bg-gray-700 hover:text-white' 67 + }`} 68 + > 69 + <Icon className="mr-3 h-5 w-5" /> 70 + {item.name} 71 + </Link> 72 + ) 73 + })} 74 + </nav> 75 + </div> 76 + </div> 77 + 78 + {/* Desktop sidebar */} 79 + <div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col"> 80 + <div className="flex flex-col flex-grow bg-gray-900/50 border-r border-gray-700"> 81 + <div className="flex h-16 items-center px-4"> 82 + <h1 className="text-xl font-bold text-white">Aethel Dashboard</h1> 83 + </div> 84 + <nav className="flex-1 space-y-1 px-2 py-4"> 85 + {navigation.map((item) => { 86 + const Icon = item.icon 87 + const isActive = location.pathname === item.href 88 + return ( 89 + <Link 90 + key={item.name} 91 + to={item.href} 92 + className={`group flex items-center px-2 py-2 text-sm font-medium rounded-lg ${ 93 + isActive 94 + ? 'bg-gray-800 text-white' 95 + : 'text-gray-300 hover:bg-gray-700 hover:text-white' 96 + }`} 97 + > 98 + <Icon className="mr-3 h-5 w-5" /> 99 + {item.name} 100 + </Link> 101 + ) 102 + })} 103 + </nav> 104 + 105 + {/* User info and logout */} 106 + <div className="border-t border-gray-700 p-4"> 107 + <div className="flex items-center mb-3"> 108 + <div className="flex-shrink-0"> 109 + {user?.avatar ? ( 110 + <img 111 + className="h-8 w-8 rounded-full" 112 + src={`https://cdn.discordapp.com/avatars/${user.id}/${user.avatar}.png`} 113 + alt={user.username} 114 + /> 115 + ) : ( 116 + <div className="h-8 w-8 rounded-full bg-gray-800 flex items-center justify-center"> 117 + <User className="h-4 w-4 text-white" /> 118 + </div> 119 + )} 120 + </div> 121 + <div className="ml-3"> 122 + <p className="text-sm font-medium text-gray-300"> 123 + {user?.discriminator && user.discriminator !== '0' 124 + ? `${user.username}#${user.discriminator}` 125 + : user?.username} 126 + </p> 127 + </div> 128 + </div> 129 + <button 130 + onClick={handleLogout} 131 + className="flex w-full items-center px-2 py-2 text-sm font-medium text-gray-300 rounded-lg hover:bg-gray-700 hover:text-white" 132 + > 133 + <LogOut className="mr-3 h-5 w-5" /> 134 + Logout 135 + </button> 136 + </div> 137 + </div> 138 + </div> 139 + 140 + {/* Main content */} 141 + <div className="lg:pl-64"> 142 + {/* Mobile header */} 143 + <div className="sticky top-0 z-40 flex h-16 shrink-0 items-center gap-x-4 border-b border-gray-700 bg-gray-900/50 px-4 shadow-sm lg:hidden"> 144 + <button 145 + type="button" 146 + className="-m-2.5 p-2.5 text-gray-300 lg:hidden" 147 + onClick={() => setSidebarOpen(true)} 148 + > 149 + <Menu className="h-6 w-6" /> 150 + </button> 151 + <div className="flex-1 text-sm font-semibold leading-6 text-white"> 152 + Aethel Dashboard 153 + </div> 154 + </div> 155 + 156 + {/* Page content */} 157 + <main className="pt-20 pb-12"> 158 + <div className="mx-auto max-w-6xl px-12 sm:px-16 lg:px-20"> 159 + {children} 160 + </div> 161 + </main> 162 + </div> 163 + </div> 164 + ) 165 + } 166 + 167 + export default Layout
+35
web/src/index.css
··· 1 + @tailwind base; 2 + @tailwind components; 3 + @tailwind utilities; 4 + 5 + body { 6 + font-family: Inter, system-ui, sans-serif; 7 + } 8 + 9 + .btn { 10 + @apply inline-flex items-center justify-center rounded-lg px-4 py-2 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50; 11 + } 12 + 13 + .btn-primary { 14 + @apply bg-blue-600 text-white hover:bg-blue-700; 15 + } 16 + 17 + .btn-secondary { 18 + @apply bg-gray-700 text-white hover:bg-gray-600 border border-gray-600; 19 + } 20 + 21 + .btn-danger { 22 + @apply bg-red-600 text-white hover:bg-red-700; 23 + } 24 + 25 + .btn-success { 26 + @apply bg-green-600 text-white hover:bg-green-700; 27 + } 28 + 29 + .card { 30 + @apply bg-gray-900/50 rounded-lg border border-gray-700 shadow-sm; 31 + } 32 + 33 + .input { 34 + @apply flex h-10 w-full rounded-lg border border-gray-600 bg-gray-800 text-white px-3 py-2 text-sm placeholder:text-gray-400 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 focus-visible:ring-offset-0 disabled:cursor-not-allowed disabled:opacity-50; 35 + }
+73
web/src/lib/api.ts
··· 1 + import axios from 'axios'; 2 + import { toast } from 'sonner'; 3 + 4 + const api = axios.create({ 5 + baseURL: '/api', 6 + timeout: 10000, 7 + }); 8 + 9 + api.interceptors.request.use( 10 + (config) => { 11 + const token = localStorage.getItem('token'); 12 + if (token) { 13 + config.headers.Authorization = `Bearer ${token}`; 14 + } 15 + return config; 16 + }, 17 + (error) => { 18 + return Promise.reject(error); 19 + } 20 + ); 21 + 22 + api.interceptors.response.use( 23 + (response) => response, 24 + (error) => { 25 + if (error.response?.status === 401) { 26 + localStorage.removeItem('token'); 27 + window.location.href = '/'; 28 + toast.error('Session expired. Please login again.'); 29 + } else if (error.response?.status >= 500) { 30 + toast.error('Server error. Please try again later.'); 31 + } else if (error.response?.data?.message) { 32 + toast.error(error.response.data.message); 33 + } else { 34 + toast.error('An unexpected error occurred.'); 35 + } 36 + return Promise.reject(error); 37 + } 38 + ); 39 + 40 + export default api; 41 + 42 + export const authAPI = { 43 + getDiscordAuthUrl: () => api.get('/auth/discord'), 44 + getMe: () => api.get('/auth/me'), 45 + logout: () => api.post('/auth/logout'), 46 + }; 47 + 48 + export const todosAPI = { 49 + getTodos: () => api.get('/todos'), 50 + createTodo: (data: { item: string }) => api.post('/todos', data), 51 + updateTodo: (id: number, data: { item?: string; done?: boolean }) => 52 + api.put(`/todos/${id}`, data), 53 + deleteTodo: (id: number) => api.delete(`/todos/${id}`), 54 + clearTodos: () => api.delete('/todos'), 55 + }; 56 + 57 + export const apiKeysAPI = { 58 + getApiKeys: () => api.get('/user/api-keys'), 59 + updateApiKey: (data: { apiKey?: string; model?: string; apiUrl?: string }) => 60 + api.post('/user/api-keys', data), 61 + deleteApiKey: () => api.delete('/user/api-keys'), 62 + testApiKey: (data: { apiKey: string; model?: string; apiUrl?: string }) => 63 + api.post('/user/api-keys/test', data), 64 + }; 65 + 66 + export const remindersAPI = { 67 + getReminders: () => api.get('/reminders'), 68 + createReminder: (data: { message: string; expires_at: string }) => api.post('/reminders', data), 69 + getReminder: (id: string) => api.get(`/reminders/${id}`), 70 + completeReminder: (id: string) => api.patch(`/reminders/${id}/complete`), 71 + getActiveReminders: () => api.get('/reminders/active/all'), 72 + clearCompletedReminders: () => api.delete('/reminders/completed'), 73 + };
+27
web/src/main.tsx
··· 1 + import React from 'react' 2 + import ReactDOM from 'react-dom/client' 3 + import { QueryClient, QueryClientProvider } from '@tanstack/react-query' 4 + import { BrowserRouter } from 'react-router-dom' 5 + import { Toaster } from 'sonner' 6 + import App from './App.tsx' 7 + import './index.css' 8 + 9 + const queryClient = new QueryClient({ 10 + defaultOptions: { 11 + queries: { 12 + retry: 1, 13 + refetchOnWindowFocus: false, 14 + }, 15 + }, 16 + }) 17 + 18 + ReactDOM.createRoot(document.getElementById('root')!).render( 19 + <React.StrictMode> 20 + <QueryClientProvider client={queryClient}> 21 + <BrowserRouter> 22 + <App /> 23 + <Toaster position="top-right" /> 24 + </BrowserRouter> 25 + </QueryClientProvider> 26 + </React.StrictMode> 27 + )
+340
web/src/pages/ApiKeysPage.tsx
··· 1 + import { useState } from 'react' 2 + import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' 3 + import { Key, Eye, EyeOff, TestTube, Save, Trash2, AlertCircle, CheckCircle } from 'lucide-react' 4 + import { toast } from 'sonner' 5 + import { apiKeysAPI } from '../lib/api' 6 + 7 + const ApiKeysPage = () => { 8 + const [showApiKey, setShowApiKey] = useState(false) 9 + const [formData, setFormData] = useState({ 10 + apiKey: '', 11 + model: '', 12 + apiUrl: '' 13 + }) 14 + const [isEditing, setIsEditing] = useState(false) 15 + const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null) 16 + const queryClient = useQueryClient() 17 + 18 + const { data: apiKeyInfo, isLoading } = useQuery({ 19 + queryKey: ['api-keys'], 20 + queryFn: () => apiKeysAPI.getApiKeys().then(res => res.data), 21 + }) 22 + 23 + const updateApiKeyMutation = useMutation({ 24 + mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => 25 + apiKeysAPI.updateApiKey(data), 26 + onSuccess: () => { 27 + queryClient.invalidateQueries({ queryKey: ['api-keys'] }) 28 + setIsEditing(false) 29 + setFormData({ apiKey: '', model: '', apiUrl: '' }) 30 + toast.success('API key updated successfully!') 31 + }, 32 + onError: () => { 33 + toast.error('Failed to update API key') 34 + }, 35 + }) 36 + 37 + const deleteApiKeyMutation = useMutation({ 38 + mutationFn: () => apiKeysAPI.deleteApiKey(), 39 + onSuccess: () => { 40 + queryClient.invalidateQueries({ queryKey: ['api-keys'] }) 41 + setFormData({ apiKey: '', model: '', apiUrl: '' }) 42 + setIsEditing(false) 43 + toast.success('API key deleted successfully!') 44 + }, 45 + onError: () => { 46 + toast.error('Failed to delete API key') 47 + }, 48 + }) 49 + 50 + const testApiKeyMutation = useMutation({ 51 + mutationFn: (data: { apiKey: string; model?: string; apiUrl?: string }) => 52 + apiKeysAPI.testApiKey(data), 53 + onSuccess: () => { 54 + setTestResult({ success: true, message: 'API key is valid and working!' }) 55 + toast.success('API key test successful!') 56 + }, 57 + onError: (error: any) => { 58 + const message = error.response?.data?.error || 'API key test failed' 59 + setTestResult({ success: false, message }) 60 + toast.error(message) 61 + }, 62 + }) 63 + 64 + const handleSubmit = (e: React.FormEvent) => { 65 + e.preventDefault() 66 + if (!formData.apiKey.trim()) { 67 + toast.error('API key is required') 68 + return 69 + } 70 + updateApiKeyMutation.mutate({ 71 + apiKey: formData.apiKey, 72 + model: formData.model || undefined, 73 + apiUrl: formData.apiUrl || undefined, 74 + }) 75 + } 76 + 77 + const handleTest = () => { 78 + if (!formData.apiKey.trim()) { 79 + toast.error('API key is required for testing') 80 + return 81 + } 82 + setTestResult(null) 83 + testApiKeyMutation.mutate({ 84 + apiKey: formData.apiKey, 85 + model: formData.model || undefined, 86 + apiUrl: formData.apiUrl || undefined, 87 + }) 88 + } 89 + 90 + const handleEdit = () => { 91 + setIsEditing(true) 92 + setFormData({ 93 + apiKey: '', 94 + model: apiKeyInfo?.model || '', 95 + apiUrl: apiKeyInfo?.apiUrl || '' 96 + }) 97 + setTestResult(null) 98 + } 99 + 100 + const handleCancel = () => { 101 + setIsEditing(false) 102 + setFormData({ apiKey: '', model: '', apiUrl: '' }) 103 + setTestResult(null) 104 + } 105 + 106 + if (isLoading) { 107 + return ( 108 + <div className="flex items-center justify-center h-64"> 109 + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-discord-blurple"></div> 110 + </div> 111 + ) 112 + } 113 + 114 + return ( 115 + <div className="space-y-6"> 116 + {/* Header */} 117 + <div> 118 + <h1 className="text-2xl font-bold text-white">AI API Keys</h1> 119 + <p className="text-gray-400"> 120 + Configure your custom AI API keys and endpoints for personalized AI interactions. 121 + </p> 122 + </div> 123 + 124 + {/* Current Status */} 125 + <div className="bg-gray-900/50 p-6 rounded-lg border border-gray-700"> 126 + <div className="flex items-center justify-between mb-4"> 127 + <h2 className="text-lg font-medium text-white">Current Configuration</h2> 128 + {apiKeyInfo?.hasApiKey && !isEditing && ( 129 + <div className="flex space-x-2"> 130 + <button 131 + onClick={handleEdit} 132 + className="btn btn-secondary" 133 + > 134 + <Key className="h-4 w-4 mr-2" /> 135 + Edit 136 + </button> 137 + <button 138 + onClick={() => deleteApiKeyMutation.mutate()} 139 + className="btn btn-danger" 140 + disabled={deleteApiKeyMutation.isPending} 141 + > 142 + <Trash2 className="h-4 w-4 mr-2" /> 143 + Remove 144 + </button> 145 + </div> 146 + )} 147 + </div> 148 + 149 + {apiKeyInfo?.hasApiKey && !isEditing ? ( 150 + <div className="space-y-3"> 151 + <div className="flex items-center justify-between"> 152 + <span className="text-sm font-medium text-gray-400">Status</span> 153 + <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-900/30 text-green-400 border border-green-700"> 154 + <CheckCircle className="h-3 w-3 mr-1" /> 155 + Configured 156 + </span> 157 + </div> 158 + {apiKeyInfo.model && ( 159 + <div className="flex items-center justify-between"> 160 + <span className="text-sm font-medium text-gray-400">Model</span> 161 + <span className="text-sm text-white">{apiKeyInfo.model}</span> 162 + </div> 163 + )} 164 + {apiKeyInfo.apiUrl && ( 165 + <div className="flex items-center justify-between"> 166 + <span className="text-sm font-medium text-gray-400">API Endpoint</span> 167 + <span className="text-sm text-white truncate max-w-64"> 168 + {apiKeyInfo.apiUrl} 169 + </span> 170 + </div> 171 + )} 172 + </div> 173 + ) : ( 174 + <div className="text-center py-8"> 175 + <Key className="h-12 w-12 text-gray-400 mx-auto mb-4" /> 176 + <h3 className="text-lg font-medium text-white mb-2"> 177 + No API Key Configured 178 + </h3> 179 + <p className="text-gray-400 mb-4"> 180 + Set up your custom AI API key to use personalized models and endpoints. 181 + </p> 182 + <button 183 + onClick={() => setIsEditing(true)} 184 + className="btn btn-primary" 185 + > 186 + <Key className="h-4 w-4 mr-2" /> 187 + Configure API Key 188 + </button> 189 + </div> 190 + )} 191 + </div> 192 + 193 + {/* Configuration Form */} 194 + {isEditing && ( 195 + <div className="bg-gray-900/50 p-6 rounded-lg border border-gray-700"> 196 + <h2 className="text-lg font-medium text-white mb-4"> 197 + {apiKeyInfo?.hasApiKey ? 'Update' : 'Configure'} API Key 198 + </h2> 199 + 200 + <form onSubmit={handleSubmit} className="space-y-4"> 201 + {/* API Key */} 202 + <div> 203 + <label className="block text-sm font-medium text-gray-400 mb-2"> 204 + API Key * 205 + </label> 206 + <div className="relative"> 207 + <input 208 + type={showApiKey ? 'text' : 'password'} 209 + value={formData.apiKey} 210 + onChange={(e) => setFormData({ ...formData, apiKey: e.target.value })} 211 + placeholder="Enter your API key" 212 + className="input pr-10" 213 + required 214 + /> 215 + <button 216 + type="button" 217 + onClick={() => setShowApiKey(!showApiKey)} 218 + className="absolute inset-y-0 right-0 pr-3 flex items-center" 219 + > 220 + {showApiKey ? ( 221 + <EyeOff className="h-4 w-4 text-gray-400" /> 222 + ) : ( 223 + <Eye className="h-4 w-4 text-gray-400" /> 224 + )} 225 + </button> 226 + </div> 227 + </div> 228 + 229 + {/* Model */} 230 + <div> 231 + <label className="block text-sm font-medium text-gray-400 mb-2"> 232 + Model (Optional) 233 + </label> 234 + <input 235 + type="text" 236 + value={formData.model} 237 + onChange={(e) => setFormData({ ...formData, model: e.target.value })} 238 + placeholder="e.g., gpt-4, claude-3-opus" 239 + className="input" 240 + /> 241 + <p className="text-xs text-gray-400 mt-1"> 242 + Leave empty to use the default model 243 + </p> 244 + </div> 245 + 246 + {/* API URL */} 247 + <div> 248 + <label className="block text-sm font-medium text-gray-400 mb-2"> 249 + API Endpoint URL (Optional) 250 + </label> 251 + <input 252 + type="url" 253 + value={formData.apiUrl} 254 + onChange={(e) => setFormData({ ...formData, apiUrl: e.target.value })} 255 + placeholder="https://api.openai.com/v1/chat/completions" 256 + className="input" 257 + /> 258 + <p className="text-xs text-gray-400 mt-1"> 259 + Enter the full API endpoint URL including the path (e.g., /chat/completions) 260 + </p> 261 + </div> 262 + 263 + {/* Test Result */} 264 + {testResult && ( 265 + <div className={`p-3 rounded-lg flex items-center space-x-2 ${ 266 + testResult.success ? 'bg-green-50 text-green-800' : 'bg-red-50 text-red-800' 267 + }`}> 268 + {testResult.success ? ( 269 + <CheckCircle className="h-4 w-4" /> 270 + ) : ( 271 + <AlertCircle className="h-4 w-4" /> 272 + )} 273 + <span className="text-sm">{testResult.message}</span> 274 + </div> 275 + )} 276 + 277 + {/* Actions */} 278 + <div className="flex justify-between pt-4"> 279 + <button 280 + type="button" 281 + onClick={handleTest} 282 + disabled={!formData.apiKey.trim() || testApiKeyMutation.isPending} 283 + className="btn btn-secondary" 284 + > 285 + <TestTube className="h-4 w-4 mr-2" /> 286 + {testApiKeyMutation.isPending ? 'Testing...' : 'Test API Key'} 287 + </button> 288 + 289 + <div className="flex space-x-3"> 290 + <button 291 + type="button" 292 + onClick={handleCancel} 293 + className="btn btn-secondary" 294 + disabled={updateApiKeyMutation.isPending} 295 + > 296 + Cancel 297 + </button> 298 + <button 299 + type="submit" 300 + disabled={!formData.apiKey.trim() || updateApiKeyMutation.isPending} 301 + className="btn btn-primary" 302 + > 303 + <Save className="h-4 w-4 mr-2" /> 304 + {updateApiKeyMutation.isPending ? 'Saving...' : 'Save'} 305 + </button> 306 + </div> 307 + </div> 308 + </form> 309 + </div> 310 + )} 311 + 312 + {/* Information */} 313 + <div className="bg-gray-800/50 p-6 rounded-lg border border-gray-700"> 314 + <h2 className="text-lg font-medium text-white mb-4">Information</h2> 315 + <div className="space-y-3 text-sm text-gray-400"> 316 + <div className="flex items-start space-x-2"> 317 + <AlertCircle className="h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0" /> 318 + <p> 319 + Your API key is encrypted and stored securely. It will only be used for AI interactions within the Discord bot. 320 + </p> 321 + </div> 322 + <div className="flex items-start space-x-2"> 323 + <Key className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" /> 324 + <p> 325 + Supported providers include OpenAI, Anthropic, and any OpenAI-compatible API endpoints. 326 + </p> 327 + </div> 328 + <div className="flex items-start space-x-2"> 329 + <TestTube className="h-4 w-4 text-purple-500 mt-0.5 flex-shrink-0" /> 330 + <p> 331 + Use the test function to verify your API key works before saving. 332 + </p> 333 + </div> 334 + </div> 335 + </div> 336 + </div> 337 + ) 338 + } 339 + 340 + export default ApiKeysPage
+338
web/src/pages/DashboardPage.tsx
··· 1 + import { useQuery } from '@tanstack/react-query' 2 + import { CheckSquare, Key, Clock, TrendingUp, Bell, AlertCircle } from 'lucide-react' 3 + import { todosAPI, apiKeysAPI, remindersAPI } from '../lib/api' 4 + import { useAuthStore } from '../stores/authStore' 5 + import { useEffect, useState } from 'react' 6 + import { toast } from 'sonner' 7 + 8 + const DashboardPage = () => { 9 + const { user } = useAuthStore() 10 + const [notifications, setNotifications] = useState<any[]>([]) 11 + 12 + const { data: todos } = useQuery({ 13 + queryKey: ['todos'], 14 + queryFn: () => todosAPI.getTodos().then(res => res.data), 15 + }) 16 + 17 + const { data: apiKeyInfo } = useQuery({ 18 + queryKey: ['api-keys'], 19 + queryFn: () => apiKeysAPI.getApiKeys().then(res => res.data), 20 + }) 21 + 22 + const { data: reminders } = useQuery({ 23 + queryKey: ['reminders'], 24 + queryFn: () => remindersAPI.getReminders().then(res => res.data.reminders), 25 + }) 26 + 27 + const { data: activeReminders } = useQuery({ 28 + queryKey: ['active-reminders'], 29 + queryFn: () => remindersAPI.getActiveReminders().then(res => res.data.reminders), 30 + refetchInterval: 30000, // Check every 30 seconds 31 + }) 32 + 33 + const completedTodos = todos?.filter((todo: any) => todo.done).length || 0 34 + const pendingTodos = todos?.filter((todo: any) => !todo.done).length || 0 35 + const totalTodos = todos?.length || 0 36 + const hasApiKey = !!apiKeyInfo?.hasApiKey 37 + 38 + const activeRemindersCount = activeReminders?.length || 0 39 + const overdueReminders = activeReminders?.filter((reminder: any) => 40 + new Date(reminder.expires_at) < new Date() 41 + ) || [] 42 + 43 + const stats = [ 44 + { 45 + name: 'Total Todos', 46 + value: totalTodos, 47 + icon: CheckSquare, 48 + color: 'text-blue-600', 49 + bgColor: 'bg-blue-100', 50 + }, 51 + { 52 + name: 'Completed', 53 + value: completedTodos, 54 + icon: TrendingUp, 55 + color: 'text-green-600', 56 + bgColor: 'bg-green-100', 57 + }, 58 + { 59 + name: 'Pending', 60 + value: pendingTodos, 61 + icon: Clock, 62 + color: 'text-yellow-600', 63 + bgColor: 'bg-yellow-100', 64 + }, 65 + { 66 + name: 'Active Reminders', 67 + value: activeRemindersCount, 68 + icon: Bell, 69 + color: overdueReminders.length > 0 ? 'text-red-600' : 'text-blue-600', 70 + bgColor: overdueReminders.length > 0 ? 'bg-red-100' : 'bg-blue-100', 71 + }, 72 + { 73 + name: 'API Key', 74 + value: hasApiKey ? 'Configured' : 'Not Set', 75 + icon: Key, 76 + color: hasApiKey ? 'text-green-600' : 'text-red-600', 77 + bgColor: hasApiKey ? 'bg-green-100' : 'bg-red-100', 78 + }, 79 + ] 80 + 81 + const recentTodos = todos?.slice(0, 5) || [] 82 + const recentReminders = reminders?.slice(0, 5) || [] 83 + 84 + useEffect(() => { 85 + if (overdueReminders.length > 0) { 86 + overdueReminders.forEach((reminder: any) => { 87 + const notificationId = `reminder-${reminder.reminder_id}` 88 + if (!notifications.includes(notificationId)) { 89 + toast.error(`Reminder: ${reminder.message}`, { 90 + duration: 10000, 91 + action: { 92 + label: 'Mark Complete', 93 + onClick: () => handleCompleteReminder(reminder.reminder_id) 94 + } 95 + }) 96 + setNotifications(prev => [...prev, notificationId]) 97 + } 98 + }) 99 + } 100 + }, [overdueReminders]) 101 + 102 + const handleCompleteReminder = async (id: string) => { 103 + try { 104 + await remindersAPI.completeReminder(id) 105 + toast.success('Reminder completed!') 106 + setNotifications(prev => prev.filter(notif => notif !== `reminder-${id}`)) 107 + } catch (error) { 108 + toast.error('Failed to complete reminder') 109 + } 110 + } 111 + 112 + const formatDate = (dateString: string) => { 113 + const date = new Date(dateString) 114 + return date.toLocaleString() 115 + } 116 + 117 + const isExpired = (dateString: string) => { 118 + return new Date(dateString) < new Date() 119 + } 120 + 121 + return ( 122 + <div className="space-y-6"> 123 + {/* Header */} 124 + <div> 125 + <h1 className="text-2xl font-bold text-white"> 126 + Welcome back, {user?.username}! 127 + </h1> 128 + <p className="text-gray-400"> 129 + Here's an overview of your todos and settings. 130 + </p> 131 + </div> 132 + 133 + {/* Stats Grid */} 134 + <div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4"> 135 + {stats.map((stat) => { 136 + const Icon = stat.icon 137 + return ( 138 + <div key={stat.name} className="bg-gray-900/50 border border-gray-700 rounded-lg p-5"> 139 + <div className="flex items-center"> 140 + <div className="flex-shrink-0"> 141 + <div className={`p-3 rounded-lg ${stat.bgColor}`}> 142 + <Icon className={`h-6 w-6 ${stat.color}`} /> 143 + </div> 144 + </div> 145 + <div className="ml-5 w-0 flex-1"> 146 + <dl> 147 + <dt className="text-sm font-medium text-gray-400 truncate"> 148 + {stat.name} 149 + </dt> 150 + <dd className="text-lg font-medium text-white"> 151 + {stat.value} 152 + </dd> 153 + </dl> 154 + </div> 155 + </div> 156 + </div> 157 + ) 158 + })} 159 + </div> 160 + 161 + {/* Overdue Reminders Alert */} 162 + {overdueReminders.length > 0 && ( 163 + <div className="bg-red-900/20 border border-red-700 rounded-lg p-4"> 164 + <div className="flex items-center"> 165 + <AlertCircle className="h-5 w-5 text-red-600 mr-2" /> 166 + <h3 className="text-sm font-medium text-red-300"> 167 + You have {overdueReminders.length} overdue reminder{overdueReminders.length > 1 ? 's' : ''} 168 + </h3> 169 + </div> 170 + <div className="mt-2 space-y-1"> 171 + {overdueReminders.slice(0, 3).map((reminder: any) => ( 172 + <div key={reminder.reminder_id} className="flex items-center justify-between"> 173 + <p className="text-sm text-red-200 truncate">{reminder.message}</p> 174 + <button 175 + onClick={() => handleCompleteReminder(reminder.reminder_id)} 176 + className="text-xs bg-red-600 hover:bg-red-700 text-white px-2 py-1 rounded transition-colors" 177 + > 178 + Complete 179 + </button> 180 + </div> 181 + ))} 182 + {overdueReminders.length > 3 && ( 183 + <p className="text-xs text-red-400">And {overdueReminders.length - 3} more...</p> 184 + )} 185 + </div> 186 + </div> 187 + )} 188 + 189 + {/* Recent Activity */} 190 + <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> 191 + {/* Recent Todos */} 192 + <div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6"> 193 + <div className="flex items-center justify-between mb-4"> 194 + <h2 className="text-lg font-medium text-white">Recent Todos</h2> 195 + <a 196 + href="/todos" 197 + className="text-sm text-white hover:text-gray-300" 198 + > 199 + View all 200 + </a> 201 + </div> 202 + {recentTodos.length > 0 ? ( 203 + <div className="space-y-3"> 204 + {recentTodos.map((todo: any) => ( 205 + <div key={todo.id} className="flex items-center space-x-3"> 206 + <div className={`flex-shrink-0 w-2 h-2 rounded-full ${ 207 + todo.done ? 'bg-green-400' : 'bg-yellow-400' 208 + }`} /> 209 + <span className={`text-sm ${ 210 + todo.done ? 'text-gray-500 line-through' : 'text-white' 211 + }`}> 212 + {todo.item} 213 + </span> 214 + </div> 215 + ))} 216 + </div> 217 + ) : ( 218 + <p className="text-gray-400 text-sm">No todos yet. Create your first one!</p> 219 + )} 220 + </div> 221 + 222 + {/* Recent Reminders */} 223 + <div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6"> 224 + <div className="flex items-center justify-between mb-4"> 225 + <h2 className="text-lg font-medium text-white">Recent Reminders</h2> 226 + <a 227 + href="/reminders" 228 + className="text-sm text-white hover:text-gray-300" 229 + > 230 + View all 231 + </a> 232 + </div> 233 + {recentReminders.length > 0 ? ( 234 + <div className="space-y-3"> 235 + {recentReminders.map((reminder: any) => ( 236 + <div key={reminder.reminder_id} className="flex items-start space-x-3"> 237 + <div className={`flex-shrink-0 w-2 h-2 rounded-full mt-2 ${ 238 + reminder.is_completed 239 + ? 'bg-green-400' 240 + : isExpired(reminder.expires_at) 241 + ? 'bg-red-400' 242 + : 'bg-blue-400' 243 + }`} /> 244 + <div className="flex-1 min-w-0"> 245 + <p className={`text-sm ${ 246 + reminder.is_completed ? 'text-gray-500 line-through' : 'text-white' 247 + }`}> 248 + {reminder.message} 249 + </p> 250 + <p className="text-xs text-gray-400 mt-1"> 251 + {formatDate(reminder.expires_at)} 252 + </p> 253 + </div> 254 + </div> 255 + ))} 256 + </div> 257 + ) : ( 258 + <p className="text-gray-400 text-sm">No reminders yet. Create your first one!</p> 259 + )} 260 + </div> 261 + 262 + {/* API Key Status */} 263 + <div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6"> 264 + <div className="flex items-center justify-between mb-4"> 265 + <h2 className="text-lg font-medium text-white">AI Configuration</h2> 266 + <a 267 + href="/api-keys" 268 + className="text-sm text-white hover:text-gray-300" 269 + > 270 + Manage 271 + </a> 272 + </div> 273 + <div className="space-y-3"> 274 + <div className="flex items-center justify-between"> 275 + <span className="text-sm text-gray-400">API Key</span> 276 + <span className={`text-sm font-medium ${ 277 + hasApiKey ? 'text-green-600' : 'text-red-600' 278 + }`}> 279 + {hasApiKey ? 'Configured' : 'Not Set'} 280 + </span> 281 + </div> 282 + {apiKeyInfo?.model && ( 283 + <div className="flex items-center justify-between"> 284 + <span className="text-sm text-gray-400">Model</span> 285 + <span className="text-sm font-medium text-white"> 286 + {apiKeyInfo.model} 287 + </span> 288 + </div> 289 + )} 290 + {apiKeyInfo?.apiUrl && ( 291 + <div className="flex items-center justify-between"> 292 + <span className="text-sm text-gray-400">Endpoint</span> 293 + <span className="text-sm font-medium text-white truncate max-w-32"> 294 + {new URL(apiKeyInfo.apiUrl).hostname} 295 + </span> 296 + </div> 297 + )} 298 + {!hasApiKey && ( 299 + <p className="text-sm text-gray-400"> 300 + Configure your AI API key to use custom models and endpoints. 301 + </p> 302 + )} 303 + </div> 304 + </div> 305 + </div> 306 + 307 + {/* Quick Actions */} 308 + <div className="bg-gray-900/50 border border-gray-700 rounded-lg p-6"> 309 + <h2 className="text-lg font-medium text-white mb-4">Quick Actions</h2> 310 + <div className="flex flex-wrap gap-3"> 311 + <a 312 + href="/todos" 313 + className="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center transition-colors" 314 + > 315 + <CheckSquare className="h-4 w-4 mr-2" /> 316 + Manage Todos 317 + </a> 318 + <a 319 + href="/reminders" 320 + className="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors" 321 + > 322 + <Bell className="h-4 w-4 mr-2" /> 323 + Manage Reminders 324 + </a> 325 + <a 326 + href="/api-keys" 327 + className="bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors" 328 + > 329 + <Key className="h-4 w-4 mr-2" /> 330 + Configure AI 331 + </a> 332 + </div> 333 + </div> 334 + </div> 335 + ) 336 + } 337 + 338 + export default DashboardPage
+156
web/src/pages/LandingPage.tsx
··· 1 + import { Bot, MessageSquare, Cloud, Bell, Shield, Zap } from 'lucide-react'; 2 + import { Link } from 'react-router-dom'; 3 + 4 + const LandingPage = () => { 5 + return ( 6 + <div className="min-h-screen bg-[#0A0A0A] text-white"> 7 + {/* Header */} 8 + <header> 9 + <div className="max-w-6xl mx-auto px-6 py-4"> 10 + <div className="flex justify-between items-center"> 11 + <div className="flex items-center space-x-3"> 12 + <span className="text-xl font-semibold text-white">Aethel</span> 13 + </div> 14 + 15 + <nav className="hidden md:flex items-center space-x-8"> 16 + <a href="#features" className="text-gray-400 hover:text-white transition-colors"> 17 + Features 18 + </a> 19 + <Link 20 + to="/status" 21 + className="text-gray-400 hover:text-white transition-colors" 22 + > 23 + Status 24 + </Link> 25 + </nav> 26 + 27 + <Link 28 + to="/login" 29 + className="bg-white text-black px-4 py-2 rounded-lg font-medium hover:bg-gray-100 transition-colors" 30 + > 31 + Dashboard 32 + </Link> 33 + </div> 34 + </div> 35 + </header> 36 + 37 + {/* Hero Section */} 38 + <section className="py-32 px-6"> 39 + <div className="max-w-4xl mx-auto text-center"> 40 + <h1 className="text-5xl md:text-6xl font-bold mb-6 text-white"> 41 + A useful Discord user bot 42 + <span className="block text-gray-400 mt-2">for your account</span> 43 + </h1> 44 + 45 + <p className="text-xl text-gray-400 mb-12 max-w-2xl mx-auto"> 46 + Enhance your Discord experience with AI chat, weather updates, reminders, and more useful features. 47 + </p> 48 + 49 + <a 50 + href="https://discord.com/api/oauth2/authorize?client_id=YOUR_BOT_CLIENT_ID&permissions=8&scope=bot%20applications.commands" 51 + target="_blank" 52 + rel="noopener noreferrer" 53 + className="inline-flex items-center space-x-3 bg-[#5865F2] text-white px-8 py-4 rounded-lg hover:bg-[#4752C4] transition-colors font-medium" 54 + > 55 + <svg className="h-5 w-5" viewBox="0 0 24 24" fill="currentColor"> 56 + <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/> 57 + </svg> 58 + <span>Add to Discord</span> 59 + </a> 60 + </div> 61 + </section> 62 + 63 + {/* Features Section */} 64 + <section id="features" className="py-24 px-6"> 65 + <div className="max-w-6xl mx-auto"> 66 + <div className="text-center mb-16"> 67 + <h2 className="text-4xl font-bold text-white mb-4">Features</h2> 68 + <p className="text-xl text-gray-400 max-w-2xl mx-auto"> 69 + Everything you need to enhance your Discord experience 70 + </p> 71 + </div> 72 + 73 + <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8"> 74 + <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 75 + <div className="bg-blue-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 76 + <MessageSquare className="h-6 w-6 text-blue-400" /> 77 + </div> 78 + <h3 className="text-xl font-semibold text-white mb-4">AI Chat Assistant</h3> 79 + <p className="text-gray-400 leading-relaxed"> 80 + Get intelligent responses and assistance with our advanced AI chat system. 81 + </p> 82 + </div> 83 + 84 + <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 85 + <div className="bg-green-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 86 + <Cloud className="h-6 w-6 text-green-400" /> 87 + </div> 88 + <h3 className="text-xl font-semibold text-white mb-4">Weather Updates</h3> 89 + <p className="text-gray-400 leading-relaxed"> 90 + Stay informed with real-time weather information for any location. 91 + </p> 92 + </div> 93 + 94 + <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 95 + <div className="bg-yellow-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 96 + <Bell className="h-6 w-6 text-yellow-400" /> 97 + </div> 98 + <h3 className="text-xl font-semibold text-white mb-4">Smart Reminders</h3> 99 + <p className="text-gray-400 leading-relaxed"> 100 + Never miss important events with our intelligent reminder system. 101 + </p> 102 + </div> 103 + 104 + <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 105 + <div className="bg-purple-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 106 + <Shield className="h-6 w-6 text-purple-400" /> 107 + </div> 108 + <h3 className="text-xl font-semibold text-white mb-4">Secure & Private</h3> 109 + <p className="text-gray-400 leading-relaxed"> 110 + Your data is protected with enterprise-grade security measures. 111 + </p> 112 + </div> 113 + 114 + <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 115 + <div className="bg-orange-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 116 + <Zap className="h-6 w-6 text-orange-400" /> 117 + </div> 118 + <h3 className="text-xl font-semibold text-white mb-4">Lightning Fast</h3> 119 + <p className="text-gray-400 leading-relaxed"> 120 + Experience blazing fast response times and seamless performance. 121 + </p> 122 + </div> 123 + 124 + <div className="bg-gray-900/50 backdrop-blur-sm p-8 rounded-xl border border-gray-700/50 hover:border-gray-600/50 transition-all duration-300"> 125 + <div className="bg-indigo-500/10 w-12 h-12 rounded-lg flex items-center justify-center mb-6"> 126 + <Bot className="h-6 w-6 text-indigo-400" /> 127 + </div> 128 + <h3 className="text-xl font-semibold text-white mb-4">Discord Native</h3> 129 + <p className="text-gray-400 leading-relaxed"> 130 + Built specifically for Discord with seamless integration. 131 + </p> 132 + </div> 133 + </div> 134 + </div> 135 + </section> 136 + 137 + {/* Footer */} 138 + <footer className="py-12 px-6"> 139 + <div className="max-w-6xl mx-auto text-center"> 140 + <div className="flex items-center justify-center space-x-3 mb-4"> 141 + <span className="text-lg font-semibold text-white">Aethel</span> 142 + </div> 143 + <p className="text-gray-400 mb-6"> 144 + A useful Discord user bot for your account 145 + </p> 146 + <div className="flex justify-center space-x-8 text-sm text-gray-400"> 147 + <a href="#features" className="hover:text-white transition-colors">Features</a> 148 + <a href="/status" className="hover:text-white transition-colors">Status</a> 149 + </div> 150 + </div> 151 + </footer> 152 + </div> 153 + ) 154 + } 155 + 156 + export default LandingPage
+84
web/src/pages/LoginPage.tsx
··· 1 + import { useEffect } from 'react' 2 + import { useSearchParams } from 'react-router-dom' 3 + import { toast } from 'sonner' 4 + import { useAuthStore } from '../stores/authStore' 5 + 6 + const LoginPage = () => { 7 + const [searchParams] = useSearchParams() 8 + const { login } = useAuthStore() 9 + 10 + useEffect(() => { 11 + const token = searchParams.get('token') 12 + const error = searchParams.get('error') 13 + 14 + if (error) { 15 + toast.error('Authentication failed. Please try again.') 16 + } else if (token) { 17 + const userData = { 18 + id: searchParams.get('user_id') || '', 19 + username: searchParams.get('username') || '', 20 + discriminator: searchParams.get('discriminator') || '', 21 + avatar: searchParams.get('avatar'), 22 + } 23 + 24 + login(token, userData) 25 + toast.success('Successfully logged in!') 26 + } 27 + }, [searchParams, login]) 28 + 29 + const handleDiscordLogin = async () => { 30 + try { 31 + window.location.href = '/api/auth/discord' 32 + } catch (error) { 33 + toast.error('Failed to initiate Discord login') 34 + } 35 + } 36 + 37 + return ( 38 + <div className="min-h-screen flex items-center justify-center bg-[#0A0A0A]"> 39 + <div className="max-w-md w-full space-y-8"> 40 + <div className="text-center"> 41 + <h1 className="text-4xl font-bold text-white mb-2">Aethel Dashboard</h1> 42 + <p className="text-gray-400 text-lg"> 43 + Manage your todos and AI API keys 44 + </p> 45 + </div> 46 + 47 + <div className="card p-8"> 48 + <div className="text-center space-y-6"> 49 + <div> 50 + <h2 className="text-2xl font-bold text-white mb-2"> 51 + Welcome back 52 + </h2> 53 + <p className="text-gray-400"> 54 + Sign in with your Discord account to continue 55 + </p> 56 + </div> 57 + 58 + <button 59 + onClick={handleDiscordLogin} 60 + className="w-full flex items-center justify-center px-4 py-3 border border-transparent rounded-lg shadow-sm text-white bg-discord-blurple hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-discord-blurple transition-colors font-medium" 61 + > 62 + <svg className="w-5 h-5 mr-3" viewBox="0 0 24 24" fill="currentColor"> 63 + <path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515a.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0a12.64 12.64 0 0 0-.617-1.25a.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057a19.9 19.9 0 0 0 5.993 3.03a.078.078 0 0 0 .084-.028a14.09 14.09 0 0 0 1.226-1.994a.076.076 0 0 0-.041-.106a13.107 13.107 0 0 1-1.872-.892a.077.077 0 0 1-.008-.128a10.2 10.2 0 0 0 .372-.292a.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127a12.299 12.299 0 0 1-1.873.892a.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028a19.839 19.839 0 0 0 6.002-3.03a.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.956-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419c0-1.333.955-2.419 2.157-2.419c1.21 0 2.176 1.096 2.157 2.42c0 1.333-.946 2.418-2.157 2.418z"/> 64 + </svg> 65 + Continue with Discord 66 + </button> 67 + 68 + <div className="text-xs text-gray-400 text-center"> 69 + By signing in, you agree to our terms of service and privacy policy. 70 + </div> 71 + </div> 72 + </div> 73 + 74 + <div className="text-center"> 75 + <p className="text-gray-400 text-sm"> 76 + Need help? Contact us on our Discord server 77 + </p> 78 + </div> 79 + </div> 80 + </div> 81 + ) 82 + } 83 + 84 + export default LoginPage
+114
web/src/pages/PrivacyPage.tsx
··· 1 + import { Link } from 'react-router-dom'; 2 + import { ArrowLeft } from 'lucide-react'; 3 + 4 + export default function PrivacyPolicy() { 5 + return ( 6 + <div className="min-h-screen bg-[#0A0A0A] text-white"> 7 + {/* Header */} 8 + <header className="border-b border-gray-800"> 9 + <div className="max-w-4xl mx-auto px-6 py-4"> 10 + <Link 11 + to="/" 12 + className="inline-flex items-center gap-2 text-gray-400 hover:text-white transition-colors" 13 + > 14 + <ArrowLeft className="w-4 h-4" /> 15 + Back to Home 16 + </Link> 17 + </div> 18 + </header> 19 + 20 + {/* Content */} 21 + <main className="max-w-4xl mx-auto px-6 py-12"> 22 + <div className="mb-8"> 23 + <h1 className="text-4xl font-bold text-white mb-2">Privacy Policy</h1> 24 + <p className="text-gray-400">Last Updated: July 21, 2025</p> 25 + </div> 26 + <div className="space-y-8"> 27 + <section> 28 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">1. Information We Collect</h2> 29 + <p className="text-gray-400 leading-relaxed"> 30 + The bot (&quot;the Bot&quot;) collects the following information: 31 + </p> 32 + <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 33 + <li>Discord user IDs for command processing and functionality</li> 34 + <li>Server IDs where the Bot is used</li> 35 + <li>Channel IDs where commands are used</li> 36 + <li>Message content for commands that require it (e.g., reminders, AI chat)</li> 37 + <li>API keys provided by users (encrypted and stored safely on our database until the user says otherwise)</li> 38 + <li>Commands ran and users who ran them</li> 39 + 40 + </ul> 41 + </section> 42 + 43 + <section> 44 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">2. How We Use Your Information</h2> 45 + <p className="text-gray-400 leading-relaxed"> 46 + We use the collected information to provide, maintain, and improve our Bot&apos;s services, including: 47 + </p> 48 + <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 49 + <li>Provide and maintain the Bot&apos;s functionality</li> 50 + <li>Process commands and provide responses</li> 51 + <li>Improve the Bot&apos;s performance and features</li> 52 + <li>Monitor for abuse and prevent violations of our Terms of Service</li> 53 + </ul> 54 + </section> 55 + 56 + <section> 57 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">3. Data Storage</h2> 58 + <p className="text-gray-400 leading-relaxed"> 59 + We take your privacy seriously: 60 + </p> 61 + <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 62 + <li>API keys are securely hashed using industry-standard encryption before being stored in our database</li> 63 + <li>Your custom API keys and model preferences are stored until you choose to remove them using the <code className="bg-gray-800 px-2 py-0.5 rounded text-sm font-mono text-gray-200">/ai use_custom_api:false</code> command</li> 64 + <li>We log all message content (like Wiki searches, reminders, and 8-ball queries) for monitoring purposes.</li> 65 + <li>We do not sell or share your personal information with third parties</li> 66 + <li>You can delete your stored API key and preferences at any time by running <code className="bg-gray-800 px-1 rounded">/ai use_custom_api:false</code></li> 67 + </ul> 68 + </section> 69 + 70 + <section> 71 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">4. Third-Party Services</h2> 72 + <p className="text-gray-400 leading-relaxed"> 73 + Our Bot may contain links to third-party websites or services that are not operated by us. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. 74 + </p> 75 + <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 76 + <li>Discord&apos;s Privacy Policy for user and server data</li> 77 + <li>OpenRouter&apos;s Privacy Policy for AI chat functionality</li> 78 + <li>Weather API providers for weather information</li> 79 + <li>Wikipedia&apos;s Terms of Service for wiki lookups</li> 80 + </ul> 81 + </section> 82 + 83 + <section> 84 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">5. Data Security</h2> 85 + <p className="text-gray-400 leading-relaxed"> 86 + We implement reasonable security measures to protect your information, but no method of transmission over the internet is 100% secure. 87 + </p> 88 + </section> 89 + 90 + <section> 91 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">6. Children&apos;s Privacy</h2> 92 + <p className="text-gray-400 leading-relaxed"> 93 + Our Bot is not intended for use by children under the age of 13. We do not knowingly collect personally identifiable information from children under 13. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us. 94 + </p> 95 + </section> 96 + 97 + <section> 98 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">7. Changes to This Policy</h2> 99 + <p className="text-gray-400 leading-relaxed"> 100 + We may update our Privacy Policy from time to time. We will notify you of any changes by posting the new Privacy Policy on this page. 101 + </p> 102 + </section> 103 + 104 + <section> 105 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">8. Contact Us</h2> 106 + <p className="text-gray-400 leading-relaxed"> 107 + If you have any questions about this Privacy Policy, please contact us at <a href="mailto:scan@scanash.com" className="text-blue-400 hover:text-blue-300 hover:underline font-medium">scan@scanash.com</a>. 108 + </p> 109 + </section> 110 + </div> 111 + </main> 112 + </div> 113 + ); 114 + }
+262
web/src/pages/RemindersPage.tsx
··· 1 + import React, { useState, useEffect } from 'react' 2 + import { Plus, Clock, Check, Trash2, Bell } from 'lucide-react' 3 + import { remindersAPI } from '../lib/api' 4 + import { toast } from 'sonner' 5 + 6 + interface Reminder { 7 + reminder_id: string 8 + message: string 9 + expires_at: string 10 + is_completed: boolean 11 + created_at: string 12 + } 13 + 14 + const RemindersPage: React.FC = () => { 15 + const [reminders, setReminders] = useState<Reminder[]>([]) 16 + const [loading, setLoading] = useState(true) 17 + const [showCreateForm, setShowCreateForm] = useState(false) 18 + const [newReminder, setNewReminder] = useState({ 19 + message: '', 20 + expires_at: '' 21 + }) 22 + 23 + useEffect(() => { 24 + fetchReminders() 25 + }, []) 26 + 27 + const fetchReminders = async () => { 28 + try { 29 + const response = await remindersAPI.getReminders() 30 + setReminders(response.data.reminders || []) 31 + } catch (error) { 32 + toast.error('Failed to fetch reminders') 33 + } finally { 34 + setLoading(false) 35 + } 36 + } 37 + 38 + const handleCreateReminder = async (e: React.FormEvent) => { 39 + e.preventDefault() 40 + 41 + if (!newReminder.message.trim() || !newReminder.expires_at) { 42 + toast.error('Please fill in all fields') 43 + return 44 + } 45 + 46 + try { 47 + await remindersAPI.createReminder({ 48 + message: newReminder.message.trim(), 49 + expires_at: newReminder.expires_at 50 + }) 51 + 52 + toast.success('Reminder created successfully!') 53 + setNewReminder({ message: '', expires_at: '' }) 54 + setShowCreateForm(false) 55 + fetchReminders() 56 + } catch (error) { 57 + toast.error('Failed to create reminder') 58 + } 59 + } 60 + 61 + const handleCompleteReminder = async (id: string) => { 62 + try { 63 + await remindersAPI.completeReminder(id) 64 + toast.success('Reminder completed!') 65 + fetchReminders() 66 + } catch (error) { 67 + toast.error('Failed to complete reminder') 68 + } 69 + } 70 + 71 + const handleClearCompleted = async () => { 72 + try { 73 + const completedReminders = reminders.filter(r => r.is_completed) 74 + if (completedReminders.length === 0) { 75 + toast.info('No completed reminders to clear') 76 + return 77 + } 78 + 79 + await remindersAPI.clearCompletedReminders() 80 + toast.success('Completed reminders cleared!') 81 + fetchReminders() 82 + } catch (error) { 83 + toast.error('Failed to clear completed reminders') 84 + } 85 + } 86 + 87 + const formatDate = (dateString: string) => { 88 + const date = new Date(dateString) 89 + return date.toLocaleString() 90 + } 91 + 92 + const isExpired = (dateString: string) => { 93 + return new Date(dateString) < new Date() 94 + } 95 + 96 + const getMinDateTime = () => { 97 + const now = new Date() 98 + const minTime = new Date(now.getTime() + 60000) 99 + const year = minTime.getFullYear() 100 + const month = String(minTime.getMonth() + 1).padStart(2, '0') 101 + const day = String(minTime.getDate()).padStart(2, '0') 102 + const hours = String(minTime.getHours()).padStart(2, '0') 103 + const minutes = String(minTime.getMinutes()).padStart(2, '0') 104 + 105 + return `${year}-${month}-${day}T${hours}:${minutes}` 106 + } 107 + 108 + if (loading) { 109 + return ( 110 + <div className="flex items-center justify-center h-64"> 111 + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-discord-blurple"></div> 112 + </div> 113 + ) 114 + } 115 + 116 + return ( 117 + <div className="space-y-8"> 118 + <div className="flex items-center justify-between"> 119 + <div> 120 + <h1 className="text-3xl font-bold text-white">Reminders</h1> 121 + <p className="text-gray-400 mt-2">Manage your personal reminders and notifications</p> 122 + </div> 123 + <div className="flex items-center gap-3"> 124 + {reminders.some(r => r.is_completed) && ( 125 + <button 126 + onClick={handleClearCompleted} 127 + className="btn btn-danger" 128 + > 129 + <Trash2 className="h-4 w-4 mr-2" /> 130 + Clear Completed 131 + </button> 132 + )} 133 + <button 134 + onClick={() => setShowCreateForm(true)} 135 + className="btn btn-primary" 136 + > 137 + <Plus className="h-4 w-4 mr-2" /> 138 + New Reminder 139 + </button> 140 + </div> 141 + </div> 142 + 143 + {/* Create Reminder Form */} 144 + {showCreateForm && ( 145 + <div className="bg-gray-900/50 p-8 rounded-lg border border-gray-700"> 146 + <h2 className="text-xl font-semibold mb-6 text-white">Create New Reminder</h2> 147 + <form onSubmit={handleCreateReminder} className="space-y-6"> 148 + <div> 149 + <label htmlFor="message" className="block text-sm font-medium text-gray-400 mb-2"> 150 + Reminder Message 151 + </label> 152 + <textarea 153 + id="message" 154 + value={newReminder.message} 155 + onChange={(e) => setNewReminder({ ...newReminder, message: e.target.value })} 156 + placeholder="What would you like to be reminded about?" 157 + className="input" 158 + rows={3} 159 + required 160 + /> 161 + </div> 162 + <div> 163 + <label htmlFor="expires_at" className="block text-sm font-medium text-gray-400 mb-2"> 164 + Remind me at 165 + </label> 166 + <input 167 + type="datetime-local" 168 + id="expires_at" 169 + value={newReminder.expires_at} 170 + onChange={(e) => setNewReminder({ ...newReminder, expires_at: e.target.value })} 171 + min={getMinDateTime()} 172 + className="input" 173 + required 174 + /> 175 + </div> 176 + <div className="flex gap-3"> 177 + <button 178 + type="submit" 179 + className="btn btn-primary" 180 + > 181 + Create Reminder 182 + </button> 183 + <button 184 + type="button" 185 + onClick={() => { 186 + setShowCreateForm(false) 187 + setNewReminder({ message: '', expires_at: '' }) 188 + }} 189 + className="btn btn-secondary" 190 + > 191 + Cancel 192 + </button> 193 + </div> 194 + </form> 195 + </div> 196 + )} 197 + 198 + {/* Reminders List */} 199 + <div className="space-y-4"> 200 + {reminders.length === 0 ? ( 201 + <div className="text-center py-12"> 202 + <Bell className="h-12 w-12 text-gray-400 mx-auto mb-4" /> 203 + <h3 className="text-lg font-medium text-white mb-2">No reminders yet</h3> 204 + <p className="text-gray-400 mb-4">Create your first reminder to get started!</p> 205 + <button 206 + onClick={() => setShowCreateForm(true)} 207 + className="btn btn-primary" 208 + > 209 + Create Reminder 210 + </button> 211 + </div> 212 + ) : ( 213 + reminders.map((reminder) => ( 214 + <div 215 + key={reminder.reminder_id} 216 + className={`card p-6 border-l-4 ${ 217 + reminder.is_completed 218 + ? 'border-green-500 bg-green-900/20' 219 + : isExpired(reminder.expires_at) 220 + ? 'border-red-500 bg-red-900/20' 221 + : 'border-blue-500' 222 + }`} 223 + > 224 + <div className="flex items-start justify-between"> 225 + <div className="flex-1"> 226 + <p className={`text-lg ${reminder.is_completed ? 'line-through text-gray-500' : 'text-white'}`}> 227 + {reminder.message} 228 + </p> 229 + <div className="flex items-center gap-4 mt-3 text-sm text-gray-400"> 230 + <div className="flex items-center gap-1"> 231 + <Clock className="h-4 w-4" /> 232 + <span>Remind at: {formatDate(reminder.expires_at)}</span> 233 + </div> 234 + {isExpired(reminder.expires_at) && !reminder.is_completed && ( 235 + <span className="text-red-600 font-medium">Overdue</span> 236 + )} 237 + {reminder.is_completed && ( 238 + <span className="text-green-600 font-medium">Completed</span> 239 + )} 240 + </div> 241 + </div> 242 + <div className="flex items-center gap-2 ml-4"> 243 + {!reminder.is_completed && ( 244 + <button 245 + onClick={() => handleCompleteReminder(reminder.reminder_id)} 246 + className="btn btn-success p-2" 247 + title="Mark as completed" 248 + > 249 + <Check className="h-4 w-4" /> 250 + </button> 251 + )} 252 + </div> 253 + </div> 254 + </div> 255 + )) 256 + )} 257 + </div> 258 + </div> 259 + ) 260 + } 261 + 262 + export default RemindersPage
+322
web/src/pages/StatusPage.tsx
··· 1 + import { useEffect, useState } from 'react' 2 + import { Link } from 'react-router-dom' 3 + import { 4 + Activity, 5 + Clock, 6 + GitBranch, 7 + Home, 8 + RefreshCw, 9 + Server, 10 + Wifi, 11 + WifiOff, 12 + AlertCircle 13 + } from 'lucide-react' 14 + 15 + interface StatusData { 16 + status: string 17 + uptime: { 18 + days: number 19 + hours: number 20 + minutes: number 21 + seconds: number 22 + } 23 + botStatus: string 24 + ping: number 25 + lastReady: string | null 26 + commitHash: string 27 + } 28 + 29 + const StatusPage = () => { 30 + const [statusData, setStatusData] = useState<StatusData | null>(null) 31 + const [loading, setLoading] = useState(true) 32 + const [error, setError] = useState<string | null>(null) 33 + const [lastUpdated, setLastUpdated] = useState<Date>(new Date()) 34 + 35 + const fetchStatus = async () => { 36 + try { 37 + setError(null) 38 + const response = await fetch('http://localhost:2020/status', { 39 + headers: { 40 + 'X-API-Key': 'XIoypvTfaDxWLTFFcHu9ta0aJpvRPVIGADxSMNCNJ50QYtIpSIUsi1WKLglQ7TTRYX6mWgYq15i4NqPl92l0Lzepsrju2fXV1aZpNdDTtIu5mFMvcLhouhmxwb7R93' 41 + } 42 + }) 43 + 44 + if (!response.ok) { 45 + throw new Error(`HTTP ${response.status}: ${response.statusText}`) 46 + } 47 + 48 + const data = await response.json() 49 + setStatusData(data) 50 + setLastUpdated(new Date()) 51 + } catch (err) { 52 + setError(err instanceof Error ? err.message : 'Failed to fetch status') 53 + } finally { 54 + setLoading(false) 55 + } 56 + } 57 + 58 + useEffect(() => { 59 + fetchStatus() 60 + 61 + const interval = setInterval(fetchStatus, 30000) 62 + return () => clearInterval(interval) 63 + }, []) 64 + 65 + const formatUptime = (uptime: StatusData['uptime']) => { 66 + const parts = [] 67 + if (uptime.days > 0) parts.push(`${uptime.days}d`) 68 + if (uptime.hours > 0) parts.push(`${uptime.hours}h`) 69 + if (uptime.minutes > 0) parts.push(`${uptime.minutes}m`) 70 + if (uptime.seconds > 0 || parts.length === 0) parts.push(`${uptime.seconds}s`) 71 + return parts.join(' ') 72 + } 73 + 74 + const getStatusColor = (status: string) => { 75 + switch (status.toLowerCase()) { 76 + case 'online': 77 + case 'connected': 78 + return 'text-green-400 bg-green-500/20' 79 + case 'offline': 80 + case 'disconnected': 81 + return 'text-red-400 bg-red-500/20' 82 + default: 83 + return 'text-yellow-400 bg-yellow-500/20' 84 + } 85 + } 86 + 87 + const getPingColor = (ping: number) => { 88 + if (ping < 100) return 'text-green-400' 89 + if (ping < 300) return 'text-yellow-400' 90 + return 'text-red-400' 91 + } 92 + 93 + return ( 94 + <div className="min-h-screen bg-[#0A0A0A] text-white"> 95 + {/* Header */} 96 + <header className="border-b border-gray-800"> 97 + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> 98 + <div className="flex justify-between items-center py-6"> 99 + <div className="flex items-center space-x-4"> 100 + <Link to="/" className="flex items-center space-x-2 text-gray-400 hover:text-white transition-colors"> 101 + <Home className="h-5 w-5" /> 102 + <span>Back to Home</span> 103 + </Link> 104 + </div> 105 + <div className="flex items-center space-x-3"> 106 + <span className="text-2xl font-bold text-white">Aethel Status</span> 107 + </div> 108 + <button 109 + onClick={fetchStatus} 110 + disabled={loading} 111 + className="flex items-center space-x-2 px-4 py-2 bg-white text-black rounded-lg hover:bg-gray-100 disabled:opacity-50 disabled:cursor-not-allowed transition-colors font-medium" 112 + > 113 + <RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} /> 114 + <span>Refresh</span> 115 + </button> 116 + </div> 117 + </div> 118 + </header> 119 + 120 + <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> 121 + {error && ( 122 + <div className="mb-8 bg-red-900/20 border border-red-800 rounded-lg p-4"> 123 + <div className="flex items-center space-x-2"> 124 + <AlertCircle className="h-5 w-5 text-red-400" /> 125 + <span className="text-red-300 font-medium">Error loading status</span> 126 + </div> 127 + <p className="text-red-400 mt-2">{error}</p> 128 + </div> 129 + )} 130 + 131 + {loading && !statusData ? ( 132 + <div className="flex items-center justify-center py-12"> 133 + <div className="flex items-center space-x-3"> 134 + <RefreshCw className="h-6 w-6 animate-spin text-white" /> 135 + <span className="text-lg text-gray-400">Loading status...</span> 136 + </div> 137 + </div> 138 + ) : statusData ? ( 139 + <div className="space-y-8"> 140 + {/* Overall Status */} 141 + <div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-8"> 142 + <div className="flex items-center justify-between mb-6"> 143 + <h2 className="text-3xl font-bold text-white">System Status</h2> 144 + <div className="text-sm text-gray-400"> 145 + Last updated: {lastUpdated.toLocaleTimeString()} 146 + </div> 147 + </div> 148 + 149 + <div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6"> 150 + <div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30"> 151 + <div className="flex items-center space-x-4"> 152 + <div className={`p-3 rounded-lg ${getStatusColor(statusData.status)}`}> 153 + <Server className="h-6 w-6" /> 154 + </div> 155 + <div> 156 + <p className="text-sm text-gray-400 mb-1">Server Status</p> 157 + <p className="font-semibold text-white text-lg capitalize">{statusData.status}</p> 158 + </div> 159 + </div> 160 + </div> 161 + 162 + <div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30"> 163 + <div className="flex items-center space-x-4"> 164 + <div className={`p-3 rounded-lg ${getStatusColor(statusData.botStatus)}`}> 165 + {statusData.botStatus === 'connected' ? ( 166 + <Wifi className="h-6 w-6" /> 167 + ) : ( 168 + <WifiOff className="h-6 w-6" /> 169 + )} 170 + </div> 171 + <div> 172 + <p className="text-sm text-gray-400 mb-1">Bot Status</p> 173 + <p className="font-semibold text-white text-lg capitalize">{statusData.botStatus}</p> 174 + </div> 175 + </div> 176 + </div> 177 + 178 + <div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30"> 179 + <div className="flex items-center space-x-4"> 180 + <div className="p-3 rounded-lg bg-blue-500/20 text-blue-400"> 181 + <Activity className="h-6 w-6" /> 182 + </div> 183 + <div> 184 + <p className="text-sm text-gray-400 mb-1">Ping</p> 185 + <p className={`font-semibold text-lg ${getPingColor(statusData.ping)}`}> 186 + {statusData.ping}ms 187 + </p> 188 + </div> 189 + </div> 190 + </div> 191 + 192 + <div className="bg-gray-800/50 rounded-xl p-6 border border-gray-700/30"> 193 + <div className="flex items-center space-x-4"> 194 + <div className="p-3 rounded-lg bg-purple-500/20 text-purple-400"> 195 + <Clock className="h-6 w-6" /> 196 + </div> 197 + <div> 198 + <p className="text-sm text-gray-400 mb-1">Uptime</p> 199 + <p className="font-semibold text-white text-lg"> 200 + {formatUptime(statusData.uptime)} 201 + </p> 202 + </div> 203 + </div> 204 + </div> 205 + </div> 206 + </div> 207 + 208 + {/* Detailed Information */} 209 + <div className="grid md:grid-cols-2 gap-6"> 210 + <div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6"> 211 + <h3 className="text-xl font-semibold text-white mb-6 flex items-center space-x-2"> 212 + <Clock className="h-6 w-6 text-purple-400" /> 213 + <span>Uptime Details</span> 214 + </h3> 215 + <div className="space-y-4"> 216 + <div className="flex justify-between items-center"> 217 + <span className="text-gray-400">Days:</span> 218 + <span className="font-semibold text-white text-lg">{statusData.uptime.days}</span> 219 + </div> 220 + <div className="flex justify-between items-center"> 221 + <span className="text-gray-400">Hours:</span> 222 + <span className="font-semibold text-white text-lg">{statusData.uptime.hours}</span> 223 + </div> 224 + <div className="flex justify-between items-center"> 225 + <span className="text-gray-400">Minutes:</span> 226 + <span className="font-semibold text-white text-lg">{statusData.uptime.minutes}</span> 227 + </div> 228 + <div className="flex justify-between items-center"> 229 + <span className="text-gray-400">Seconds:</span> 230 + <span className="font-semibold text-white text-lg">{statusData.uptime.seconds}</span> 231 + </div> 232 + </div> 233 + </div> 234 + 235 + <div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6"> 236 + <h3 className="text-xl font-semibold text-white mb-6 flex items-center space-x-2"> 237 + <GitBranch className="h-6 w-6 text-blue-400" /> 238 + <span>System Information</span> 239 + </h3> 240 + <div className="space-y-4"> 241 + <div className="flex justify-between items-center"> 242 + <span className="text-gray-400">Commit Hash:</span> 243 + <span className="font-mono text-sm bg-gray-800 text-gray-300 px-3 py-1 rounded-lg"> 244 + {statusData.commitHash || 'Unknown'} 245 + </span> 246 + </div> 247 + <div className="flex justify-between items-center"> 248 + <span className="text-gray-400">Last Ready:</span> 249 + <span className="font-medium text-white"> 250 + {statusData.lastReady 251 + ? new Date(statusData.lastReady).toLocaleString() 252 + : 'Never' 253 + } 254 + </span> 255 + </div> 256 + <div className="flex justify-between items-center"> 257 + <span className="text-gray-400">Response Time:</span> 258 + <span className={`font-semibold text-lg ${getPingColor(statusData.ping)}`}> 259 + {statusData.ping}ms 260 + </span> 261 + </div> 262 + </div> 263 + </div> 264 + </div> 265 + 266 + {/* Status Indicators */} 267 + <div className="bg-gray-900/50 backdrop-blur-sm rounded-xl border border-gray-700/50 p-6"> 268 + <h3 className="text-xl font-semibold text-white mb-6">Service Health</h3> 269 + <div className="grid md:grid-cols-3 gap-4"> 270 + <div className="flex items-center space-x-3 p-4 bg-gray-800/50 rounded-lg border border-gray-700/30"> 271 + <div className={`w-4 h-4 rounded-full ${ 272 + statusData.status === 'online' ? 'bg-green-500' : 'bg-red-500' 273 + }`}></div> 274 + <span className="text-gray-300 font-medium">API Server</span> 275 + <span className={`ml-auto px-3 py-1 text-xs rounded-full font-medium ${ 276 + statusData.status === 'online' 277 + ? 'bg-green-500/20 text-green-400' 278 + : 'bg-red-500/20 text-red-400' 279 + }`}> 280 + {statusData.status} 281 + </span> 282 + </div> 283 + 284 + <div className="flex items-center space-x-3 p-4 bg-gray-800/50 rounded-lg border border-gray-700/30"> 285 + <div className={`w-4 h-4 rounded-full ${ 286 + statusData.botStatus === 'connected' ? 'bg-green-500' : 'bg-red-500' 287 + }`}></div> 288 + <span className="text-gray-300 font-medium">Discord Bot</span> 289 + <span className={`ml-auto px-3 py-1 text-xs rounded-full font-medium ${ 290 + statusData.botStatus === 'connected' 291 + ? 'bg-green-500/20 text-green-400' 292 + : 'bg-red-500/20 text-red-400' 293 + }`}> 294 + {statusData.botStatus} 295 + </span> 296 + </div> 297 + 298 + <div className="flex items-center space-x-3 p-4 bg-gray-800/50 rounded-lg border border-gray-700/30"> 299 + <div className={`w-4 h-4 rounded-full ${ 300 + statusData.ping < 300 ? 'bg-green-500' : 'bg-yellow-500' 301 + }`}></div> 302 + <span className="text-gray-300 font-medium">Network</span> 303 + <span className={`ml-auto px-3 py-1 text-xs rounded-full font-medium ${ 304 + statusData.ping < 100 305 + ? 'bg-green-500/20 text-green-400' 306 + : statusData.ping < 300 307 + ? 'bg-yellow-500/20 text-yellow-400' 308 + : 'bg-red-500/20 text-red-400' 309 + }`}> 310 + {statusData.ping}ms 311 + </span> 312 + </div> 313 + </div> 314 + </div> 315 + </div> 316 + ) : null} 317 + </div> 318 + </div> 319 + ) 320 + } 321 + 322 + export default StatusPage
+99
web/src/pages/TermsPage.tsx
··· 1 + import { Link } from 'react-router-dom'; 2 + import { ArrowLeft } from 'lucide-react'; 3 + 4 + export default function TermsOfService() { 5 + return ( 6 + <div className="min-h-screen bg-[#0A0A0A] text-white"> 7 + {/* Header */} 8 + <header className="border-b border-gray-800"> 9 + <div className="max-w-4xl mx-auto px-6 py-4"> 10 + <Link 11 + to="/" 12 + className="inline-flex items-center gap-2 text-gray-400 hover:text-white transition-colors" 13 + > 14 + <ArrowLeft className="w-4 h-4" /> 15 + Back to Home 16 + </Link> 17 + </div> 18 + </header> 19 + 20 + {/* Content */} 21 + <main className="max-w-4xl mx-auto px-6 py-12"> 22 + <div className="mb-8"> 23 + <h1 className="text-4xl font-bold text-white mb-2">Terms of Service</h1> 24 + <p className="text-gray-400">Last Updated: June 16, 2025</p> 25 + </div> 26 + <div className="space-y-8"> 27 + <section> 28 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">1. Acceptance of Terms</h2> 29 + <p className="text-gray-400 leading-relaxed"> 30 + By using the Bot, you agree to be bound by these Terms of Service. If you do not agree to these terms, please do not use the Bot. 31 + </p> 32 + </section> 33 + 34 + <section> 35 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">2. Description of Service</h2> 36 + <p className="text-gray-400 leading-relaxed mb-4"> 37 + The Bot provides various Discord utilities including but not limited to: reminders, random cat and dog images, weather information, wiki lookups, and fun commands. You agree to use the Bot in accordance with Discord&apos;s Terms of Service and Community Guidelines. 38 + </p> 39 + <ul className="list-disc pl-6 space-y-3 text-gray-400"> 40 + <li>Reminder system</li> 41 + <li>Random cat and dog images</li> 42 + <li>Weather information</li> 43 + <li>Wiki lookups</li> 44 + <li>And other Discord utilities</li> 45 + </ul> 46 + </section> 47 + 48 + <section> 49 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">3. User Responsibilities</h2> 50 + <p className="text-gray-400 leading-relaxed mb-4"> 51 + When using the Bot, you agree not to: 52 + </p> 53 + <ul className="list-disc pl-6 space-y-3 text-gray-400"> 54 + <li>Use the Bot for any illegal or unauthorized purpose</li> 55 + <li>Violate any laws in your jurisdiction</li> 56 + <li>Attempt to disrupt or interfere with the Bot&apos;s operation</li> 57 + <li>Spam or harass others</li> 58 + <li>Attempt to reverse engineer or modify the Bot</li> 59 + </ul> 60 + </section> 61 + 62 + <section> 63 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">4. API Usage</h2> 64 + <p className="text-gray-400 leading-relaxed"> 65 + The Bot may use third-party APIs and services (&quot;Third-Party Services&quot;). Your use of these services is subject to their respective terms and privacy policies. 66 + </p> 67 + <ul className="list-disc pl-6 space-y-3 text-gray-400 mt-2"> 68 + <li>You are responsible for the security of your API keys</li> 69 + <li>We do not store your API keys permanently - they are only kept in memory during your active session</li> 70 + <li>You must comply with the terms of service of any third-party APIs you use with the Bot</li> 71 + <li>We are not responsible for any charges or fees you may incur from third-party API usage</li> 72 + </ul> 73 + </section> 74 + 75 + <section> 76 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">5. Limitation of Liability</h2> 77 + <p className="text-gray-400 leading-relaxed"> 78 + The Bot is provided &quot;as is&quot; without any warranties. We are not responsible for any direct, indirect, incidental, or consequential damages resulting from the use of the Bot. 79 + </p> 80 + </section> 81 + 82 + <section> 83 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">6. Changes to Terms</h2> 84 + <p className="text-gray-400 leading-relaxed"> 85 + We reserve the right to modify these terms at any time. Continued use of the Bot after changes constitutes acceptance of the new terms. 86 + </p> 87 + </section> 88 + 89 + <section> 90 + <h2 className="text-2xl font-bold text-white mb-4 pt-8 border-t border-gray-800 first:border-t-0 first:pt-0">7. Contact</h2> 91 + <p className="text-gray-400 leading-relaxed"> 92 + If you have any questions about these Terms of Service, please contact us at <a href="mailto:scan@scanash.com" className="text-blue-400 hover:text-blue-300 hover:underline font-medium">scan@scanash.com</a>. 93 + </p> 94 + </section> 95 + </div> 96 + </main> 97 + </div> 98 + ); 99 + }
+268
web/src/pages/TodosPage.tsx
··· 1 + import { useState } from 'react' 2 + import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' 3 + import { Plus, Check, Trash2, AlertCircle } from 'lucide-react' 4 + import { toast } from 'sonner' 5 + import { todosAPI } from '../lib/api' 6 + 7 + interface Todo { 8 + id: number 9 + item: string 10 + done: boolean 11 + created_at: string 12 + completed_at?: string 13 + } 14 + 15 + const TodosPage = () => { 16 + const [newTodo, setNewTodo] = useState('') 17 + const [showClearConfirm, setShowClearConfirm] = useState(false) 18 + const queryClient = useQueryClient() 19 + 20 + const { data: todos, isLoading } = useQuery({ 21 + queryKey: ['todos'], 22 + queryFn: () => todosAPI.getTodos().then(res => res.data), 23 + }) 24 + 25 + const addTodoMutation = useMutation({ 26 + mutationFn: (item: string) => todosAPI.createTodo({ item }), 27 + onSuccess: () => { 28 + queryClient.invalidateQueries({ queryKey: ['todos'] }) 29 + setNewTodo('') 30 + toast.success('Todo added successfully!') 31 + }, 32 + onError: () => { 33 + toast.error('Failed to add todo') 34 + }, 35 + }) 36 + 37 + const updateTodoMutation = useMutation({ 38 + mutationFn: ({ id, done }: { id: number; done: boolean }) => 39 + todosAPI.updateTodo(id, { done }), 40 + onSuccess: () => { 41 + queryClient.invalidateQueries({ queryKey: ['todos'] }) 42 + }, 43 + onError: () => { 44 + toast.error('Failed to update todo') 45 + }, 46 + }) 47 + 48 + const deleteTodoMutation = useMutation({ 49 + mutationFn: (id: number) => todosAPI.deleteTodo(id), 50 + onSuccess: () => { 51 + queryClient.invalidateQueries({ queryKey: ['todos'] }) 52 + toast.success('Todo deleted successfully!') 53 + }, 54 + onError: () => { 55 + toast.error('Failed to delete todo') 56 + }, 57 + }) 58 + 59 + const clearAllMutation = useMutation({ 60 + mutationFn: () => todosAPI.clearTodos(), 61 + onSuccess: () => { 62 + queryClient.invalidateQueries({ queryKey: ['todos'] }) 63 + setShowClearConfirm(false) 64 + toast.success('All todos cleared successfully!') 65 + }, 66 + onError: () => { 67 + toast.error('Failed to clear todos') 68 + }, 69 + }) 70 + 71 + const handleAddTodo = (e: React.FormEvent) => { 72 + e.preventDefault() 73 + if (newTodo.trim()) { 74 + addTodoMutation.mutate(newTodo.trim()) 75 + } 76 + } 77 + 78 + const handleToggleTodo = (id: number, done: boolean) => { 79 + updateTodoMutation.mutate({ id, done: !done }) 80 + } 81 + 82 + const handleDeleteTodo = (id: number) => { 83 + deleteTodoMutation.mutate(id) 84 + } 85 + 86 + const handleClearAll = () => { 87 + clearAllMutation.mutate() 88 + } 89 + 90 + const completedTodos = todos?.filter((todo: Todo) => todo.done) || [] 91 + const pendingTodos = todos?.filter((todo: Todo) => !todo.done) || [] 92 + 93 + if (isLoading) { 94 + return ( 95 + <div className="flex items-center justify-center h-64"> 96 + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-discord-blurple"></div> 97 + </div> 98 + ) 99 + } 100 + 101 + return ( 102 + <div className="space-y-6"> 103 + {/* Header */} 104 + <div className="flex items-center justify-between"> 105 + <div> 106 + <h1 className="text-2xl font-bold text-white">Todos</h1> 107 + <p className="text-gray-400"> 108 + Manage your todo list. {todos?.length || 0} total, {completedTodos.length} completed 109 + </p> 110 + </div> 111 + {todos && todos.length > 0 && ( 112 + <button 113 + onClick={() => setShowClearConfirm(true)} 114 + className="btn btn-danger" 115 + disabled={clearAllMutation.isPending} 116 + > 117 + <Trash2 className="h-4 w-4 mr-2" /> 118 + Clear All 119 + </button> 120 + )} 121 + </div> 122 + 123 + {/* Add Todo Form */} 124 + <div className="card p-6"> 125 + <form onSubmit={handleAddTodo} className="flex gap-3"> 126 + <input 127 + type="text" 128 + value={newTodo} 129 + onChange={(e) => setNewTodo(e.target.value)} 130 + placeholder="Add a new todo..." 131 + className="input flex-1" 132 + disabled={addTodoMutation.isPending} 133 + /> 134 + <button 135 + type="submit" 136 + disabled={!newTodo.trim() || addTodoMutation.isPending} 137 + className="btn btn-primary" 138 + > 139 + <Plus className="h-4 w-4 mr-2" /> 140 + Add Todo 141 + </button> 142 + </form> 143 + </div> 144 + 145 + {/* Todos List */} 146 + {todos && todos.length > 0 ? ( 147 + <div className="space-y-4"> 148 + {/* Pending Todos */} 149 + {pendingTodos.length > 0 && ( 150 + <div className="bg-gray-900/50 p-6 rounded-lg border border-gray-700"> 151 + <h2 className="text-lg font-medium text-white mb-4"> 152 + Pending ({pendingTodos.length}) 153 + </h2> 154 + <div className="space-y-3"> 155 + {pendingTodos.map((todo: Todo) => ( 156 + <div 157 + key={todo.id} 158 + className="flex items-center justify-between p-3 bg-gray-800/30 rounded-lg" 159 + > 160 + <div className="flex items-center space-x-3"> 161 + <button 162 + onClick={() => handleToggleTodo(todo.id, todo.done)} 163 + className="flex-shrink-0 w-5 h-5 border-2 border-gray-300 rounded hover:border-green-500 transition-colors" 164 + disabled={updateTodoMutation.isPending} 165 + > 166 + {updateTodoMutation.isPending ? ( 167 + <div className="w-full h-full animate-spin rounded-full border-b border-gray-400"></div> 168 + ) : null} 169 + </button> 170 + <span className="text-white">{todo.item}</span> 171 + </div> 172 + <button 173 + onClick={() => handleDeleteTodo(todo.id)} 174 + className="text-red-600 hover:text-red-800 p-1" 175 + disabled={deleteTodoMutation.isPending} 176 + > 177 + <Trash2 className="h-4 w-4" /> 178 + </button> 179 + </div> 180 + ))} 181 + </div> 182 + </div> 183 + )} 184 + 185 + {/* Completed Todos */} 186 + {completedTodos.length > 0 && ( 187 + <div className="card p-6"> 188 + <h2 className="text-lg font-medium text-white mb-4"> 189 + Completed ({completedTodos.length}) 190 + </h2> 191 + <div className="space-y-3"> 192 + {completedTodos.map((todo: Todo) => ( 193 + <div 194 + key={todo.id} 195 + className="flex items-center justify-between p-3 bg-green-900/20 rounded-lg border border-green-700" 196 + > 197 + <div className="flex items-center space-x-3"> 198 + <button 199 + onClick={() => handleToggleTodo(todo.id, todo.done)} 200 + className="flex-shrink-0 w-5 h-5 bg-green-500 border-2 border-green-500 rounded flex items-center justify-center hover:bg-green-600 transition-colors" 201 + disabled={updateTodoMutation.isPending} 202 + > 203 + <Check className="h-3 w-3 text-white" /> 204 + </button> 205 + <span className="text-gray-400 line-through">{todo.item}</span> 206 + </div> 207 + <button 208 + onClick={() => handleDeleteTodo(todo.id)} 209 + className="text-red-600 hover:text-red-800 p-1" 210 + disabled={deleteTodoMutation.isPending} 211 + > 212 + <Trash2 className="h-4 w-4" /> 213 + </button> 214 + </div> 215 + ))} 216 + </div> 217 + </div> 218 + )} 219 + </div> 220 + ) : ( 221 + <div className="card p-12 text-center"> 222 + <AlertCircle className="h-12 w-12 text-gray-400 mx-auto mb-4" /> 223 + <h3 className="text-lg font-medium text-white mb-2"> 224 + No todos yet 225 + </h3> 226 + <p className="text-gray-400"> 227 + Create your first todo to get started! 228 + </p> 229 + </div> 230 + )} 231 + 232 + {/* Clear All Confirmation Modal */} 233 + {showClearConfirm && ( 234 + <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"> 235 + <div className="bg-gray-900 rounded-lg p-6 max-w-md w-full mx-4"> 236 + <div className="flex items-center mb-4"> 237 + <AlertCircle className="h-6 w-6 text-red-600 mr-3" /> 238 + <h3 className="text-lg font-medium text-white"> 239 + Clear All Todos 240 + </h3> 241 + </div> 242 + <p className="text-gray-400 mb-6"> 243 + Are you sure you want to clear all todos? This action cannot be undone. 244 + </p> 245 + <div className="flex justify-end space-x-3"> 246 + <button 247 + onClick={() => setShowClearConfirm(false)} 248 + className="btn btn-secondary" 249 + disabled={clearAllMutation.isPending} 250 + > 251 + Cancel 252 + </button> 253 + <button 254 + onClick={handleClearAll} 255 + className="btn btn-danger" 256 + disabled={clearAllMutation.isPending} 257 + > 258 + {clearAllMutation.isPending ? 'Clearing...' : 'Clear All'} 259 + </button> 260 + </div> 261 + </div> 262 + </div> 263 + )} 264 + </div> 265 + ) 266 + } 267 + 268 + export default TodosPage
+71
web/src/stores/authStore.ts
··· 1 + import { create } from 'zustand'; 2 + import { persist } from 'zustand/middleware'; 3 + import axios from 'axios'; 4 + 5 + interface User { 6 + id: string; 7 + username: string; 8 + discriminator: string | null; 9 + avatar: string | null; 10 + email?: string; 11 + } 12 + 13 + interface AuthState { 14 + user: User | null; 15 + token: string | null; 16 + isAuthenticated: boolean; 17 + login: (token: string, user: User) => void; 18 + logout: () => void; 19 + checkAuth: () => Promise<void>; 20 + } 21 + 22 + export const useAuthStore = create<AuthState>()( 23 + persist( 24 + (set, get) => ({ 25 + user: null, 26 + token: null, 27 + isAuthenticated: false, 28 + 29 + login: (token: string, user: User) => { 30 + localStorage.setItem('token', token); 31 + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 32 + set({ token, user, isAuthenticated: true }); 33 + }, 34 + 35 + logout: () => { 36 + localStorage.removeItem('token'); 37 + delete axios.defaults.headers.common['Authorization']; 38 + set({ token: null, user: null, isAuthenticated: false }); 39 + }, 40 + 41 + checkAuth: async () => { 42 + const token = localStorage.getItem('token'); 43 + if (!token) { 44 + set({ isAuthenticated: false }); 45 + return; 46 + } 47 + 48 + try { 49 + axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; 50 + const response = await axios.get('/api/auth/me'); 51 + set({ 52 + token, 53 + user: response.data.user, 54 + isAuthenticated: true, 55 + }); 56 + } catch (error) { 57 + console.error('Auth check failed:', error); 58 + get().logout(); 59 + } 60 + }, 61 + }), 62 + { 63 + name: 'auth-storage', 64 + partialize: (state) => ({ 65 + token: state.token, 66 + user: state.user, 67 + isAuthenticated: state.isAuthenticated, 68 + }), 69 + } 70 + ) 71 + );
+42
web/tailwind.config.js
··· 1 + /** @type {import('tailwindcss').Config} */ 2 + export default { 3 + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], 4 + theme: { 5 + extend: { 6 + colors: { 7 + discord: { 8 + blurple: '#5865F2', 9 + dark: '#2C2F33', 10 + gray: '#99AAB5', 11 + red: '#F04747', 12 + }, 13 + primary: { 14 + 50: '#eef2ff', 15 + 100: '#e0e7ff', 16 + 500: '#5865F2', 17 + 600: '#4f46e5', 18 + 700: '#4338ca', 19 + }, 20 + gray: { 21 + 50: '#f9fafb', 22 + 100: '#f3f4f6', 23 + 200: '#e5e7eb', 24 + 300: '#d1d5db', 25 + 400: '#9ca3af', 26 + 500: '#6b7280', 27 + 600: '#4b5563', 28 + 700: '#374151', 29 + 800: '#1f2937', 30 + 900: '#111827', 31 + }, 32 + }, 33 + fontFamily: { 34 + sans: ['Inter', 'system-ui', 'sans-serif'], 35 + }, 36 + borderRadius: { 37 + lg: '8px', 38 + }, 39 + }, 40 + }, 41 + plugins: [], 42 + };
+25
web/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "target": "ES2020", 4 + "useDefineForClassFields": true, 5 + "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 + "module": "ESNext", 7 + "skipLibCheck": true, 8 + "moduleResolution": "bundler", 9 + "allowImportingTsExtensions": true, 10 + "resolveJsonModule": true, 11 + "isolatedModules": true, 12 + "noEmit": true, 13 + "jsx": "react-jsx", 14 + "strict": true, 15 + "noUnusedLocals": true, 16 + "noUnusedParameters": true, 17 + "noFallthroughCasesInSwitch": true, 18 + "baseUrl": ".", 19 + "paths": { 20 + "@/*": ["./src/*"] 21 + } 22 + }, 23 + "include": ["src"], 24 + "references": [{ "path": "./tsconfig.node.json" }] 25 + }
+10
web/tsconfig.node.json
··· 1 + { 2 + "compilerOptions": { 3 + "composite": true, 4 + "skipLibCheck": true, 5 + "module": "ESNext", 6 + "moduleResolution": "bundler", 7 + "allowSyntheticDefaultImports": true 8 + }, 9 + "include": ["vite.config.ts"] 10 + }
+25
web/vite.config.ts
··· 1 + import { defineConfig } from 'vite'; 2 + import react from '@vitejs/plugin-react'; 3 + import path from 'path'; 4 + 5 + export default defineConfig({ 6 + plugins: [react()], 7 + resolve: { 8 + alias: { 9 + '@': path.resolve(__dirname, './src'), 10 + }, 11 + }, 12 + server: { 13 + port: 3000, 14 + proxy: { 15 + '/api': { 16 + target: 'http://localhost:2020', 17 + changeOrigin: true, 18 + }, 19 + }, 20 + }, 21 + build: { 22 + outDir: 'dist', 23 + sourcemap: true, 24 + }, 25 + });