From bf90427bfa0bf0c566c23b06922d400ee2b18d58 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 1 Jun 2026 20:03:43 +0200 Subject: [PATCH 1/4] feat(document): drop the read-only Originaltext field from the edit form The "Originaltext:" line in WhoWhenSection rendered the verbatim import cell (metaDateRaw) as static text plus a hidden input that re-submitted it on every save. Editors mistook it for an editable field. Remove the visible line, the hidden round-trip input, and the now-unused rawDate prop (here and at the DocumentEditLayout call site). The backend's partial update preserves the stored value, so no data is lost. Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/document/DocumentEditLayout.svelte | 1 - frontend/src/lib/document/WhoWhenSection.svelte | 11 ----------- .../src/lib/document/WhoWhenSection.svelte.test.ts | 13 ++++++------- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/frontend/src/lib/document/DocumentEditLayout.svelte b/frontend/src/lib/document/DocumentEditLayout.svelte index c32887c6..2b72e58c 100644 --- a/frontend/src/lib/document/DocumentEditLayout.svelte +++ b/frontend/src/lib/document/DocumentEditLayout.svelte @@ -209,7 +209,6 @@ async function handleReplaceFile(e: Event) { bind:dateIso={dateIso} bind:precision={datePrecision} bind:endDateIso={dateEndIso} - rawDate={doc.metaDateRaw ?? ''} initialDateIso={doc.documentDate ?? ''} initialLocation={doc.location ?? ''} initialSenderName={doc.sender?.displayName ?? ''} diff --git a/frontend/src/lib/document/WhoWhenSection.svelte b/frontend/src/lib/document/WhoWhenSection.svelte index 8c49a72b..4912f8d7 100644 --- a/frontend/src/lib/document/WhoWhenSection.svelte +++ b/frontend/src/lib/document/WhoWhenSection.svelte @@ -16,7 +16,6 @@ let { dateIso = $bindable(''), precision = $bindable('DAY'), endDateIso = $bindable(''), - rawDate = '', initialDateIso = '', initialLocation = '', initialSenderName = '', @@ -30,7 +29,6 @@ let { dateIso?: string; precision?: DatePrecision; endDateIso?: string; - rawDate?: string; initialDateIso?: string; initialLocation?: string; initialSenderName?: string; @@ -179,15 +177,6 @@ $effect(() => { {/if} - - - {#if rawDate && rawDate.trim().length > 0} -
-

{m.date_original_label()}

-

{rawDate}

- -
- {/if} {/if} diff --git a/frontend/src/lib/document/WhoWhenSection.svelte.test.ts b/frontend/src/lib/document/WhoWhenSection.svelte.test.ts index f2d7746f..136c260c 100644 --- a/frontend/src/lib/document/WhoWhenSection.svelte.test.ts +++ b/frontend/src/lib/document/WhoWhenSection.svelte.test.ts @@ -93,13 +93,12 @@ describe('WhoWhenSection — precision controls', () => { expect(document.querySelector('input#metaDateEnd')).not.toBeNull(); }); - it('renders the raw cell as static text (not an editable input) and escapes it', async () => { - render(WhoWhenSection, { rawDate: 'Sommer 1916' }); - const raw = document.querySelector('[data-testid="who-when-raw"]'); - expect(raw).not.toBeNull(); - // Verbatim shown as escaped text; no injected element. - expect(raw?.textContent).toContain('Sommer 1916'); - expect(raw?.querySelector('b')).toBeNull(); + it('never renders the raw cell, and never re-submits it via a hidden input', async () => { + render(WhoWhenSection, {}); + // The confusing "Originaltext" line is gone … + expect(document.querySelector('[data-testid="who-when-raw"]')).toBeNull(); + // … and editing no longer round-trips metaDateRaw to the backend. + expect(document.querySelector('input[name="metaDateRaw"]')).toBeNull(); }); }); -- 2.49.1 From 4944918692976f33cbfe0376815755ca0e86c210 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 1 Jun 2026 20:06:12 +0200 Subject: [PATCH 2/4] feat(document): remove the visible Originaltext line from DocumentDate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DocumentDate rendered an "Originaltext: " secondary line for UNKNOWN/SEASON/APPROX dates, gated by a showRaw prop. Drop the visible line, the showRaw prop, the showRawLine derived, and the now-unused date_original_label message import. The raw prop stays — it still feeds the SEASON word in formatDocumentDate, which only ever maps a fixed German season token (never emits raw text), so no XSS surface remains. Update both DocumentRow call sites to drop the now-gone showRaw={false} and the comment that justified it. Remove the two DocumentDate tests that asserted on the deleted DOM sink (the UNKNOWN secondary line and its XSS-escaping); the DAY/MONTH coverage stays. Co-Authored-By: Claude Opus 4.8 --- frontend/src/lib/document/DocumentDate.svelte | 20 ++----------------- .../lib/document/DocumentDate.svelte.test.ts | 15 -------------- frontend/src/lib/document/DocumentRow.svelte | 6 ------ 3 files changed, 2 insertions(+), 39 deletions(-) diff --git a/frontend/src/lib/document/DocumentDate.svelte b/frontend/src/lib/document/DocumentDate.svelte index 5b959881..8b06dbd4 100644 --- a/frontend/src/lib/document/DocumentDate.svelte +++ b/frontend/src/lib/document/DocumentDate.svelte @@ -1,30 +1,20 @@ @@ -61,10 +51,4 @@ const showRawLine = $derived( {:else} {label} {/if} - {#if showRawLine} - - {m.date_original_label()} {raw} - {/if} diff --git a/frontend/src/lib/document/DocumentDate.svelte.test.ts b/frontend/src/lib/document/DocumentDate.svelte.test.ts index fa842b7b..af0b8842 100644 --- a/frontend/src/lib/document/DocumentDate.svelte.test.ts +++ b/frontend/src/lib/document/DocumentDate.svelte.test.ts @@ -17,19 +17,4 @@ describe('DocumentDate', () => { render(DocumentDate, { props: { iso: '1916-06-01', precision: 'MONTH', raw: 'Juni 1916' } }); await expect.element(page.getByText('Juni 1916')).toBeInTheDocument(); }); - - it('shows the verbatim raw cell as a visible secondary line for UNKNOWN (not tooltip-only)', async () => { - render(DocumentDate, { props: { iso: null, precision: 'UNKNOWN', raw: 'Sommer?' } }); - // Real, visible text — not hidden behind a title attribute. - await expect.element(page.getByText('Datum unbekannt')).toBeInTheDocument(); - await expect.element(page.getByText(/Sommer\?/)).toBeVisible(); - }); - - it('renders a malicious raw value as inert escaped text (no element injected)', async () => { - const malicious = ''; - render(DocumentDate, { props: { iso: null, precision: 'UNKNOWN', raw: malicious } }); - // The payload appears as literal text, and no is created in the DOM. - await expect.element(page.getByText(/
-
@@ -194,7 +189,6 @@ function safeTagColor(color: string | null | undefined): string { iso={doc.documentDate} precision={doc.metaDatePrecision} end={doc.metaDateEnd} - showRaw={false} />
-- 2.49.1 From d5bf401085134104baa58d535d11d4d8b4296e49 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 1 Jun 2026 20:07:20 +0200 Subject: [PATCH 3/4] feat(document): stop surfacing the raw cell in the detail drawer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The detail drawer's date cell rendered DocumentDate whenever a date OR a raw cell was present (`{#if documentDate || metaDateRaw}`). For an undated, raw-only document that meant the verbatim import text leaked into the view. Tighten the guard to `{#if documentDate}` so such a document shows "—". The raw prop is still passed through for the SEASON word on dated documents. Covered by a new test. Co-Authored-By: Claude Opus 4.8 --- .../src/lib/document/DocumentMetadataDrawer.svelte | 2 +- .../document/DocumentMetadataDrawer.svelte.test.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/document/DocumentMetadataDrawer.svelte b/frontend/src/lib/document/DocumentMetadataDrawer.svelte index 4b8081e9..19505265 100644 --- a/frontend/src/lib/document/DocumentMetadataDrawer.svelte +++ b/frontend/src/lib/document/DocumentMetadataDrawer.svelte @@ -113,7 +113,7 @@ function getFullName(person: Person): string {
{m.doc_details_field_date()}
- {#if documentDate || metaDateRaw} + {#if documentDate} { expect(dashTexts.length).toBeGreaterThan(0); }); + it('shows an em-dash and never the raw cell for an undated, raw-only document', async () => { + render(DocumentMetadataDrawer, { + props: { ...baseProps, documentDate: null, metaDateRaw: 'Sommer 1916' } + }); + + await expect.element(page.getByText('Sommer 1916')).not.toBeInTheDocument(); + const dashTexts = Array.from(document.querySelectorAll('dd, p')) + .map((el) => el.textContent?.trim()) + .filter((t) => t === '—'); + expect(dashTexts.length).toBeGreaterThan(0); + }); + it('renders the no-persons placeholder when sender and receivers are empty', async () => { render(DocumentMetadataDrawer, { props: baseProps }); -- 2.49.1 From 8a1cc2d1f06af5baf62519c29fdb1a3d50d29850 Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 1 Jun 2026 20:09:10 +0200 Subject: [PATCH 4/4] chore(i18n): drop the unused date_original_label key and stale comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With the visible "Originaltext" line gone from every view, the date_original_label message has no remaining references — remove it from de/en/es. Also drop the now-inaccurate comments in documentDate.ts that described the raw cell as "preserved separately as the visible secondary line"; the raw cell now only feeds the SEASON word and is never shown. Co-Authored-By: Claude Opus 4.8 --- frontend/messages/de.json | 1 - frontend/messages/en.json | 1 - frontend/messages/es.json | 1 - frontend/src/lib/shared/utils/documentDate.ts | 6 ++---- 4 files changed, 2 insertions(+), 7 deletions(-) diff --git a/frontend/messages/de.json b/frontend/messages/de.json index 5590c6b7..a058a329 100644 --- a/frontend/messages/de.json +++ b/frontend/messages/de.json @@ -302,7 +302,6 @@ "date_season_summer": "Sommer", "date_season_autumn": "Herbst", "date_season_winter": "Winter", - "date_original_label": "Originaltext:", "date_unknown_icon_label": "Datum unbekannt", "form_label_date_precision": "Datumsgenauigkeit", "form_label_date_end": "Enddatum", diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 5b7c2698..2c771571 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -302,7 +302,6 @@ "date_season_summer": "Summer", "date_season_autumn": "Autumn", "date_season_winter": "Winter", - "date_original_label": "Original:", "date_unknown_icon_label": "Date unknown", "form_label_date_precision": "Date precision", "form_label_date_end": "End date", diff --git a/frontend/messages/es.json b/frontend/messages/es.json index 4e856892..aab63403 100644 --- a/frontend/messages/es.json +++ b/frontend/messages/es.json @@ -302,7 +302,6 @@ "date_season_summer": "Verano", "date_season_autumn": "Otoño", "date_season_winter": "Invierno", - "date_original_label": "Texto original:", "date_unknown_icon_label": "Fecha desconocida", "form_label_date_precision": "Precisión de la fecha", "form_label_date_end": "Fecha final", diff --git a/frontend/src/lib/shared/utils/documentDate.ts b/frontend/src/lib/shared/utils/documentDate.ts index d1965cb5..bc5aa5d7 100644 --- a/frontend/src/lib/shared/utils/documentDate.ts +++ b/frontend/src/lib/shared/utils/documentDate.ts @@ -20,8 +20,7 @@ export type DatePrecision = 'DAY' | 'MONTH' | 'SEASON' | 'YEAR' | 'RANGE' | 'APP * {@code DocumentTitleFormatter}: both are asserted against * `docs/date-label-fixtures.json` so they cannot drift. The untrusted `raw` * cell is only used to derive a season word (a known German season token) — it - * is otherwise rendered separately by the caller via Svelte default escaping, - * never interpolated into HTML here. + * is never displayed and never interpolated into HTML here. * * @param iso the sort/filter anchor day (`YYYY-MM-DD`), nullable for UNKNOWN rows * @param precision descriptive precision metadata @@ -82,8 +81,7 @@ function seasonLabel( ): string { const month = Number(iso.slice(5, 7)); // Prefer the season named in the raw cell; fall back to deriving it from the - // anchor month. Either way the WORD is localized (Decision 4) — the verbatim - // German raw cell is preserved separately as the visible secondary line. + // anchor month. Either way the WORD is localized (Decision 4). const season = seasonFromRaw(raw) ?? seasonOfMonth(month); return `${seasonWord(season, locale)} ${year}`; } -- 2.49.1