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(|_| ())
|
connect_with_retries(&host, port).await.map(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Connects to the device, trying up to 3 times, returning a stable error
|
/// Connects to the device, trying up to 3 times, returning the last error
|
||||||
/// code (not a user-facing message) on failure so the frontend can
|
/// message as-is on failure.
|
||||||
/// translate it.
|
|
||||||
async fn connect_with_retries(host: &str, port: u16) -> Result<R701, String> {
|
async fn connect_with_retries(host: &str, port: u16) -> Result<R701, String> {
|
||||||
let mut last_error = String::new();
|
let mut last_error = String::new();
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
let err = match R701::connect((host, port)).await {
|
match R701::connect((host, port)).await {
|
||||||
Ok(r701) => return Ok(r701),
|
Ok(r701) => return Ok(r701),
|
||||||
Err(err) => err,
|
Err(err) => last_error = err.to_string(),
|
||||||
};
|
}
|
||||||
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()
|
|
||||||
}
|
|
||||||
_ => "unknown".to_string(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
Err(last_error)
|
Err(last_error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,11 @@
|
|||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { monthName } from '$lib/month';
|
import { monthName } from '$lib/month';
|
||||||
|
import { error, clearError } from '$lib/error';
|
||||||
import Column from './Column.svelte';
|
import Column from './Column.svelte';
|
||||||
import Intro from './Intro.svelte';
|
import Intro from './Intro.svelte';
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
|
import ErrorBanner from './ErrorBanner.svelte';
|
||||||
|
|
||||||
let { onstart }: { onstart: (selection: { year: number; month: number }) => void } = $props();
|
let { onstart }: { onstart: (selection: { year: number; month: number }) => void } = $props();
|
||||||
|
|
||||||
@@ -46,12 +48,14 @@
|
|||||||
if (isFuture(i)) return;
|
if (isFuture(i)) return;
|
||||||
month = i;
|
month = i;
|
||||||
open = null;
|
open = null;
|
||||||
|
clearError();
|
||||||
}
|
}
|
||||||
|
|
||||||
function pickYear(y: number) {
|
function pickYear(y: number) {
|
||||||
year = y;
|
year = y;
|
||||||
if (y === currentYear && month > currentMonth) month = currentMonth;
|
if (y === currentYear && month > currentMonth) month = currentMonth;
|
||||||
open = null;
|
open = null;
|
||||||
|
clearError();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onYearScroll() {
|
function onYearScroll() {
|
||||||
@@ -157,6 +161,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button label={$_('confirm_month.button')} onclick={start} />
|
<Button label={$_('confirm_month.button')} onclick={start} />
|
||||||
|
|
||||||
|
{#if $error}
|
||||||
|
<ErrorBanner title={$_('errors.title')} message={$error} />
|
||||||
|
{/if}
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { _ } from 'svelte-i18n';
|
import { _ } from 'svelte-i18n';
|
||||||
|
import { error, setError, clearError } from '$lib/error';
|
||||||
import Column from './Column.svelte';
|
import Column from './Column.svelte';
|
||||||
import Intro from './Intro.svelte';
|
import Intro from './Intro.svelte';
|
||||||
import Field from './Field.svelte';
|
import Field from './Field.svelte';
|
||||||
@@ -10,40 +11,29 @@
|
|||||||
|
|
||||||
let { onconnected }: { onconnected: (cfg: { host: string; port: number }) => void } = $props();
|
let { onconnected }: { onconnected: (cfg: { host: string; port: number }) => void } = $props();
|
||||||
|
|
||||||
type Phase = 'idle' | 'testing' | 'err';
|
|
||||||
|
|
||||||
let host = $state('');
|
let host = $state('');
|
||||||
let port = $state(5005);
|
let port = $state(5005);
|
||||||
let phase = $state<Phase>('idle');
|
let testing = $state(false);
|
||||||
let errorCode = $state('');
|
|
||||||
|
|
||||||
const trimmedHost = $derived(host.trim());
|
const trimmedHost = $derived(host.trim());
|
||||||
const empty = $derived(trimmedHost === '');
|
const empty = $derived(trimmedHost === '');
|
||||||
const disabled = $derived(empty || phase === 'testing');
|
const disabled = $derived(empty || testing);
|
||||||
const btnLabel = $derived(
|
const btnLabel = $derived(
|
||||||
phase === 'testing'
|
testing ? $_('onboarding.button.testing') : $error ? $_('onboarding.button.retry') : $_('onboarding.button.idle')
|
||||||
? $_('onboarding.button.testing')
|
|
||||||
: phase === 'err'
|
|
||||||
? $_('onboarding.button.retry')
|
|
||||||
: $_('onboarding.button.idle')
|
|
||||||
);
|
);
|
||||||
|
|
||||||
function resetPhase() {
|
|
||||||
phase = 'idle';
|
|
||||||
errorCode = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
async function testAndSave() {
|
async function testAndSave() {
|
||||||
if (disabled) return;
|
if (disabled) return;
|
||||||
phase = 'testing';
|
testing = true;
|
||||||
errorCode = '';
|
clearError();
|
||||||
try {
|
try {
|
||||||
await invoke('test_connection', { host: trimmedHost, port });
|
await invoke('test_connection', { host: trimmedHost, port });
|
||||||
await invoke('save_preferences', { host: trimmedHost, port });
|
await invoke('save_preferences', { host: trimmedHost, port });
|
||||||
onconnected({ host: trimmedHost, port });
|
onconnected({ host: trimmedHost, port });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorCode = String(err);
|
setError(err);
|
||||||
phase = 'err';
|
} finally {
|
||||||
|
testing = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -64,7 +54,7 @@
|
|||||||
label={$_('onboarding.host_label')}
|
label={$_('onboarding.host_label')}
|
||||||
bind:value={host}
|
bind:value={host}
|
||||||
placeholder="192.168.1.100"
|
placeholder="192.168.1.100"
|
||||||
oninput={resetPhase}
|
oninput={clearError}
|
||||||
>
|
>
|
||||||
{#snippet icon()}
|
{#snippet icon()}
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
|
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
@@ -79,14 +69,14 @@
|
|||||||
bind:value={port}
|
bind:value={port}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="5005"
|
placeholder="5005"
|
||||||
oninput={resetPhase}
|
oninput={clearError}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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'}
|
{#if $error}
|
||||||
<ErrorBanner title={$_('onboarding.error_title')} message={$_(`onboarding.errors.${errorCode}`)} />
|
<ErrorBanner title={$_('errors.title')} message={$error} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Hint text={$_('onboarding.hint')} />
|
<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",
|
"idle": "Test connection",
|
||||||
"testing": "Connecting...",
|
"testing": "Connecting...",
|
||||||
"retry": "Retry"
|
"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": {
|
"confirm_month": {
|
||||||
@@ -38,6 +31,9 @@
|
|||||||
"title": "Fetching employees...",
|
"title": "Fetching employees...",
|
||||||
"description": "Reading the employee names registered on the time clock. Just a moment."
|
"description": "Reading the employee names registered on the time clock. Just a moment."
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"title": "Could not reach the time clock"
|
||||||
|
},
|
||||||
"done": {
|
"done": {
|
||||||
"message": "Nothing to do here."
|
"message": "Nothing to do here."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,6 @@
|
|||||||
"idle": "Testa connessione",
|
"idle": "Testa connessione",
|
||||||
"testing": "Connessione in corso...",
|
"testing": "Connessione in corso...",
|
||||||
"retry": "Riprova"
|
"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": {
|
"confirm_month": {
|
||||||
@@ -38,6 +31,9 @@
|
|||||||
"title": "Recupero i dipendenti...",
|
"title": "Recupero i dipendenti...",
|
||||||
"description": "Sto leggendo i nomi dei dipendenti registrati sul timbratore. Ci vuole solo un attimo."
|
"description": "Sto leggendo i nomi dei dipendenti registrati sul timbratore. Ci vuole solo un attimo."
|
||||||
},
|
},
|
||||||
|
"errors": {
|
||||||
|
"title": "Impossibile raggiungere il timbratore"
|
||||||
|
},
|
||||||
"done": {
|
"done": {
|
||||||
"message": "Non c'è niente da fare."
|
"message": "Non c'è niente da fare."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { _, locale } from 'svelte-i18n';
|
import { _, locale } from 'svelte-i18n';
|
||||||
import { monthName } from '$lib/month';
|
import { monthName } from '$lib/month';
|
||||||
|
import { setError, clearError } from '$lib/error';
|
||||||
import Screen from '$lib/components/Screen.svelte';
|
import Screen from '$lib/components/Screen.svelte';
|
||||||
import Onboarding from '$lib/components/Onboarding.svelte';
|
import Onboarding from '$lib/components/Onboarding.svelte';
|
||||||
import ConfirmMonth from '$lib/components/ConfirmMonth.svelte';
|
import ConfirmMonth from '$lib/components/ConfirmMonth.svelte';
|
||||||
@@ -25,13 +26,14 @@
|
|||||||
selectedYear = selection.year;
|
selectedYear = selection.year;
|
||||||
selectedMonth = selection.month;
|
selectedMonth = selection.month;
|
||||||
step = 'loading-records';
|
step = 'loading-records';
|
||||||
|
clearError();
|
||||||
try {
|
try {
|
||||||
const records = await invoke('load_records', selection);
|
const records = await invoke('load_records', selection);
|
||||||
step = 'loading-employees';
|
step = 'loading-employees';
|
||||||
await invoke('load_employees', { records });
|
await invoke('load_employees', { records });
|
||||||
step = 'done';
|
step = 'done';
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
setError(err);
|
||||||
step = 'confirm-month';
|
step = 'confirm-month';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user