diff --git a/Imagotipo-azul.png b/Imagotipo-azul.png new file mode 100644 index 0000000..2c6ca39 Binary files /dev/null and b/Imagotipo-azul.png differ diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 29ffa8d..f08a14c 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "data-entry-app-backend" -version = "0.1.3" +version = "0.1.4" description = "Costing platform MVP backend" requires-python = ">=3.11" dependencies = [ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ef4e4e2..f90791d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "data-entry-app-frontend", - "version": "0.1.2", + "version": "0.1.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "data-entry-app-frontend", - "version": "0.1.2", + "version": "0.1.4", "devDependencies": { "@sveltejs/adapter-auto": "^3.2.0", "@sveltejs/adapter-node": "^5.2.12", diff --git a/frontend/package.json b/frontend/package.json index d736a83..cc0fcde 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "data-entry-app-frontend", - "version": "0.1.3", + "version": "0.1.4", "private": true, "type": "module", "scripts": { diff --git a/frontend/src/lib/api.test.ts b/frontend/src/lib/api.test.ts index 9838f3a..f474da7 100644 --- a/frontend/src/lib/api.test.ts +++ b/frontend/src/lib/api.test.ts @@ -57,4 +57,24 @@ describe('api fetch injection', () => { expect(injectedFetch.mock.calls[0]?.[0]).toBe(`http://127.0.0.1:8000${path}`); 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.' + ); + }); }); diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 274ffee..69ae4c5 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -37,6 +37,7 @@ import type { import { getStoredAdminSession, getStoredClientSession } from '$lib/session'; 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 ApiFetch = typeof fetch; @@ -85,6 +86,24 @@ function getToken(auth: AuthMode) { 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(path: string, fallback: T, auth: AuthMode = 'none', fetcher: ApiFetch = fetch): Promise { try { const token = getToken(auth); @@ -100,7 +119,7 @@ async function fetchJson(path: string, fallback: T, auth: AuthMode = 'none', return (await response.json()) as T; } catch (error) { if (auth !== 'none') { - throw error; + throw normalizeRequestError(error); } return fallback; } @@ -112,30 +131,34 @@ async function request( auth: AuthMode = 'none', fetcher: ApiFetch = fetch ): Promise { - const token = getToken(auth); - const response = await fetcher(buildApiUrl(path), { - headers: { - 'Content-Type': 'application/json', - ...(token ? { Authorization: `Bearer ${token}` } : {}), - ...(options.headers ?? {}) - }, - ...options - }); + try { + const token = getToken(auth); + const response = await fetcher(buildApiUrl(path), { + headers: { + 'Content-Type': 'application/json', + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(options.headers ?? {}) + }, + ...options + }); - if (!response.ok) { - let message = 'Request failed'; + if (!response.ok) { + let message = 'Request failed'; - try { - const body = (await response.json()) as { detail?: string }; - message = body.detail ?? message; - } catch { - message = response.statusText || message; + try { + const body = (await response.json()) as { detail?: string }; + message = body.detail ?? message; + } catch { + message = response.statusText || message; + } + + throw new Error(message); } - throw new Error(message); + return (await response.json()) as T; + } catch (error) { + throw normalizeRequestError(error); } - - return (await response.json()) as T; } export const api = { diff --git a/frontend/src/routes/+page.svelte b/frontend/src/routes/+page.svelte index 0d6734d..805999e 100644 --- a/frontend/src/routes/+page.svelte +++ b/frontend/src/routes/+page.svelte @@ -1,6 +1,5 @@