diff --git a/web/src/lib/components/ApiErrorBanner.svelte b/web/src/lib/components/ApiErrorBanner.svelte
new file mode 100644
index 00000000..3bf9399b
--- /dev/null
+++ b/web/src/lib/components/ApiErrorBanner.svelte
@@ -0,0 +1,22 @@
+
+
+
+
{message}
+
+
diff --git a/web/src/lib/components/ApiErrorBanner.test.ts b/web/src/lib/components/ApiErrorBanner.test.ts
new file mode 100644
index 00000000..299405b2
--- /dev/null
+++ b/web/src/lib/components/ApiErrorBanner.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, test, vi } from 'vitest';
+import { render, screen, fireEvent } from '@testing-library/svelte';
+import ApiErrorBanner from './ApiErrorBanner.svelte';
+
+describe('ApiErrorBanner', () => {
+ test('renders error.message when present', () => {
+ render(ApiErrorBanner, {
+ props: {
+ error: { code: 'server_error', message: 'database down', status: 500 },
+ onRetry: () => {}
+ }
+ });
+ expect(screen.getByRole('alert')).toHaveTextContent('database down');
+ });
+
+ test('falls back to generic text when error lacks message', () => {
+ render(ApiErrorBanner, { props: { error: undefined, onRetry: () => {} } });
+ expect(screen.getByRole('alert')).toHaveTextContent(/something went wrong/i);
+ });
+
+ test('clicking the button calls onRetry', async () => {
+ const onRetry = vi.fn();
+ render(ApiErrorBanner, {
+ props: {
+ error: { code: 'server_error', message: 'x', status: 500 },
+ onRetry
+ }
+ });
+ await fireEvent.click(screen.getByRole('button', { name: /try again/i }));
+ expect(onRetry).toHaveBeenCalledTimes(1);
+ });
+});