fix(oxibadge): show connection errors instead of failing silently
Check, format and test / build (push) Successful in 10m4s
Check, format and test / build (push) Successful in 10m4s
load_records/load_employees failures were only logged to the console and silently reset the UI to the month picker. Errors now flow through a shared store (src/lib/error.ts) into an ErrorBanner, reused by both Onboarding and ConfirmMonth, with the raw message passed straight through from Rust (no stable-code classification layer — these are mostly plain I/O errors, not worth categorizing). i18n stays grouped per screen as before, with error-related strings split into their own top-level "errors" group. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,27 +22,15 @@ async fn test_connection(host: String, port: u16) -> Result<(), String> {
|
||||
connect_with_retries(&host, port).await.map(|_| ())
|
||||
}
|
||||
|
||||
/// Connects to the device, trying up to 3 times, returning a stable error
|
||||
/// code (not a user-facing message) on failure so the frontend can
|
||||
/// translate it.
|
||||
/// Connects to the device, trying up to 3 times, returning the last error
|
||||
/// message as-is on failure.
|
||||
async fn connect_with_retries(host: &str, port: u16) -> Result<R701, String> {
|
||||
let mut last_error = String::new();
|
||||
for _ in 0..3 {
|
||||
let err = match R701::connect((host, port)).await {
|
||||
match R701::connect((host, port)).await {
|
||||
Ok(r701) => return Ok(r701),
|
||||
Err(err) => err,
|
||||
};
|
||||
last_error = match err.kind() {
|
||||
std::io::ErrorKind::ConnectionRefused => "connection-refused".to_string(),
|
||||
std::io::ErrorKind::TimedOut => "timed-out".to_string(),
|
||||
_ if err
|
||||
.to_string()
|
||||
.contains("failed to lookup address information") =>
|
||||
{
|
||||
"dns-failure".to_string()
|
||||
Err(err) => last_error = err.to_string(),
|
||||
}
|
||||
_ => "unknown".to_string(),
|
||||
};
|
||||
}
|
||||
Err(last_error)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
import { tick } from 'svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { monthName } from '$lib/month';
|
||||
import { error, clearError } from '$lib/error';
|
||||
import Column from './Column.svelte';
|
||||
import Intro from './Intro.svelte';
|
||||
import Button from './Button.svelte';
|
||||
import ErrorBanner from './ErrorBanner.svelte';
|
||||
|
||||
let { onstart }: { onstart: (selection: { year: number; month: number }) => void } = $props();
|
||||
|
||||
@@ -46,12 +48,14 @@
|
||||
if (isFuture(i)) return;
|
||||
month = i;
|
||||
open = null;
|
||||
clearError();
|
||||
}
|
||||
|
||||
function pickYear(y: number) {
|
||||
year = y;
|
||||
if (y === currentYear && month > currentMonth) month = currentMonth;
|
||||
open = null;
|
||||
clearError();
|
||||
}
|
||||
|
||||
function onYearScroll() {
|
||||
@@ -157,6 +161,10 @@
|
||||
</div>
|
||||
|
||||
<Button label={$_('confirm_month.button')} onclick={start} />
|
||||
|
||||
{#if $error}
|
||||
<ErrorBanner title={$_('errors.title')} message={$error} />
|
||||
{/if}
|
||||
</Column>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { error, setError, clearError } from '$lib/error';
|
||||
import Column from './Column.svelte';
|
||||
import Intro from './Intro.svelte';
|
||||
import Field from './Field.svelte';
|
||||
@@ -10,40 +11,29 @@
|
||||
|
||||
let { onconnected }: { onconnected: (cfg: { host: string; port: number }) => void } = $props();
|
||||
|
||||
type Phase = 'idle' | 'testing' | 'err';
|
||||
|
||||
let host = $state('');
|
||||
let port = $state(5005);
|
||||
let phase = $state<Phase>('idle');
|
||||
let errorCode = $state('');
|
||||
let testing = $state(false);
|
||||
|
||||
const trimmedHost = $derived(host.trim());
|
||||
const empty = $derived(trimmedHost === '');
|
||||
const disabled = $derived(empty || phase === 'testing');
|
||||
const disabled = $derived(empty || testing);
|
||||
const btnLabel = $derived(
|
||||
phase === 'testing'
|
||||
? $_('onboarding.button.testing')
|
||||
: phase === 'err'
|
||||
? $_('onboarding.button.retry')
|
||||
: $_('onboarding.button.idle')
|
||||
testing ? $_('onboarding.button.testing') : $error ? $_('onboarding.button.retry') : $_('onboarding.button.idle')
|
||||
);
|
||||
|
||||
function resetPhase() {
|
||||
phase = 'idle';
|
||||
errorCode = '';
|
||||
}
|
||||
|
||||
async function testAndSave() {
|
||||
if (disabled) return;
|
||||
phase = 'testing';
|
||||
errorCode = '';
|
||||
testing = true;
|
||||
clearError();
|
||||
try {
|
||||
await invoke('test_connection', { host: trimmedHost, port });
|
||||
await invoke('save_preferences', { host: trimmedHost, port });
|
||||
onconnected({ host: trimmedHost, port });
|
||||
} catch (err) {
|
||||
errorCode = String(err);
|
||||
phase = 'err';
|
||||
setError(err);
|
||||
} finally {
|
||||
testing = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -64,7 +54,7 @@
|
||||
label={$_('onboarding.host_label')}
|
||||
bind:value={host}
|
||||
placeholder="192.168.1.100"
|
||||
oninput={resetPhase}
|
||||
oninput={clearError}
|
||||
>
|
||||
{#snippet icon()}
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
|
||||
@@ -79,14 +69,14 @@
|
||||
bind:value={port}
|
||||
type="number"
|
||||
placeholder="5005"
|
||||
oninput={resetPhase}
|
||||
oninput={clearError}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type="submit" label={btnLabel} faded={empty} busy={phase === 'testing'} {disabled} />
|
||||
<Button type="submit" label={btnLabel} faded={empty} busy={testing} {disabled} />
|
||||
|
||||
{#if phase === 'err'}
|
||||
<ErrorBanner title={$_('onboarding.error_title')} message={$_(`onboarding.errors.${errorCode}`)} />
|
||||
{#if $error}
|
||||
<ErrorBanner title={$_('errors.title')} message={$error} />
|
||||
{/if}
|
||||
|
||||
<Hint text={$_('onboarding.hint')} />
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export const error = writable('');
|
||||
|
||||
export function setError(err: unknown) {
|
||||
error.set(String(err));
|
||||
}
|
||||
|
||||
export function clearError() {
|
||||
error.set('');
|
||||
}
|
||||
@@ -10,13 +10,6 @@
|
||||
"idle": "Test connection",
|
||||
"testing": "Connecting...",
|
||||
"retry": "Retry"
|
||||
},
|
||||
"error_title": "Could not reach the time clock",
|
||||
"errors": {
|
||||
"connection-refused": "Connection refused",
|
||||
"timed-out": "Connection timed out",
|
||||
"dns-failure": "Invalid or unreachable address",
|
||||
"unknown": "Connection error"
|
||||
}
|
||||
},
|
||||
"confirm_month": {
|
||||
@@ -38,6 +31,9 @@
|
||||
"title": "Fetching employees...",
|
||||
"description": "Reading the employee names registered on the time clock. Just a moment."
|
||||
},
|
||||
"errors": {
|
||||
"title": "Could not reach the time clock"
|
||||
},
|
||||
"done": {
|
||||
"message": "Nothing to do here."
|
||||
}
|
||||
|
||||
@@ -10,13 +10,6 @@
|
||||
"idle": "Testa connessione",
|
||||
"testing": "Connessione in corso...",
|
||||
"retry": "Riprova"
|
||||
},
|
||||
"error_title": "Impossibile raggiungere il timbratore",
|
||||
"errors": {
|
||||
"connection-refused": "Connessione rifiutata",
|
||||
"timed-out": "Connessione scaduta",
|
||||
"dns-failure": "Indirizzo non valido o non raggiungibile",
|
||||
"unknown": "Errore di connessione"
|
||||
}
|
||||
},
|
||||
"confirm_month": {
|
||||
@@ -38,6 +31,9 @@
|
||||
"title": "Recupero i dipendenti...",
|
||||
"description": "Sto leggendo i nomi dei dipendenti registrati sul timbratore. Ci vuole solo un attimo."
|
||||
},
|
||||
"errors": {
|
||||
"title": "Impossibile raggiungere il timbratore"
|
||||
},
|
||||
"done": {
|
||||
"message": "Non c'è niente da fare."
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { monthName } from '$lib/month';
|
||||
import { setError, clearError } from '$lib/error';
|
||||
import Screen from '$lib/components/Screen.svelte';
|
||||
import Onboarding from '$lib/components/Onboarding.svelte';
|
||||
import ConfirmMonth from '$lib/components/ConfirmMonth.svelte';
|
||||
@@ -25,13 +26,14 @@
|
||||
selectedYear = selection.year;
|
||||
selectedMonth = selection.month;
|
||||
step = 'loading-records';
|
||||
clearError();
|
||||
try {
|
||||
const records = await invoke('load_records', selection);
|
||||
step = 'loading-employees';
|
||||
await invoke('load_employees', { records });
|
||||
step = 'done';
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
setError(err);
|
||||
step = 'confirm-month';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user