diff --git a/app/static/app.js b/app/static/app.js index 00f3534..ef788f5 100644 --- a/app/static/app.js +++ b/app/static/app.js @@ -51,7 +51,13 @@ async function call(method, path, body) { clearTimeout(timeoutId); } const data = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(apiErrorMessage(data.detail) || res.statusText); + if (!res.ok) { + // A token the server no longer accepts can't be recovered from by retrying: + // without this every poll keeps failing against a dashboard that still looks + // logged in, toasting "session expired" forever. Drop back to the login form. + if (res.status === 401 && data.detail?.code === 'session_expired' && token) logout(); + throw new Error(apiErrorMessage(data.detail) || res.statusText); + } return data; } @@ -62,6 +68,11 @@ async function call(method, path, body) { function apiErrorMessage(detail) { if (!detail) return null; if (typeof detail === 'string') return detail; // endpoints still returning a bare string + // FastAPI's own request-validation failures (422) use a list of field errors + // instead, in English and phrased for an API client ("Input should be a valid + // integer"). Nothing here can act on which field it was, so say the one useful + // thing — the request was malformed — in the user's language. + if (Array.isArray(detail)) return t('error.invalid_request'); return tOrNull('error.' + detail.code, errorParams(detail.params)) || detail.message || null; } @@ -702,9 +713,15 @@ async function withdraw() { const btn = document.getElementById('withdraw-btn'); const ext = document.getElementById('wd-address').value; const isFullAmount = document.getElementById('wd-full-amount').checked; - const amtSats = isFullAmount - ? myBalanceSats - : Math.round(parseFloat(document.getElementById('wd-amount').value) * SATS_PER_PLM); + const amount = parseFloat(document.getElementById('wd-amount').value); + // Caught here rather than left to the server: an empty or non-numeric field + // parses to NaN, which JSON.stringify sends as null, which comes back as a + // 422 whose only readable text is an English HTTP status line. + if (!isFullAmount && !(amount > 0)) { + toast(t('error.invalid_amount'), 'error'); + return; + } + const amtSats = isFullAmount ? myBalanceSats : Math.round(amount * SATS_PER_PLM); await withLoading(btn, t('loading.sending'), async () => { try { await call('POST', '/withdrawals', { external_address: ext, amount_sats: amtSats }); diff --git a/app/static/i18n.js b/app/static/i18n.js index 6dbe1ad..14fbdc0 100644 --- a/app/static/i18n.js +++ b/app/static/i18n.js @@ -130,6 +130,8 @@ const TRANSLATIONS = { 'error.invalid_credentials': 'Wrong username or password.', 'error.derivation_index_conflict': 'Registration failed, please try again.', 'error.session_expired': 'Session expired, please log in again.', + 'error.invalid_request': 'Invalid request, please check the entered data.', + 'error.invalid_amount': 'Enter an amount greater than zero.', 'loading.creating': 'Creating…', 'loading.loggingIn': 'Logging in…', @@ -260,6 +262,8 @@ const TRANSLATIONS = { 'error.invalid_credentials': 'Username o password errati.', 'error.derivation_index_conflict': 'Registrazione non riuscita, riprova.', 'error.session_expired': 'Sessione scaduta, accedi di nuovo.', + 'error.invalid_request': 'Richiesta non valida, controlla i dati inseriti.', + 'error.invalid_amount': 'Inserisci un importo maggiore di zero.', 'loading.creating': 'Creazione…', 'loading.loggingIn': 'Accesso…', @@ -390,6 +394,8 @@ const TRANSLATIONS = { 'error.invalid_credentials': 'Usuario o contraseña incorrectos.', 'error.derivation_index_conflict': 'No se pudo completar el registro, inténtalo de nuevo.', 'error.session_expired': 'Sesión caducada, vuelve a iniciar sesión.', + 'error.invalid_request': 'Solicitud no válida, revisa los datos introducidos.', + 'error.invalid_amount': 'Introduce un importe mayor que cero.', 'loading.creating': 'Creando…', 'loading.loggingIn': 'Entrando…', @@ -520,6 +526,8 @@ const TRANSLATIONS = { 'error.invalid_credentials': "Nom d'utilisateur ou mot de passe incorrect.", 'error.derivation_index_conflict': "L'inscription a échoué, veuillez réessayer.", 'error.session_expired': 'Session expirée, veuillez vous reconnecter.', + 'error.invalid_request': 'Requête invalide, vérifiez les données saisies.', + 'error.invalid_amount': 'Saisissez un montant supérieur à zéro.', 'loading.creating': 'Création…', 'loading.loggingIn': 'Connexion…', @@ -650,6 +658,8 @@ const TRANSLATIONS = { 'error.invalid_credentials': 'Benutzername oder Passwort falsch.', 'error.derivation_index_conflict': 'Registrierung fehlgeschlagen, bitte erneut versuchen.', 'error.session_expired': 'Sitzung abgelaufen, bitte melde dich erneut an.', + 'error.invalid_request': 'Ungültige Anfrage, bitte überprüfe die eingegebenen Daten.', + 'error.invalid_amount': 'Gib einen Betrag größer als null ein.', 'loading.creating': 'Wird erstellt…', 'loading.loggingIn': 'Anmeldung…', @@ -780,6 +790,8 @@ const TRANSLATIONS = { 'error.invalid_credentials': 'Неверное имя пользователя или пароль.', 'error.derivation_index_conflict': 'Не удалось завершить регистрацию, попробуйте ещё раз.', 'error.session_expired': 'Сессия истекла, войдите снова.', + 'error.invalid_request': 'Некорректный запрос, проверьте введённые данные.', + 'error.invalid_amount': 'Введите сумму больше нуля.', 'loading.creating': 'Создание…', 'loading.loggingIn': 'Вход…', @@ -910,6 +922,8 @@ const TRANSLATIONS = { 'error.invalid_credentials': '用户名或密码错误。', 'error.derivation_index_conflict': '注册失败,请重试。', 'error.session_expired': '会话已过期,请重新登录。', + 'error.invalid_request': '请求无效,请检查填写的内容。', + 'error.invalid_amount': '请输入大于零的金额。', 'loading.creating': '正在创建…', 'loading.loggingIn': '正在登录…',