Improve backend error handling, fix lean logo on login screen

This commit is contained in:
2026-05-01 17:40:47 +12:00
parent 38a619b4bd
commit 2799946091
8 changed files with 81 additions and 55 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "data-entry-app-backend" name = "data-entry-app-backend"
version = "0.1.3" version = "0.1.4"
description = "Costing platform MVP backend" description = "Costing platform MVP backend"
requires-python = ">=3.11" requires-python = ">=3.11"
dependencies = [ dependencies = [
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "data-entry-app-frontend", "name": "data-entry-app-frontend",
"version": "0.1.2", "version": "0.1.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "data-entry-app-frontend", "name": "data-entry-app-frontend",
"version": "0.1.2", "version": "0.1.4",
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.2.0", "@sveltejs/adapter-auto": "^3.2.0",
"@sveltejs/adapter-node": "^5.2.12", "@sveltejs/adapter-node": "^5.2.12",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "data-entry-app-frontend", "name": "data-entry-app-frontend",
"version": "0.1.3", "version": "0.1.4",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+20
View File
@@ -57,4 +57,24 @@ describe('api fetch injection', () => {
expect(injectedFetch.mock.calls[0]?.[0]).toBe(`http://127.0.0.1:8000${path}`); expect(injectedFetch.mock.calls[0]?.[0]).toBe(`http://127.0.0.1:8000${path}`);
expect(globalFetch).not.toHaveBeenCalled(); expect(globalFetch).not.toHaveBeenCalled();
}); });
it('shows a backend-unavailable message for login network failures', async () => {
globalThis.fetch = vi.fn(async () => {
throw new TypeError('Failed to fetch');
}) as typeof fetch;
await expect(api.clientLogin('user@example.com', 'secret')).rejects.toThrow(
'Unable to reach the server. Check that the backend is running and try again.'
);
});
it('shows a backend-unavailable message for authenticated read network failures', async () => {
const injectedFetch = vi.fn(async () => {
throw new TypeError('NetworkError when attempting to fetch resource.');
}) as typeof fetch;
await expect(api.rawMaterials(injectedFetch)).rejects.toThrow(
'Unable to reach the server. Check that the backend is running and try again.'
);
});
}); });
+24 -1
View File
@@ -37,6 +37,7 @@ import type {
import { getStoredAdminSession, getStoredClientSession } from '$lib/session'; import { getStoredAdminSession, getStoredClientSession } from '$lib/session';
const DEFAULT_API_PORT = env.PUBLIC_API_PORT || '8000'; const DEFAULT_API_PORT = env.PUBLIC_API_PORT || '8000';
const BACKEND_UNAVAILABLE_MESSAGE = 'Unable to reach the server. Check that the backend is running and try again.';
type AuthMode = 'none' | 'client' | 'admin' | 'manager'; type AuthMode = 'none' | 'client' | 'admin' | 'manager';
type ApiFetch = typeof fetch; type ApiFetch = typeof fetch;
@@ -85,6 +86,24 @@ function getToken(auth: AuthMode) {
return null; return null;
} }
function normalizeRequestError(error: unknown) {
if (error instanceof Error) {
const message = error.message.trim();
const isNetworkFetchFailure =
/failed to fetch|fetch failed|networkerror when attempting to fetch resource|load failed|network request failed/i.test(
message
) || error.name === 'NetworkError';
if (isNetworkFetchFailure) {
return new Error(BACKEND_UNAVAILABLE_MESSAGE);
}
return error;
}
return new Error('An unexpected error occurred while contacting the server.');
}
async function fetchJson<T>(path: string, fallback: T, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise<T> { async function fetchJson<T>(path: string, fallback: T, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise<T> {
try { try {
const token = getToken(auth); const token = getToken(auth);
@@ -100,7 +119,7 @@ async function fetchJson<T>(path: string, fallback: T, auth: AuthMode = 'none',
return (await response.json()) as T; return (await response.json()) as T;
} catch (error) { } catch (error) {
if (auth !== 'none') { if (auth !== 'none') {
throw error; throw normalizeRequestError(error);
} }
return fallback; return fallback;
} }
@@ -112,6 +131,7 @@ async function request<T>(
auth: AuthMode = 'none', auth: AuthMode = 'none',
fetcher: ApiFetch = fetch fetcher: ApiFetch = fetch
): Promise<T> { ): Promise<T> {
try {
const token = getToken(auth); const token = getToken(auth);
const response = await fetcher(buildApiUrl(path), { const response = await fetcher(buildApiUrl(path), {
headers: { headers: {
@@ -136,6 +156,9 @@ async function request<T>(
} }
return (await response.json()) as T; return (await response.json()) as T;
} catch (error) {
throw normalizeRequestError(error);
}
} }
export const api = { export const api = {
+14 -31
View File
@@ -1,6 +1,5 @@
<script lang="ts"> <script lang="ts">
import { api } from '$lib/api'; import { api } from '$lib/api';
import Lean101Logo from '$lib/components/Lean101Logo.svelte';
import { clientSession, sessionHydrated } from '$lib/session'; import { clientSession, sessionHydrated } from '$lib/session';
import type { Mix, ProductCostBreakdown, RawMaterial } from '$lib/types'; import type { Mix, ProductCostBreakdown, RawMaterial } from '$lib/types';
import packageInfo from '../../package.json'; import packageInfo from '../../package.json';
@@ -28,8 +27,8 @@
}; };
let { data } = $props(); let { data } = $props();
let email = $state('operator@example.com'); let email = $state('');
let password = $state('changeme'); let password = $state('');
let isLoggingIn = $state(false); let isLoggingIn = $state(false);
let loginError = $state(''); let loginError = $state('');
let passwordInput: HTMLInputElement | null = null; let passwordInput: HTMLInputElement | null = null;
@@ -304,7 +303,7 @@
<div class="auth-card auth-card-loading"> <div class="auth-card auth-card-loading">
<div class="auth-header"> <div class="auth-header">
<div class="client-logo-block"> <div class="client-logo-block">
<Lean101Logo className="hero-logo" /> <img class="hero-login-logo" src="/lean101-login-logo.png" alt="Lean 101" />
<div class="client-logo-copy"> <div class="client-logo-copy">
<p class="eyebrow">Client Workspace</p> <p class="eyebrow">Client Workspace</p>
<strong>Hunter Premium Produce</strong> <strong>Hunter Premium Produce</strong>
@@ -328,7 +327,7 @@
<div class="auth-footer"> <div class="auth-footer">
<div class="lean-brand"> <div class="lean-brand">
<Lean101Logo className="footer-logo" showTagline={false} /> <img class="footer-login-logo" src="/lean101-login-logo.png" alt="Lean 101" />
</div> </div>
<div class="auth-meta"> <div class="auth-meta">
<span class="version-badge"> <span class="version-badge">
@@ -345,7 +344,7 @@
<div class="auth-card auth-card-login"> <div class="auth-card auth-card-login">
<div class="auth-header"> <div class="auth-header">
<div class="client-logo-block"> <div class="client-logo-block">
<Lean101Logo className="hero-logo" /> <img class="hero-login-logo" src="/lean101-login-logo.png" alt="Lean 101" />
<div class="client-logo-copy"> <div class="client-logo-copy">
<p class="eyebrow">Client Sign-In</p> <p class="eyebrow">Client Sign-In</p>
<strong>Hunter Premium Produce</strong> <strong>Hunter Premium Produce</strong>
@@ -365,7 +364,7 @@
<form class="signin-form auth-form" onsubmit={handleLogin}> <form class="signin-form auth-form" onsubmit={handleLogin}>
<label class="field"> <label class="field">
<span>Email</span> <span>Email</span>
<input bind:value={email} type="email" autocomplete="username" placeholder="Email" /> <input bind:value={email} type="email" autocomplete="username" placeholder="Email" autofocus />
</label> </label>
<label class="field field-password" class:is-invalid={Boolean(loginError)}> <label class="field field-password" class:is-invalid={Boolean(loginError)}>
@@ -390,7 +389,7 @@
<div class="auth-footer"> <div class="auth-footer">
<div class="lean-brand"> <div class="lean-brand">
<Lean101Logo className="footer-logo" showTagline={false} /> <img class="footer-login-logo" src="/lean101-login-logo.png" alt="Lean 101" />
</div> </div>
<div class="auth-meta"> <div class="auth-meta">
<span class="version-badge"> <span class="version-badge">
@@ -798,20 +797,16 @@
font-size: 0.88rem; font-size: 0.88rem;
} }
:global(.hero-logo) { .hero-login-logo {
width: min(100%, 19rem); width: min(100%, 19rem);
height: auto;
display: block;
} }
:global(.hero-logo .logo-mark) { .footer-login-logo {
width: 4.8rem; width: 9.8rem;
} height: auto;
display: block;
:global(.hero-logo .logo-copy strong) {
font-size: clamp(2rem, 4.6vw, 3.2rem);
}
:global(.hero-logo .logo-copy small) {
font-size: 0.64rem;
} }
.auth-status-pill { .auth-status-pill {
@@ -962,18 +957,6 @@
align-items: center; align-items: center;
} }
:global(.footer-logo) {
width: 9.8rem;
}
:global(.footer-logo .logo-mark) {
width: 2rem;
}
:global(.footer-logo .logo-copy strong) {
font-size: 1.15rem;
}
.auth-meta { .auth-meta {
display: grid; display: grid;
justify-items: end; justify-items: end;
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB