fix(cabinet): recover "Сервис недоступен" false-positive + top-up fixes

- health probe: tolerant timeout (12s) + retry before flagging the backend down. A
  hardcoded 5s probe racing auth bootstrap falsely showed ServiceUnavailableScreen on
  slow devices / cold mobile connections while the 30s API requests would have
  succeeded ("works on one device, not another on the same Wi-Fi"). The recovery poll
  self-reschedules with the tolerant timeout so slow devices auto-recover.
- TopUpAmount: fetch payment methods with a real query (fixes the infinite spinner on a
  cold cache / browser-back) and use canonical RUB for quick-amount chips so FX rounding
  can't reject a min-amount selection in non-RUB locales.
- settings UI: render secret values as a masked password input (pairs with the backend
  secret masking); leaving the field empty keeps the stored secret.
- deps: npm audit fix (18 -> 5 advisories).

Also bundles in-progress settings env-lock UI work.
This commit is contained in:
c0mrade
2026-06-10 16:15:55 +03:00
parent 75a570b862
commit 16fad9f4fe
14 changed files with 391 additions and 227 deletions

365
package-lock.json generated
View File

@@ -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": {

View File

@@ -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;
}

View File

@@ -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<boolean> {
export async function pingBackend(timeoutMs: number = HEALTH.PROBE_TIMEOUT_MS): Promise<boolean> {
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<boolean> {
}
}
/**
* 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<boolean> {
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,10 +116,14 @@ export async function reportPossibleBackendDown(): Promise<void> {
if (confirmInFlight) return;
confirmInFlight = true;
try {
const reachable = await pingBackend();
if (!reachable) {
// 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<void> {
/**
* 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<void> {
if (everReachedBackend) return;
if (useBlockingStore.getState().blockingType !== null) return;
const reachable = await pingBackend();
if (!reachable && !everReachedBackend && useBlockingStore.getState().blockingType === null) {
if (await confirmBackendDown()) {
if (!everReachedBackend && useBlockingStore.getState().blockingType === null) {
useBlockingStore.getState().setBackendUnavailable();
}
}
}

View File

@@ -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;

View File

@@ -40,7 +40,10 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps)
const inputRef = useRef<HTMLInputElement>(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);
};
@@ -128,7 +139,14 @@ export function SettingInput({ setting, onUpdate, disabled }: SettingInputProps)
<div className="flex items-center gap-2">
<input
ref={inputRef}
type={setting.type === 'int' || setting.type === 'float' ? 'number' : 'text'}
type={
setting.is_secret
? 'password'
: setting.type === 'int' || setting.type === 'float'
? 'number'
: 'text'
}
autoComplete={setting.is_secret ? 'new-password' : undefined}
value={value}
onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => {

View File

@@ -30,6 +30,10 @@ export function SettingRow({
const displayName = t(`admin.settings.settingNames.${formattedKey}`, formattedKey);
const description = setting.hint?.description ? stripHtml(setting.hint.description) : null;
// env-locked keys behave like read-only here: the .env value shadows the DB,
// so editing would be silently discarded by the bot. Show the value, no input.
const locked = setting.read_only || setting.env_locked;
// Check if this is a long/complex value
const isLongValue = (() => {
const val = String(setting.current ?? '');
@@ -65,6 +69,15 @@ export function SettingRow({
{t('admin.settings.readOnly')}
</span>
)}
{setting.env_locked && !setting.read_only && (
<span
className="flex items-center gap-1 rounded-full bg-dark-600/50 px-2 py-0.5 text-xs font-medium text-dark-400"
title={t('admin.settings.envLockedHint')}
>
<LockIcon />
{t('admin.settings.envLocked')}
</span>
)}
</div>
{description && (
<p className="mt-1.5 text-sm leading-relaxed text-dark-400">{description}</p>
@@ -100,11 +113,18 @@ export function SettingRow({
<div
className={`${isLongValue ? '' : 'flex items-center justify-between gap-3'} border-t border-dark-700/30 pt-3`}
>
{setting.read_only ? (
// Read-only display
{locked ? (
// Read-only / env-locked display
<div className="flex flex-col gap-1.5">
<div className="flex items-center gap-2 rounded-lg bg-dark-700/30 px-4 py-2.5 text-dark-300">
<span className="break-all font-mono text-sm">{String(setting.current ?? '-')}</span>
</div>
{setting.env_locked && !setting.read_only && (
<p className="text-xs leading-relaxed text-dark-500">
{t('admin.settings.envLockedHint')}
</p>
)}
</div>
) : setting.type === 'bool' ? (
// Boolean toggle
<div className="flex items-center justify-between gap-3">
@@ -158,7 +178,7 @@ export function SettingRow({
</div>
{/* 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' && (
<div className="mt-3 flex justify-end">
<button
onClick={onReset}

View File

@@ -37,6 +37,8 @@ export function SettingsTableRow({
const isModified = setting.has_override;
const isBool = setting.type === 'bool';
const boolChecked = setting.current === true || setting.current === 'true';
// env-locked keys are pinned in .env and shadow the DB — show value, no input.
const locked = setting.read_only || setting.env_locked;
const isLongValue = (() => {
const val = String(setting.current ?? '');
@@ -84,14 +86,17 @@ export function SettingsTableRow({
</span>
)}
{setting.has_override && !setting.read_only && (
{setting.has_override && !locked && (
<span className="rounded-full bg-sky-500/20 px-1.5 py-0.5 text-[10px] font-medium leading-none text-sky-400">
{t('admin.settings.badgeDb')}
</span>
)}
{setting.read_only && (
<span className="flex items-center gap-0.5 rounded-full bg-warning-500/15 px-1.5 py-0.5 text-[10px] font-medium leading-none text-warning-400">
{setting.env_locked && (
<span
className="flex items-center gap-0.5 rounded-full bg-warning-500/15 px-1.5 py-0.5 text-[10px] font-medium leading-none text-warning-400"
title={t('admin.settings.envLockedHint')}
>
{t('admin.settings.badgeEnv')}
<LockIcon className="h-3 w-3" />
</span>
@@ -116,7 +121,7 @@ export function SettingsTableRow({
isLongValue ? 'w-full' : 'max-lg:self-end lg:flex-shrink-0',
)}
>
{setting.read_only ? (
{locked ? (
<span className="max-w-[240px] truncate rounded bg-dark-700/30 px-3 py-1.5 font-mono text-xs text-dark-400">
{isBool
? boolChecked
@@ -138,7 +143,7 @@ export function SettingsTableRow({
)}
{/* Reset button -- hover-reveal when has_override */}
{isModified && !setting.read_only && (
{isModified && !locked && (
<button
onClick={onReset}
disabled={isResetting}

View File

@@ -4,13 +4,12 @@ import { useQueryClient } from '@tanstack/react-query';
import { useBlockingStore } from '../../store/blocking';
import { useFocusTrap } from '../../hooks/useFocusTrap';
import { pingBackend, hasEverReachedBackend } from '../../api/health';
import { HEALTH } from '../../config/constants';
import { CloudWarningIcon, RestartIcon } from '@/components/icons';
import { Button } from '@/components/primitives';
import { cn } from '@/lib/utils';
import BlockingShell from './BlockingShell';
const POLL_INTERVAL_MS = 5000;
/**
* Full-screen state shown when the backend is unreachable (transport-level
* failure), replacing the blank loader the app used to get stuck on. Polls the
@@ -54,20 +53,24 @@ export default function ServiceUnavailableScreen() {
}
}, [recover]);
// Auto-recovery: probe immediately on mount, then every POLL_INTERVAL_MS.
// Auto-recovery: probe on mount, then re-schedule after each result resolves. Self-chaining
// (rather than a fixed setInterval) prevents the now-longer tolerant probe from overlapping
// with itself.
useEffect(() => {
let cancelled = false;
let timer: ReturnType<typeof setTimeout> | 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]);

View File

@@ -29,3 +29,17 @@ export const API = {
TRAFFIC_WARN_PERCENT: 70,
TRAFFIC_CRITICAL_PERCENT: 90,
} as const;
// Backend liveness probe (ServiceUnavailableScreen detection/recovery).
export const HEALTH = {
// Tolerant of cold mobile connections (radio wake + DNS + TLS + first byte). A hardcoded
// 5s here used to falsely flag slow devices as "service unavailable" while the real 30s
// API requests would have succeeded.
PROBE_TIMEOUT_MS: 12000,
// Re-probe before declaring the backend down so a single cold-connection blip can't blank
// the app. Total worst case to show the screen on a genuine outage stays under the API timeout.
CONFIRM_RETRIES: 1,
CONFIRM_RETRY_DELAY_MS: 1500,
// Recovery poll while the screen is shown — quick feedback once the backend answers again.
RECOVERY_POLL_INTERVAL_MS: 5000,
} as const;

View File

@@ -2000,6 +2000,8 @@
"quickToggles": "Quick Toggles",
"modified": "Modified",
"readOnly": "Read only",
"envLocked": "Set in .env",
"envLockedHint": "This value is set in the .env file and overrides the database, so it can't be changed here. To manage it from the cabinet, remove the key from .env and restart the bot.",
"badgeDb": "DB",
"badgeEnv": "ENV",
"reset": "Reset",

View File

@@ -1704,6 +1704,8 @@
"quickToggles": "تغییر سریع",
"modified": "تغییر یافته",
"readOnly": "فقط خواندنی",
"envLocked": "تنظیم‌شده در .env",
"envLockedHint": "این مقدار در فایل .env تنظیم شده و پایگاه‌داده را بازنویسی می‌کند، بنابراین اینجا قابل تغییر نیست. برای مدیریت آن از پنل، کلید را از .env حذف کنید و ربات را ری‌استارت کنید.",
"reset": "بازنشانی",
"categoriesCount": "دسته",
"settingsCount": "تنظیم",

View File

@@ -2027,6 +2027,8 @@
"quickToggles": "Быстрые переключатели",
"modified": "Изменено",
"readOnly": "Только чтение",
"envLocked": "Задано в .env",
"envLockedHint": "Значение задано в файле .env и перекрывает базу данных — здесь его изменить нельзя. Чтобы управлять настройкой из кабинета, удалите ключ из .env и перезапустите бота.",
"badgeDb": "БД",
"badgeEnv": "ENV",
"reset": "Сбросить",

View File

@@ -1745,6 +1745,8 @@
"quickToggles": "快速开关",
"modified": "已修改",
"readOnly": "只读",
"envLocked": "在 .env 中设置",
"envLockedHint": "该值在 .env 文件中设置并覆盖数据库,因此无法在此处更改。若要从面板管理此设置,请从 .env 中删除该键并重启机器人。",
"reset": "重置",
"categoriesCount": "个分类",
"settingsCount": "个设置",

View File

@@ -1,7 +1,7 @@
import { useState, useRef, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate, useParams, useSearchParams } from 'react-router';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { motion } from 'framer-motion';
import { balanceApi } from '../api/balance';
@@ -77,7 +77,6 @@ export default function TopUpAmount() {
const navigate = useNavigate();
const { methodId } = useParams<{ methodId: string }>();
const [searchParams] = useSearchParams();
const queryClient = useQueryClient();
const { formatAmount, currencySymbol, convertAmount, convertToRub, targetCurrency } =
useCurrency();
const { openInvoice, openTelegramLink, openLink, platform } = usePlatform();
@@ -89,9 +88,15 @@ export default function TopUpAmount() {
? parseFloat(searchParams.get('amount')!)
: undefined;
// Get method from cached payment-methods query
const cachedMethods = queryClient.getQueryData<PaymentMethod[]>(['payment-methods']);
const method = cachedMethods?.find((m) => m.id === methodId);
// Fetch payment methods with a real query (dedupes with the method-selection page and
// Balance via the shared ['payment-methods'] key). A non-reactive getQueryData read used
// to dead-end on an infinite spinner whenever the cache was cold — reload, browser-back
// from the provider page, or a deep link straight to this route.
const { data: methods, isLoading: isMethodsLoading } = useQuery({
queryKey: ['payment-methods'],
queryFn: balanceApi.getPaymentMethods,
});
const method = methods?.find((m) => m.id === methodId);
const handleNavigateBack = useCallback(() => {
navigate(-1);
@@ -138,10 +143,15 @@ export default function TopUpAmount() {
const [paymentUrl, setPaymentUrl] = useState<string | null>(null);
const [copied, setCopied] = useState(false);
const [isInputFocused, setIsInputFocused] = useState(false);
// Canonical RUB amount when the user picked a quick-amount chip. The input shows a
// rounded display-currency value; validating/charging the canonical RUB avoids the FX
// round-trip that could push a min-amount chip just below the allowed minimum. Cleared
// as soon as the user edits the field by hand.
const [quickRub, setQuickRub] = useState<number | null>(null);
// If method not found in cache, redirect to method selection
// Once methods have loaded, redirect to method selection if this method id is unknown.
useEffect(() => {
if (cachedMethods && !method) {
if (methods && !method) {
const params = new URLSearchParams();
const amount = searchParams.get('amount');
const rt = searchParams.get('returnTo');
@@ -150,7 +160,7 @@ export default function TopUpAmount() {
const qs = params.toString();
navigate(`/balance/top-up${qs ? `?${qs}` : ''}`, { replace: true });
}
}, [cachedMethods, method, navigate, searchParams]);
}, [methods, method, navigate, searchParams]);
useEffect(() => {
if (!method?.options || method.options.length === 0) {
@@ -277,7 +287,13 @@ export default function TopUpAmount() {
return () => clearTimeout(timer);
}, [platform]);
// Spinner only while methods are actually loading. Once the query has resolved without
// this method, the redirect effect above navigates away (so we render nothing here rather
// than spinning forever on a cold cache).
if (!method) {
if (!isMethodsLoading) {
return null;
}
return (
<div className="flex items-center justify-center py-12">
<div className="h-8 w-8 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
@@ -315,27 +331,43 @@ export default function TopUpAmount() {
return;
}
const amountRubles = convertToRub(amountCurrency);
if (amountRubles < minRubles || amountRubles > maxRubles) {
// Resolve the canonical RUB amount. Prefer an exact source — an unedited prefill or a
// quick-amount chip — over the display value, whose FX round-trip rounding (e.g. 150₽ at
// rate 90.66 → "1.65" USD → back to 149.59₽) could push a min selection just below the
// allowed minimum and block the top-up. quickRub is cleared on any manual edit, so a
// non-null value means the field still holds that chip's exact amount.
const userEditedAmount = amount.trim() !== initialDisplayAmount.trim();
const usingPrefill = !userEditedAmount && !!initialAmountRubles && initialAmountRubles > 0;
const usingQuick = quickRub !== null;
let canonicalRubles = amountRubles;
if (usingPrefill) {
canonicalRubles = initialAmountRubles as number;
} else if (usingQuick && quickRub !== null) {
canonicalRubles = quickRub;
} else if (targetCurrency !== 'RUB') {
// Hand-typed non-RUB amount: snap up to the minimum when it lands within one
// display-currency rounding step below it, so typing the advertised (rounded)
// minimum isn't rejected by FX rounding.
const decimals = targetCurrency === 'IRR' ? 0 : 2;
const roundingStep = convertToRub(Math.pow(10, -decimals));
if (canonicalRubles < minRubles && canonicalRubles >= minRubles - roundingStep) {
canonicalRubles = minRubles;
}
}
if (canonicalRubles < minRubles || canonicalRubles > maxRubles) {
setError(t('balance.errors.amountRange', { min: minRubles, max: maxRubles }));
return;
}
// Сохраняем canonical RUB amount если юзер НЕ редактировал префилл.
// Display-rounding в `.toFixed(2)` теряет точность: 150₽ при rate=90.66 → "1.65" USD
// (округление вниз с 1.6545), back-конвертация даёт 1.65 × 90.66 = 149.589₽ < 150₽
// → юзер не может купить подписку 150₽. С canonical RUB обходим FX round-trip.
//
// Math.ceil для не-RUB локалей покрывает остаточные sub-копеечные ошибки
// floating-point, когда юзер реально вводит свой amount.
const userEditedAmount = amount.trim() !== initialDisplayAmount.trim();
let amountKopeks: number;
if (!userEditedAmount && initialAmountRubles && initialAmountRubles > 0) {
amountKopeks = Math.round(initialAmountRubles * 100);
} else if (targetCurrency === 'RUB') {
amountKopeks = Math.round(amountRubles * 100);
} else {
amountKopeks = Math.ceil(amountRubles * 100);
}
// Round for exact sources; ceil a hand-typed amount so float noise never lands sub-kopeck
// under the chosen value.
const amountKopeks =
targetCurrency === 'RUB' || usingPrefill || usingQuick
? Math.round(canonicalRubles * 100)
: Math.ceil(canonicalRubles * 100);
if (isStarsMethod) {
starsPaymentMutation.mutate(amountKopeks);
} else {
@@ -442,7 +474,10 @@ export default function TopUpAmount() {
inputMode="decimal"
enterKeyHint="done"
value={amount}
onChange={(e) => setAmount(e.target.value)}
onChange={(e) => {
setAmount(e.target.value);
setQuickRub(null);
}}
onFocus={() => setIsInputFocused(true)}
onBlur={() => setIsInputFocused(false)}
onKeyDown={(e) => {
@@ -496,6 +531,7 @@ export default function TopUpAmount() {
type="button"
onClick={() => {
setAmount(val);
setQuickRub(a);
inputRef.current?.blur();
}}
hover