diff --git a/package-lock.json b/package-lock.json index 96bd3f5..14b3e9d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2320,9 +2320,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", - "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", "cpu": [ "arm" ], @@ -2334,9 +2334,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", - "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", "cpu": [ "arm64" ], @@ -2348,9 +2348,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", - "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", "cpu": [ "arm64" ], @@ -2362,9 +2362,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", - "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", "cpu": [ "x64" ], @@ -2376,9 +2376,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", - "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", "cpu": [ "arm64" ], @@ -2390,9 +2390,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", - "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", "cpu": [ "x64" ], @@ -2404,9 +2404,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", - "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", "cpu": [ "arm" ], @@ -2418,9 +2418,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", - "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", "cpu": [ "arm" ], @@ -2432,9 +2432,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", - "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", "cpu": [ "arm64" ], @@ -2446,9 +2446,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", - "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", "cpu": [ "arm64" ], @@ -2460,9 +2460,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", - "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", "cpu": [ "loong64" ], @@ -2474,9 +2474,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", - "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", "cpu": [ "loong64" ], @@ -2488,9 +2488,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", - "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", "cpu": [ "ppc64" ], @@ -2502,9 +2502,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", - "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", "cpu": [ "ppc64" ], @@ -2516,9 +2516,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", - "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", "cpu": [ "riscv64" ], @@ -2530,9 +2530,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", - "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", "cpu": [ "riscv64" ], @@ -2544,9 +2544,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", - "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", "cpu": [ "s390x" ], @@ -2558,9 +2558,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", - "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", "cpu": [ "x64" ], @@ -2572,9 +2572,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", - "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", "cpu": [ "x64" ], @@ -2586,9 +2586,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", - "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", "cpu": [ "x64" ], @@ -2600,9 +2600,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", - "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", "cpu": [ "arm64" ], @@ -2614,9 +2614,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", - "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", "cpu": [ "arm64" ], @@ -2628,9 +2628,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", - "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", "cpu": [ "ia32" ], @@ -2642,9 +2642,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", - "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", "cpu": [ "x64" ], @@ -2656,9 +2656,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", - "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", "cpu": [ "x64" ], @@ -3501,9 +3501,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -3776,9 +3776,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", "dev": true, "license": "MIT", "dependencies": { @@ -3786,13 +3786,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -3900,10 +3900,22 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { @@ -4051,14 +4063,15 @@ } }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.17.0.tgz", + "integrity": "sha512-J8SwNxprqqpbfenehxWYXE7CW+wM1BB4w3+N+g+/Wx40xM4rsLrfPmHHxSWIxJLYDgSY/HqlFPIYb2/S3rxafw==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/balanced-match": { @@ -4100,9 +4113,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", "dev": true, "license": "MIT", "dependencies": { @@ -4585,7 +4598,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4651,9 +4663,9 @@ "license": "MIT" }, "node_modules/dompurify": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", - "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.9.tgz", + "integrity": "sha512-4dPSRMRDqHvs0V4YDFCsaIZo4if5u0xM+llyxiM2fwuZFdKArUBAF3VtI2+n8NKg9P870WMdYk0UhqQNoWXbfQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -5178,16 +5190,16 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -5534,6 +5546,19 @@ "void-elements": "3.1.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/husky": { "version": "9.1.7", "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", @@ -6072,9 +6097,9 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -6109,7 +6134,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/mz": { @@ -6138,9 +6162,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "funding": [ { "type": "github", @@ -6317,9 +6341,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -6363,9 +6387,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "funding": [ { "type": "opencollective", @@ -6382,7 +6406,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -6824,10 +6848,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -7049,9 +7076,9 @@ } }, "node_modules/react-router": { - "version": "7.13.0", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz", - "integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz", + "integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -7250,13 +7277,13 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.57.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", - "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@types/estree": "1.0.9" }, "bin": { "rollup": "dist/bin/rollup" @@ -7266,31 +7293,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.57.1", - "@rollup/rollup-android-arm64": "4.57.1", - "@rollup/rollup-darwin-arm64": "4.57.1", - "@rollup/rollup-darwin-x64": "4.57.1", - "@rollup/rollup-freebsd-arm64": "4.57.1", - "@rollup/rollup-freebsd-x64": "4.57.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", - "@rollup/rollup-linux-arm-musleabihf": "4.57.1", - "@rollup/rollup-linux-arm64-gnu": "4.57.1", - "@rollup/rollup-linux-arm64-musl": "4.57.1", - "@rollup/rollup-linux-loong64-gnu": "4.57.1", - "@rollup/rollup-linux-loong64-musl": "4.57.1", - "@rollup/rollup-linux-ppc64-gnu": "4.57.1", - "@rollup/rollup-linux-ppc64-musl": "4.57.1", - "@rollup/rollup-linux-riscv64-gnu": "4.57.1", - "@rollup/rollup-linux-riscv64-musl": "4.57.1", - "@rollup/rollup-linux-s390x-gnu": "4.57.1", - "@rollup/rollup-linux-x64-gnu": "4.57.1", - "@rollup/rollup-linux-x64-musl": "4.57.1", - "@rollup/rollup-openbsd-x64": "4.57.1", - "@rollup/rollup-openharmony-arm64": "4.57.1", - "@rollup/rollup-win32-arm64-msvc": "4.57.1", - "@rollup/rollup-win32-ia32-msvc": "4.57.1", - "@rollup/rollup-win32-x64-gnu": "4.57.1", - "@rollup/rollup-win32-x64-msvc": "4.57.1", + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", "fsevents": "~2.3.2" } }, @@ -7665,9 +7692,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -7925,9 +7952,9 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", "dev": true, "license": "MIT", "dependencies": { @@ -8018,9 +8045,9 @@ } }, "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -8128,9 +8155,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", - "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "dev": true, "license": "ISC", "bin": { diff --git a/public/miniapp/redirect.html b/public/miniapp/redirect.html index b6e5151..cca3db1 100644 --- a/public/miniapp/redirect.html +++ b/public/miniapp/redirect.html @@ -148,14 +148,27 @@ const manualBtn = document.getElementById('manualBtn'); manualBtn.href = url; - // Try to open the URL scheme directly - // This page is opened in external browser, so custom schemes work - window.location.href = url; + // Attempt to launch the app WITHOUT a top-level navigation. A direct + // `location.href = scheme` paints a full-page net::ERR_UNKNOWN_URL_SCHEME + // inside Android in-app browsers (Telegram/Yandex/…) and silently fails on + // iOS — hiding the manual button (Telegram bug #654272). A hidden iframe is + // contained: the app opens if installed, otherwise the failure stays inside + // the (invisible) frame and the button below stays usable. + try { + var frame = document.createElement('iframe'); + frame.style.display = 'none'; + frame.src = url; + document.body.appendChild(frame); + setTimeout(function() { + try { frame.parentNode.removeChild(frame); } catch (e) {} + }, 2000); + } catch (e) { + window.location.href = url; + } - // Show manual button after a delay in case redirect didn't work - setTimeout(function() { - document.getElementById('errorBlock').classList.add('show'); - }, 2000); + // Programmatic opening is unreliable in in-app browsers, so surface the + // manual "Open app" button immediately — a user tap is the reliable trigger. + document.getElementById('errorBlock').classList.add('show'); })(); diff --git a/src/App.tsx b/src/App.tsx index 6c47968..ce45aa3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -135,7 +135,6 @@ const AdminBroadcastDetail = lazyWithRetry(() => import('./pages/AdminBroadcastD const AdminPinnedMessages = lazyWithRetry(() => import('./pages/AdminPinnedMessages')); const AdminPinnedMessageCreate = lazyWithRetry(() => import('./pages/AdminPinnedMessageCreate')); const AdminChannelSubscriptions = lazyWithRetry(() => import('./pages/AdminChannelSubscriptions')); -const AdminEmailTemplatePreview = lazyWithRetry(() => import('./pages/AdminEmailTemplatePreview')); const AdminRoles = lazyWithRetry(() => import('./pages/AdminRoles')); const AdminRoleEdit = lazyWithRetry(() => import('./pages/AdminRoleEdit')); const AdminRoleAssign = lazyWithRetry(() => import('./pages/AdminRoleAssign')); @@ -156,6 +155,7 @@ const AdminNewsCreate = lazyWithRetry(() => import('./pages/AdminNewsCreate')); const InfoPageView = lazyWithRetry(() => import('./pages/InfoPageView')); const AdminInfoPages = lazyWithRetry(() => import('./pages/AdminInfoPages')); const AdminInfoPageEditor = lazyWithRetry(() => import('./pages/AdminInfoPageEditor')); +const AdminLegalPages = lazyWithRetry(() => import('./pages/AdminLegalPages')); function ProtectedRoute({ children, @@ -408,6 +408,18 @@ function App() { } /> + {/* Path-based method variant: some providers (Lava) reject return URLs that carry a + query string, so the method is encoded in the path instead of ?method=. */} + + + + + + } + /> } /> + {/* Deep-link target for admin ticket notification buttons (bot issue #2988): + opens a specific ticket directly. Static "/settings" above out-ranks + this dynamic segment in react-router, so there is no conflict. */} + + + + + + } + /> } /> - - - - - - } - /> - {/* RBAC routes */} } /> + + + + + + } + /> ` startapp param → /admin/tickets/. */ +const ADMIN_TICKET_START_PARAM_RE = /^admin_ticket_(\d+)$/; + +/** + * Routes a Telegram Mini App start param to an in-app destination on launch. + * + * Admin ticket notification buttons in GROUP/channel chats open the cabinet via + * a `t.me//?startapp=admin_ticket_` deep link (bot issue #2988) — + * `web_app` buttons don't work in group chats, so the startapp param is the only + * way in. Telegram delivers it as `tgWebAppStartParam`; we map it to the admin + * ticket route once on mount. Access is still gated by the route's + * `PermissionRoute permission="tickets:read"`. + */ +function StartParamNavigator() { + const navigate = useNavigate(); + const handled = useRef(false); + + useEffect(() => { + if (handled.current) return; + handled.current = true; + + let startParam: string | undefined; + try { + startParam = retrieveLaunchParams().tgWebAppStartParam; + } catch { + return; + } + if (!startParam) return; + + const match = ADMIN_TICKET_START_PARAM_RE.exec(startParam); + if (match) { + navigate(`/admin/tickets/${match[1]}`, { replace: true }); + } + }, [navigate]); + + return null; +} + export function AppWithNavigator() { const isTelegram = isInTelegramWebApp(); return ( {isTelegram && } + {isTelegram && } diff --git a/src/api/adminEmailTemplates.ts b/src/api/adminEmailTemplates.ts index 0a0164d..68e1332 100644 --- a/src/api/adminEmailTemplates.ts +++ b/src/api/adminEmailTemplates.ts @@ -15,6 +15,8 @@ export interface EmailTemplateType { export interface EmailTemplateListResponse { items: EmailTemplateType[]; available_languages: string[]; + /** Placeholders available in every template regardless of type */ + common_context_vars: string[]; } export interface EmailTemplateLanguageData { @@ -30,6 +32,8 @@ export interface EmailTemplateDetail { label: Record; description: Record; context_vars: string[]; + /** Placeholders available in every template regardless of type */ + common_context_vars?: string[]; languages: Record; } @@ -52,6 +56,9 @@ export interface EmailTemplatePreviewResponse { export interface EmailTemplateSendTestRequest { language: string; email?: string; + /** Current editor content — when set, the test sends it instead of the saved template */ + subject?: string; + body_html?: string; } export const adminEmailTemplatesApi = { diff --git a/src/api/adminLegalPages.ts b/src/api/adminLegalPages.ts new file mode 100644 index 0000000..d6b3f7e --- /dev/null +++ b/src/api/adminLegalPages.ts @@ -0,0 +1,156 @@ +import apiClient from './client'; +import type { InfoPageDisplayMode } from './infoPages'; + +export type LegalDisplayMode = InfoPageDisplayMode; + +export interface LegalDocumentItem { + language: string; + content: string; + is_enabled: boolean; + updated_at: string | null; +} + +export interface LegalDocumentResponse { + display_mode: LegalDisplayMode; + display_mode_env_locked: boolean; + items: LegalDocumentItem[]; +} + +export interface LegalDocumentUpdateRequest { + display_mode?: LegalDisplayMode; + items?: Array<{ language: string; content: string; is_enabled: boolean }>; +} + +export interface RulesItem { + language: string; + content: string; + updated_at: string | null; +} + +export interface AdminRulesResponse { + display_mode: LegalDisplayMode; + display_mode_env_locked: boolean; + items: RulesItem[]; +} + +export interface RulesUpdateRequest { + display_mode?: LegalDisplayMode; + items?: Array<{ language: string; content: string }>; +} + +export interface FaqSettingItem { + language: string; + is_enabled: boolean; +} + +export interface FaqPageItem { + id: number; + language: string; + title: string; + content: string; + display_order: number; + is_active: boolean; + updated_at: string | null; +} + +export interface FaqResponse { + display_mode: LegalDisplayMode; + display_mode_env_locked: boolean; + settings: FaqSettingItem[]; + pages: FaqPageItem[]; +} + +export interface FaqUpdateRequest { + display_mode?: LegalDisplayMode; + settings?: FaqSettingItem[]; +} + +export interface FaqPageCreateRequest { + language: string; + title: string; + content: string; + display_order?: number; + is_active?: boolean; +} + +export interface FaqPageUpdateRequest { + title?: string; + content?: string; + display_order?: number; + is_active?: boolean; +} + +export const adminLegalPagesApi = { + getPrivacyPolicy: async (): Promise => { + const response = await apiClient.get( + '/cabinet/admin/legal-pages/privacy-policy', + ); + return response.data; + }, + + updatePrivacyPolicy: async (data: LegalDocumentUpdateRequest): Promise => { + const response = await apiClient.put( + '/cabinet/admin/legal-pages/privacy-policy', + data, + ); + return response.data; + }, + + getPublicOffer: async (): Promise => { + const response = await apiClient.get( + '/cabinet/admin/legal-pages/public-offer', + ); + return response.data; + }, + + updatePublicOffer: async (data: LegalDocumentUpdateRequest): Promise => { + const response = await apiClient.put( + '/cabinet/admin/legal-pages/public-offer', + data, + ); + return response.data; + }, + + getRules: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/legal-pages/rules'); + return response.data; + }, + + updateRules: async (data: RulesUpdateRequest): Promise => { + const response = await apiClient.put( + '/cabinet/admin/legal-pages/rules', + data, + ); + return response.data; + }, + + getFaq: async (): Promise => { + const response = await apiClient.get('/cabinet/admin/legal-pages/faq'); + return response.data; + }, + + updateFaq: async (data: FaqUpdateRequest): Promise => { + const response = await apiClient.put('/cabinet/admin/legal-pages/faq', data); + return response.data; + }, + + createFaqPage: async (data: FaqPageCreateRequest): Promise => { + const response = await apiClient.post( + '/cabinet/admin/legal-pages/faq/pages', + data, + ); + return response.data; + }, + + updateFaqPage: async (id: number, data: FaqPageUpdateRequest): Promise => { + const response = await apiClient.put( + `/cabinet/admin/legal-pages/faq/pages/${id}`, + data, + ); + return response.data; + }, + + deleteFaqPage: async (id: number): Promise => { + await apiClient.delete(`/cabinet/admin/legal-pages/faq/pages/${id}`); + }, +}; diff --git a/src/api/adminOverpayCertificate.ts b/src/api/adminOverpayCertificate.ts new file mode 100644 index 0000000..dea24c9 --- /dev/null +++ b/src/api/adminOverpayCertificate.ts @@ -0,0 +1,48 @@ +import apiClient from './client'; + +export const OVERPAY_CERT_MAX_SIZE = 1024 * 1024; + +export interface OverpayCertificateStatus { + uploaded: boolean; + valid: boolean; + path: string; + subject: string | null; + not_valid_after: string | null; + has_chain: boolean | null; + env_locked_path: boolean; + env_locked_passphrase: boolean; +} + +export interface OverpayCertificateUploadResponse { + subject: string; + not_valid_after: string; + has_chain: boolean; + path: string; + env_locked_path: boolean; + env_locked_passphrase: boolean; + warning: string | null; +} + +export const adminOverpayCertificateApi = { + getStatus: async (): Promise => { + const response = await apiClient.get( + '/cabinet/admin/overpay/certificate', + ); + return response.data; + }, + + upload: async (file: File, passphrase: string): Promise => { + const formData = new FormData(); + formData.append('file', file); + formData.append('passphrase', passphrase); + const response = await apiClient.post( + '/cabinet/admin/overpay/certificate', + formData, + ); + return response.data; + }, + + remove: async (): Promise => { + await apiClient.delete('/cabinet/admin/overpay/certificate'); + }, +}; diff --git a/src/api/adminSettings.ts b/src/api/adminSettings.ts index 84d3932..786acd9 100644 --- a/src/api/adminSettings.ts +++ b/src/api/adminSettings.ts @@ -35,6 +35,11 @@ export interface SettingDefinition { original: unknown; has_override: boolean; read_only: boolean; + // Secret-bearing key (token/secret/password/key). `current`/`original` come back masked + // (••••••••); the UI renders a password input and only sends a value when actually changed. + is_secret?: boolean; + // Pinned in .env: value shadows the DB and can't be edited here (only viewed). + env_locked: boolean; choices: SettingChoice[]; hint?: SettingHint | null; } diff --git a/src/api/client.ts b/src/api/client.ts index ec4a1fc..c643618 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -70,7 +70,6 @@ const AUTH_ENDPOINTS = [ '/cabinet/auth/password/forgot', '/cabinet/auth/password/reset', '/cabinet/auth/oauth/', - '/cabinet/auth/merge/', '/cabinet/auth/account/link/server-complete', '/cabinet/auth/deeplink/', '/cabinet/auth/login/auto', diff --git a/src/api/health.ts b/src/api/health.ts index a4cac20..1967fff 100644 --- a/src/api/health.ts +++ b/src/api/health.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { useBlockingStore } from '../store/blocking'; +import { HEALTH } from '../config/constants'; /** * Backend liveness probe + "service unavailable" detection. @@ -44,9 +45,9 @@ const GATEWAY_DOWN_STATUSES = new Set([502, 503, 504]); * proxy is up but the API upstream is dead, so it counts as NOT reachable. * Resolves false on a transport-level failure with no response. */ -export async function pingBackend(): Promise { +export async function pingBackend(timeoutMs: number = HEALTH.PROBE_TIMEOUT_MS): Promise { try { - await axios.get(HEALTH_URL, { timeout: 5000 }); + await axios.get(HEALTH_URL, { timeout: timeoutMs }); return true; } catch (err) { if (axios.isAxiosError(err) && err.response) { @@ -56,6 +57,22 @@ export async function pingBackend(): Promise { } } +/** + * Confirm a *real* outage before blanking the app. Probes with the tolerant timeout and + * retries once: returns true only when the backend is unreachable across all attempts. A + * single slow/cold-connection blip on a working backend (common on mobile / in-app webviews) + * must not flip the full-screen ServiceUnavailableScreen. + */ +async function confirmBackendDown(): Promise { + for (let attempt = 0; attempt <= HEALTH.CONFIRM_RETRIES; attempt++) { + if (await pingBackend()) return false; + if (attempt < HEALTH.CONFIRM_RETRIES) { + await new Promise((resolve) => setTimeout(resolve, HEALTH.CONFIRM_RETRY_DELAY_MS)); + } + } + return true; +} + let everReachedBackend = false; /** Called on the first successful API response: the app reached the backend at @@ -99,9 +116,13 @@ export async function reportPossibleBackendDown(): Promise { if (confirmInFlight) return; confirmInFlight = true; try { - const reachable = await pingBackend(); - if (!reachable) { - useBlockingStore.getState().setBackendUnavailable(); + // Confirm with a tolerant, retried probe so a single cold-connection blip on a working + // backend can't blank an already-loaded session. + if (await confirmBackendDown()) { + // Re-check: a concurrent request may have reached the backend during the probe. + if (useBlockingStore.getState().blockingType !== 'backend_unavailable') { + useBlockingStore.getState().setBackendUnavailable(); + } } } finally { confirmInFlight = false; @@ -110,17 +131,22 @@ export async function reportPossibleBackendDown(): Promise { /** * Eager liveness check fired once at app launch (from main.tsx), in parallel - * with auth bootstrap. If the backend is already down at launch this paints the - * ServiceUnavailableScreen immediately — before the auth flow can flash the - * /login page — even on the no-stored-token path that makes no early request. - * No-op if the app has already reached the backend or another blocking screen - * is showing. + * with auth bootstrap. If the backend is genuinely down at launch this paints the + * ServiceUnavailableScreen — before the auth flow can flash the /login page — even + * on the no-stored-token path that makes no early request. + * + * Uses the tolerant, retried confirm probe (NOT a single short-timeout ping): the old + * hardcoded 5s probe falsely flagged slow devices / cold mobile connections as + * "service unavailable" while the real 30s API requests racing alongside it would have + * succeeded — the exact "works on one device, not another on the same Wi-Fi" report. + * No-op if the app has already reached the backend or another blocking screen is showing. */ export async function checkBackendOnStartup(): Promise { if (everReachedBackend) return; if (useBlockingStore.getState().blockingType !== null) return; - const reachable = await pingBackend(); - if (!reachable && !everReachedBackend && useBlockingStore.getState().blockingType === null) { - useBlockingStore.getState().setBackendUnavailable(); + if (await confirmBackendDown()) { + if (!everReachedBackend && useBlockingStore.getState().blockingType === null) { + useBlockingStore.getState().setBackendUnavailable(); + } } } diff --git a/src/api/info.ts b/src/api/info.ts index 7ccf50d..76894d1 100644 --- a/src/api/info.ts +++ b/src/api/info.ts @@ -37,6 +37,13 @@ export interface LanguageInfo { flag: string; } +export interface InfoVisibility { + faq: boolean; + rules: boolean; + privacy: boolean; + offer: boolean; +} + export const infoApi = { // Get FAQ pages list getFaqPages: async (): Promise => { @@ -99,4 +106,9 @@ export const infoApi = { const response = await apiClient.get('/cabinet/info/support-config'); return response.data; }, + + getVisibility: async (): Promise => { + const response = await apiClient.get('/cabinet/info/visibility'); + return response.data; + }, }; diff --git a/src/api/infoPages.ts b/src/api/infoPages.ts index 39a39a4..899a438 100644 --- a/src/api/infoPages.ts +++ b/src/api/infoPages.ts @@ -4,6 +4,8 @@ export type InfoPageType = 'page' | 'faq'; export type ReplacesTab = 'faq' | 'rules' | 'privacy' | 'offer'; +export type InfoPageDisplayMode = 'bot' | 'web' | 'both'; + export interface InfoPage { id: number; slug: string; @@ -14,6 +16,7 @@ export interface InfoPage { sort_order: number; icon: string | null; replaces_tab: ReplacesTab | null; + display_mode: InfoPageDisplayMode; created_at: string; updated_at: string | null; } @@ -27,6 +30,7 @@ export interface InfoPageListItem { sort_order: number; icon: string | null; replaces_tab: ReplacesTab | null; + display_mode: InfoPageDisplayMode; updated_at: string | null; } @@ -39,6 +43,7 @@ export interface InfoPageCreateRequest { sort_order: number; icon: string | null; replaces_tab: ReplacesTab | null; + display_mode: InfoPageDisplayMode; } export interface InfoPageUpdateRequest { @@ -50,6 +55,7 @@ export interface InfoPageUpdateRequest { sort_order?: number; icon?: string | null; replaces_tab?: ReplacesTab | null; + display_mode?: InfoPageDisplayMode; } export type TabReplacements = Record; diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index ab8206d..2bfb4e5 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -74,7 +74,7 @@ export class ErrorBoundary extends Component @@ -110,7 +110,7 @@ export class ErrorBoundary extends Component diff --git a/src/components/PromoOffersSection.tsx b/src/components/PromoOffersSection.tsx index 00f22df..cb26698 100644 --- a/src/components/PromoOffersSection.tsx +++ b/src/components/PromoOffersSection.tsx @@ -118,9 +118,21 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio }, }); - // Deactivate discount mutation + // Deactivate discount mutation. + // A discount granted by a PROMOCODE must be turned off via the dedicated route + // (POST /cabinet/promocode/deactivate-discount), which also rolls back the promocode + // usage (deletes the PromoCodeUse, decrements current_uses, strips the promo group) so + // the user can re-activate it later. The plain clearActiveDiscount route only zeroes + // the discount fields and is correct for admin/offer-sourced discounts. const deactivateMutation = useMutation({ - mutationFn: promoApi.clearActiveDiscount, + mutationFn: async () => { + const source = activeDiscount?.source ?? ''; + if (source.startsWith('promocode:')) { + await promoApi.deactivateDiscount(); + } else { + await promoApi.clearActiveDiscount(); + } + }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['active-discount'] }); queryClient.invalidateQueries({ queryKey: ['promo-offers'] }); @@ -131,8 +143,14 @@ export default function PromoOffersSection({ className = '' }: PromoOffersSectio setTimeout(() => setSuccessMessage(null), 5000); }, onError: (error: unknown) => { - const axiosErr = error as { response?: { data?: { detail?: string } } }; - setErrorMessage(axiosErr.response?.data?.detail || t('promo.deactivate.error')); + // detail may be a plain string (clearActiveDiscount route) or a structured + // { code, message } object (deactivate-discount route). Handle both. + const axiosErr = error as { + response?: { data?: { detail?: string | { message?: string } } }; + }; + const detail = axiosErr.response?.data?.detail; + const message = typeof detail === 'string' ? detail : detail?.message; + setErrorMessage(message || t('promo.deactivate.error')); setTimeout(() => setErrorMessage(null), 5000); }, diff --git a/src/components/SuccessNotificationModal.tsx b/src/components/SuccessNotificationModal.tsx index 5af94fd..31cc36d 100644 --- a/src/components/SuccessNotificationModal.tsx +++ b/src/components/SuccessNotificationModal.tsx @@ -281,7 +281,7 @@ export default function SuccessNotificationModal() { {isSubscription && ( @@ -253,7 +253,7 @@ export function AnalyticsTab() { @@ -307,7 +307,7 @@ export function AnalyticsTab() { diff --git a/src/components/admin/BackgroundEditor.tsx b/src/components/admin/BackgroundEditor.tsx index 59c75e0..0aea7e7 100644 --- a/src/components/admin/BackgroundEditor.tsx +++ b/src/components/admin/BackgroundEditor.tsx @@ -68,7 +68,7 @@ export function BackgroundEditor() { 'w-full rounded-xl py-3 text-sm font-medium transition-colors', saveStatus === 'saved' ? 'bg-success-500/20 text-success-400' - : 'bg-accent-500 text-white hover:bg-accent-600 disabled:opacity-50', + : 'bg-accent-500 text-on-accent hover:bg-accent-600 disabled:opacity-50', )} > {saveStatus === 'saving' diff --git a/src/components/admin/BrandingTab.tsx b/src/components/admin/BrandingTab.tsx index 6f13c67..0e9727f 100644 --- a/src/components/admin/BrandingTab.tsx +++ b/src/components/admin/BrandingTab.tsx @@ -168,7 +168,7 @@ export function BrandingTab({ accentColor = '#3b82f6' }: BrandingTabProps) { diff --git a/src/components/admin/ButtonsTab.tsx b/src/components/admin/ButtonsTab.tsx index 5d1ec29..c7b05d8 100644 --- a/src/components/admin/ButtonsTab.tsx +++ b/src/components/admin/ButtonsTab.tsx @@ -228,7 +228,7 @@ export function ButtonsTab() { ? 'bg-success-500 text-white' : cfg.style === 'danger' ? 'bg-error-500 text-white' - : 'bg-accent-500 text-white' + : 'bg-accent-500 text-on-accent' }`} > {t(`admin.buttons.styles.${cfg.style}`)} @@ -321,7 +321,7 @@ export function ButtonsTab() { diff --git a/src/components/admin/ColoredItemCombobox.tsx b/src/components/admin/ColoredItemCombobox.tsx index d023f7b..8602494 100644 --- a/src/components/admin/ColoredItemCombobox.tsx +++ b/src/components/admin/ColoredItemCombobox.tsx @@ -346,7 +346,7 @@ export function ColoredItemCombobox({ type="button" onClick={handleCreate} disabled={isCreating} - className="flex min-h-[44px] w-full items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50" + className="flex min-h-[44px] w-full items-center justify-center gap-2 rounded-lg bg-accent-500 px-4 py-2.5 text-sm font-medium text-on-accent transition-colors hover:bg-accent-600 disabled:cursor-not-allowed disabled:opacity-50" > {isCreating ? (
diff --git a/src/components/admin/MenuEditorTab.tsx b/src/components/admin/MenuEditorTab.tsx index fb8aeb4..56dbf11 100644 --- a/src/components/admin/MenuEditorTab.tsx +++ b/src/components/admin/MenuEditorTab.tsx @@ -72,7 +72,7 @@ function MaxPerRowSelector({ value, onChange }: MaxPerRowSelectorProps) { onClick={() => onChange(n)} className={`flex h-7 w-7 items-center justify-center rounded-lg text-xs font-semibold transition-all ${ value === n - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-dark-700/50 text-dark-400 hover:bg-dark-600 hover:text-dark-300' }`} > @@ -788,7 +788,7 @@ export function MenuEditorTab() { diff --git a/src/components/admin/QuickToggles.tsx b/src/components/admin/QuickToggles.tsx index 649de9c..550c5b9 100644 --- a/src/components/admin/QuickToggles.tsx +++ b/src/components/admin/QuickToggles.tsx @@ -13,7 +13,9 @@ interface QuickTogglesProps { export function QuickToggles({ settings, onUpdate, disabled, className }: QuickTogglesProps) { const { t } = useTranslation(); - const booleanSettings = settings.filter((s) => s.type === 'bool' && !s.read_only); + const booleanSettings = settings.filter( + (s) => s.type === 'bool' && !s.read_only && !s.env_locked, + ); if (booleanSettings.length === 0) { return null; diff --git a/src/components/admin/SettingInput.tsx b/src/components/admin/SettingInput.tsx index edbe97c..1b8535f 100644 --- a/src/components/admin/SettingInput.tsx +++ b/src/components/admin/SettingInput.tsx @@ -40,7 +40,10 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps) const inputRef = useRef(null); const currentValue = String(setting.current ?? ''); - const needsTextarea = isLongValue(currentValue) || isListOrJsonKey(setting.key); + // Secrets are always edited via the single-line (password) input — never a textarea — and + // never pre-filled with the masked value, so leaving the field empty means "keep current". + const needsTextarea = + !setting.is_secret && (isLongValue(currentValue) || isListOrJsonKey(setting.key)); // Auto-resize textarea useEffect(() => { @@ -51,11 +54,19 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps) }, [value, isEditing]); const handleStart = () => { - setValue(currentValue); + // For secrets, start from an empty field (the displayed value is just the mask) so the + // admin types a brand-new value; leaving it empty is treated as "no change". + setValue(setting.is_secret ? '' : currentValue); setIsEditing(true); }; const handleSave = () => { + // Empty secret field = the admin opened edit but didn't change anything → keep the stored + // secret instead of overwriting it with an empty value. + if (setting.is_secret && value === '') { + handleCancel(); + return; + } onUpdate(value); setIsEditing(false); }; @@ -111,7 +122,7 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps)
{description && (

{description}

@@ -100,10 +113,17 @@ export function SettingRow({
- {setting.read_only ? ( - // Read-only display -
- {String(setting.current ?? '-')} + {locked ? ( + // Read-only / env-locked display +
+
+ {String(setting.current ?? '-')} +
+ {setting.env_locked && !setting.read_only && ( +

+ {t('admin.settings.envLockedHint')} +

+ )}
) : setting.type === 'bool' ? ( // Boolean toggle @@ -158,7 +178,7 @@ export function SettingRow({
{/* Reset button for long values - shown below */} - {isLongValue && setting.has_override && !setting.read_only && setting.type !== 'bool' && ( + {isLongValue && setting.has_override && !locked && setting.type !== 'bool' && (
@@ -647,7 +647,7 @@ export function ActionModal({ diff --git a/src/components/admin/bulkActions/FloatingActionBar.tsx b/src/components/admin/bulkActions/FloatingActionBar.tsx index 3808c06..8a05aed 100644 --- a/src/components/admin/bulkActions/FloatingActionBar.tsx +++ b/src/components/admin/bulkActions/FloatingActionBar.tsx @@ -208,7 +208,7 @@ export function FloatingActionBar({
@@ -475,7 +475,7 @@ export function InfoTab(props: InfoTabProps) { disabled={actionLoading} className={`rounded-lg px-3 py-2 text-sm font-medium transition-all disabled:opacity-50 ${ confirmingAction === 'resetTrial' - ? 'bg-accent-500 text-white' + ? 'bg-accent-500 text-on-accent' : 'bg-accent-500/15 text-accent-400 hover:bg-accent-500/25' }`} > diff --git a/src/components/admin/userDetail/ReferralsTab.tsx b/src/components/admin/userDetail/ReferralsTab.tsx index e64c801..964bc4b 100644 --- a/src/components/admin/userDetail/ReferralsTab.tsx +++ b/src/components/admin/userDetail/ReferralsTab.tsx @@ -5,7 +5,8 @@ import { useNavigate } from 'react-router'; import { useCurrency } from '../../../hooks/useCurrency'; import { useNotify } from '../../../platform/hooks/useNotify'; import { adminUsersApi, type UserDetailResponse, type UserListItem } from '../../../api/adminUsers'; -import { XIcon } from '@/components/icons'; +import { StatCard } from '@/components/stats'; +import { BanknotesIcon, PercentIcon, TagIcon, UsersIcon, XIcon } from '@/components/icons'; // ────────────────────────────────────────────────────────────────── // Referrals tab — top-of-graph referrer + stats + referrals list, @@ -327,40 +328,35 @@ export function ReferralsTab({ user, userId, onUserRefresh }: ReferralsTabProps) {/* Section 2: Referral stats */}
-
-
- {t('admin.users.detail.referrals.totalReferrals')} -
-
- {user.referral.referrals_count} -
-
-
-
- {t('admin.users.detail.referrals.totalEarnings')} -
-
- {formatWithCurrency(user.referral.total_earnings_kopeks / 100)} -
-
-
-
- {t('admin.users.detail.referrals.commission')} -
-
- {user.referral.commission_percent != null + } + tone="neutral" + /> + } + tone="neutral" + /> + -
-
-
- {t('admin.users.detail.referrals.referralCode')} -
-
- {user.referral.referral_code} -
-
+ : t('admin.users.detail.referrals.default') + } + icon={} + tone="neutral" + /> + } + tone="neutral" + valueClassName="font-mono" + />
{/* Section 3: Referrals list */} diff --git a/src/components/admin/userDetail/SubscriptionTab.tsx b/src/components/admin/userDetail/SubscriptionTab.tsx index b6fec90..d633dc8 100644 --- a/src/components/admin/userDetail/SubscriptionTab.tsx +++ b/src/components/admin/userDetail/SubscriptionTab.tsx @@ -464,7 +464,7 @@ export function SubscriptionTab(props: SubscriptionTabProps) { diff --git a/src/components/admin/userDetail/TicketsTab.tsx b/src/components/admin/userDetail/TicketsTab.tsx index 0dbd4f0..94a9db9 100644 --- a/src/components/admin/userDetail/TicketsTab.tsx +++ b/src/components/admin/userDetail/TicketsTab.tsx @@ -348,7 +348,7 @@ function ChatView({ onClick={onReply} disabled={!replyText.trim() || replySending} aria-label={t('admin.tickets.sendReply', 'Send reply')} - className="min-h-[44px] min-w-[44px] shrink-0 self-end rounded-lg bg-accent-500 px-4 py-2 text-sm text-white transition-colors hover:bg-accent-600 disabled:opacity-50 sm:min-h-0 sm:min-w-0" + className="min-h-[44px] min-w-[44px] shrink-0 self-end rounded-lg bg-accent-500 px-4 py-2 text-sm text-on-accent transition-colors hover:bg-accent-600 disabled:opacity-50 sm:min-h-0 sm:min-w-0" > {replySending ? (
diff --git a/src/components/backgrounds/BackgroundRenderer.tsx b/src/components/backgrounds/BackgroundRenderer.tsx index df8b53e..2f8b251 100644 --- a/src/components/backgrounds/BackgroundRenderer.tsx +++ b/src/components/backgrounds/BackgroundRenderer.tsx @@ -20,6 +20,10 @@ function reduceMobileSettings(settings: Record): Record { let cancelled = false; + let timer: ReturnType | undefined; const tick = async () => { if (cancelled) return; if (await pingBackend()) { if (!cancelled) recover(); + return; } + if (!cancelled) timer = setTimeout(tick, HEALTH.RECOVERY_POLL_INTERVAL_MS); }; void tick(); - const id = setInterval(tick, POLL_INTERVAL_MS); return () => { cancelled = true; - clearInterval(id); + if (timer) clearTimeout(timer); }; }, [recover]); diff --git a/src/components/dashboard/PendingGiftCard.tsx b/src/components/dashboard/PendingGiftCard.tsx index a47f66a..2209bdb 100644 --- a/src/components/dashboard/PendingGiftCard.tsx +++ b/src/components/dashboard/PendingGiftCard.tsx @@ -57,7 +57,7 @@ export default function PendingGiftCard({ gifts, className }: PendingGiftCardPro {/* Activate button */} {t('gift.pending.activate')} diff --git a/src/components/dashboard/StatsGrid.tsx b/src/components/dashboard/StatsGrid.tsx index 3a41585..79d280f 100644 --- a/src/components/dashboard/StatsGrid.tsx +++ b/src/components/dashboard/StatsGrid.tsx @@ -1,9 +1,8 @@ -import { PiCaretRight } from 'react-icons/pi'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router'; import { useCurrency } from '../../hooks/useCurrency'; -import { useTheme } from '../../hooks/useTheme'; -import { getGlassColors } from '../../utils/glassTheme'; +import { StatCard } from '@/components/stats'; +import { CardIcon, ChevronRightIcon, UsersIcon } from '@/components/icons'; interface StatsGridProps { balanceRubles: number; @@ -12,10 +11,6 @@ interface StatsGridProps { refLoading: boolean; } -const ChevronIcon = ({ color }: { color: string }) => ( -