Improve backend error handling, fix lean logo on login screen
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -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 = [
|
||||||
|
|||||||
Generated
+2
-2
@@ -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,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": {
|
||||||
|
|||||||
@@ -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
@@ -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 = {
|
||||||
|
|||||||
@@ -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 |
Reference in New Issue
Block a user