diff --git a/frontend/e2e/persons.spec.ts b/frontend/e2e/persons.spec.ts
index 2fa49755..12f7d2f0 100644
--- a/frontend/e2e/persons.spec.ts
+++ b/frontend/e2e/persons.spec.ts
@@ -211,3 +211,84 @@ test.describe('Conversations', () => {
await page.screenshot({ path: 'test-results/e2e/conversations-sort.png' });
});
});
+
+test.describe('Conversations — enhancements', () => {
+ // Hans→Anna (1923) and Anna→Hans (1965) are seeded in DataInitializer
+ // Navigate directly by URL so the test doesn't rely on typeahead interaction
+ async function loadHansAnnaConversation(page: import('@playwright/test').Page) {
+ // Resolve person IDs from the persons list
+ await page.goto('/persons');
+ const hansLink = page.getByRole('link', { name: /Hans Müller/ }).first();
+ const hansHref = await hansLink.getAttribute('href');
+ const hansId = hansHref!.split('/').pop()!;
+
+ const annaLink = page.getByRole('link', { name: /Anna Schmidt/ }).first();
+ const annaHref = await annaLink.getAttribute('href');
+ const annaId = annaHref!.split('/').pop()!;
+
+ await page.goto(`/conversations?senderId=${hansId}&receiverId=${annaId}`);
+ await page.waitForURL(/senderId=/);
+ }
+
+ test('shows document count and year range summary when both persons are selected', async ({
+ page
+ }) => {
+ await loadHansAnnaConversation(page);
+ // Hans→Anna (1923) + Anna→Hans (1965) = 2 documents, range 1923–1965
+ await expect(page.getByTestId('conv-summary')).toContainText('2');
+ await expect(page.getByTestId('conv-summary')).toContainText('1923');
+ await expect(page.getByTestId('conv-summary')).toContainText('1965');
+ await page.screenshot({ path: 'test-results/e2e/conversations-summary.png' });
+ });
+
+ test('shows year dividers between documents from different years', async ({ page }) => {
+ await loadHansAnnaConversation(page);
+ // Expect at least two year dividers (1923 and 1965)
+ await expect(page.getByTestId('year-divider').first()).toBeVisible();
+ const dividers = page.getByTestId('year-divider');
+ const texts = await dividers.allTextContents();
+ expect(texts.some((t) => t.includes('1923'))).toBe(true);
+ expect(texts.some((t) => t.includes('1965'))).toBe(true);
+ await page.screenshot({ path: 'test-results/e2e/conversations-year-dividers.png' });
+ });
+
+ test('swap button switches sender and receiver and reloads', async ({ page }) => {
+ await loadHansAnnaConversation(page);
+ const url = new URL(page.url());
+ const originalSenderId = url.searchParams.get('senderId')!;
+ const originalReceiverId = url.searchParams.get('receiverId')!;
+
+ await page.getByTestId('conv-swap-btn').click();
+ await page.waitForURL(/senderId=/);
+
+ const swappedUrl = new URL(page.url());
+ expect(swappedUrl.searchParams.get('senderId')).toBe(originalReceiverId);
+ expect(swappedUrl.searchParams.get('receiverId')).toBe(originalSenderId);
+ await page.screenshot({ path: 'test-results/e2e/conversations-swap.png' });
+ });
+
+ test('shows "new document" link pre-filled with both persons when conversation is loaded', async ({
+ page
+ }) => {
+ await loadHansAnnaConversation(page);
+ const url = new URL(page.url());
+ const senderId = url.searchParams.get('senderId')!;
+ const receiverId = url.searchParams.get('receiverId')!;
+
+ const link = page.getByTestId('conv-new-doc-link');
+ await expect(link).toBeVisible();
+ const href = await link.getAttribute('href');
+ expect(href).toContain(`senderId=${senderId}`);
+ expect(href).toContain(`receiverId=${receiverId}`);
+ await page.screenshot({ path: 'test-results/e2e/conversations-new-doc-link.png' });
+ });
+
+ test('does not show swap button or new document link when only one person is selected', async ({
+ page
+ }) => {
+ await page.goto('/conversations');
+ await page.waitForURL('/conversations');
+ await expect(page.getByTestId('conv-swap-btn')).not.toBeVisible();
+ await expect(page.getByTestId('conv-new-doc-link')).not.toBeVisible();
+ });
+});
diff --git a/frontend/src/hooks.server.ts b/frontend/src/hooks.server.ts
index 78c5dae5..37d4823e 100644
--- a/frontend/src/hooks.server.ts
+++ b/frontend/src/hooks.server.ts
@@ -65,6 +65,12 @@ export const handleFetch: HandleFetch = async ({ event, request, fetch }) => {
const isApi = request.url.startsWith(apiUrl) || request.url.includes('/api/');
if (isApi) {
+ // If the request already carries an explicit Authorization header (e.g. the
+ // login action sends Basic auth), pass it through unchanged.
+ if (request.headers.has('Authorization')) {
+ return fetch(request);
+ }
+
const token = event.cookies.get('auth_token');
if (!token) {
diff --git a/frontend/src/lib/components/PersonMultiSelect.svelte.spec.ts b/frontend/src/lib/components/PersonMultiSelect.svelte.spec.ts
index 1209538f..1a132943 100644
--- a/frontend/src/lib/components/PersonMultiSelect.svelte.spec.ts
+++ b/frontend/src/lib/components/PersonMultiSelect.svelte.spec.ts
@@ -1,5 +1,5 @@
import { describe, expect, it, vi, afterEach } from 'vitest';
-import { render } from 'vitest-browser-svelte';
+import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import PersonMultiSelect from './PersonMultiSelect.svelte';
@@ -29,6 +29,7 @@ function receiverInputs() {
}
afterEach(() => {
+ cleanup();
vi.unstubAllGlobals();
});
diff --git a/frontend/src/lib/components/PersonTypeahead.svelte b/frontend/src/lib/components/PersonTypeahead.svelte
index fb9a54dc..2ff22708 100644
--- a/frontend/src/lib/components/PersonTypeahead.svelte
+++ b/frontend/src/lib/components/PersonTypeahead.svelte
@@ -1,4 +1,5 @@
-