mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,9 +116,13 @@ export async function reportPossibleBackendDown(): Promise<void> {
|
||||
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<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) {
|
||||
useBlockingStore.getState().setBackendUnavailable();
|
||||
if (await confirmBackendDown()) {
|
||||
if (!everReachedBackend && useBlockingStore.getState().blockingType === null) {
|
||||
useBlockingStore.getState().setBackendUnavailable();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user