docs: round-3 currency pass
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m17s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 4m9s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 4m17s
CI / OCR Service Tests (pull_request) Successful in 22s
CI / Backend Unit Tests (pull_request) Successful in 4m9s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m7s
- extractText.ts states the real safety invariant (output is text-only; JOURNEY intros arrive unsanitised by design) - geschichte README: stale glyph/pill cells updated, formatAuthorName no longer claims an email fallback, formatDocumentMetaLine documented - reader spec HTMLs: bg-white list-card cell and the mobile BottomSheet rows struck with the implemented decision (inline metabar actions) Review round 3: Nora (2), Markus (2), Felix, Elicit (3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -421,7 +421,7 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr class="grp"><td colspan="3">Seitenstruktur</td></tr>
|
<tr class="grp"><td colspan="3">Seitenstruktur</td></tr>
|
||||||
<tr><td>Page title</td><td>font-family:var(--font-display);font-size:24px;color:var(--navy)</td><td>Fraunces, nicht fett</td></tr>
|
<tr><td>Page title</td><td>font-family:var(--font-display);font-size:24px;color:var(--navy)</td><td>Fraunces, nicht fett</td></tr>
|
||||||
<tr><td>Editorial list card</td><td>bg-white shadow-sm border border-brand-sand rounded-sm</td><td>wraps alle Zeilen</td></tr>
|
<tr><td>Editorial list card</td><td><s>bg-white shadow-sm border border-brand-sand rounded-sm</s> <em>(implementiert: bg-surface border-line — semantische Tokens für Dark Mode)</em></td><td>wraps alle Zeilen</td></tr>
|
||||||
<tr class="grp"><td colspan="3">Listenzeile</td></tr>
|
<tr class="grp"><td colspan="3">Listenzeile</td></tr>
|
||||||
<tr><td>List row</td><td>flex gap-0 border-b border-brand-sand last:border-0 hover:bg-surface</td><td>min-h-[44px] auf Mobile</td></tr>
|
<tr><td>List row</td><td>flex gap-0 border-b border-brand-sand last:border-0 hover:bg-surface</td><td>min-h-[44px] auf Mobile</td></tr>
|
||||||
<tr><td>Meta column</td><td>w-40 shrink-0 flex flex-col gap-1 p-3 border-r border-line-2</td><td>feste Breite — breit genug für text-sm Namen ohne Umbruch</td></tr>
|
<tr><td>Meta column</td><td>w-40 shrink-0 flex flex-col gap-1 p-3 border-r border-line-2</td><td>feste Breite — breit genug für text-sm Namen ohne Umbruch</td></tr>
|
||||||
@@ -658,7 +658,7 @@
|
|||||||
<tr><td>Doc reference card</td><td>flex gap-3 items-start bg-white border border-brand-sand rounded-sm p-3 hover:shadow-sm</td><td>links zu /documents/[id]</td></tr>
|
<tr><td>Doc reference card</td><td>flex gap-3 items-start bg-white border border-brand-sand rounded-sm p-3 hover:shadow-sm</td><td>links zu /documents/[id]</td></tr>
|
||||||
<tr><td>Doc icon</td><td>w-9 h-9 bg-surface rounded flex items-center justify-center shrink-0</td><td>Dateisymbol SVG</td></tr>
|
<tr><td>Doc icon</td><td>w-9 h-9 bg-surface rounded flex items-center justify-center shrink-0</td><td>Dateisymbol SVG</td></tr>
|
||||||
<tr class="grp"><td colspan="3">Mobile</td></tr>
|
<tr class="grp"><td colspan="3">Mobile</td></tr>
|
||||||
<tr><td>… Menü (Mobile)</td><td>ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen</td><td>BLOG_WRITE-Aktionen auf Mobile</td></tr>
|
<tr><td>… Menü (Mobile)</td><td><s>ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen</s> <em>(implementiert: Bearbeiten/Löschen bleiben inline in der Metazeile auf allen Breiten — kein BottomSheet)</em></td><td>BLOG_WRITE-Aktionen auf Mobile</td></tr>
|
||||||
<tr><td>Person chips (Mobile)</td><td>flex-wrap, volle Breite</td><td>kein horizontales Scrollen</td></tr>
|
<tr><td>Person chips (Mobile)</td><td>flex-wrap, volle Breite</td><td>kein horizontales Scrollen</td></tr>
|
||||||
<tr><td>Doc cards (Mobile)</td><td>flex-col gap-2</td><td>stapeln vertikal</td></tr>
|
<tr><td>Doc cards (Mobile)</td><td>flex-col gap-2</td><td>stapeln vertikal</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -713,7 +713,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>Die Schaltflächen „+ Neue Geschichte", „Bearbeiten" und „Löschen" werden nur gerendert, wenn <code>currentUser.permissions.includes('BLOG_WRITE')</code> wahr ist.</li>
|
<li>Die Schaltflächen „+ Neue Geschichte", „Bearbeiten" und „Löschen" werden nur gerendert, wenn <code>currentUser.permissions.includes('BLOG_WRITE')</code> wahr ist.</li>
|
||||||
<li>Nicht nur ausblenden — Backend-Endpunkte für Schreib-/Löschoperationen sind ebenfalls durch <code>@RequirePermission(Permission.BLOG_WRITE)</code> geschützt.</li>
|
<li>Nicht nur ausblenden — Backend-Endpunkte für Schreib-/Löschoperationen sind ebenfalls durch <code>@RequirePermission(Permission.BLOG_WRITE)</code> geschützt.</li>
|
||||||
<li>Auf Mobile werden Bearbeiten/Löschen aus dem Layout entfernt und erscheinen in einem BottomSheet, das über das ··· Menü in der Metazeile geöffnet wird.</li>
|
<li><s>Auf Mobile werden Bearbeiten/Löschen aus dem Layout entfernt und erscheinen in einem BottomSheet, das über das ··· Menü in der Metazeile geöffnet wird.</s> <em>(implementiert: Aktionen bleiben inline in der Metazeile — h-11 Touch-Targets, kein BottomSheet)</em></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>Barrierefreiheit</h3>
|
<h3>Barrierefreiheit</h3>
|
||||||
|
|||||||
@@ -634,7 +634,7 @@
|
|||||||
<tr><td>Journey-Badge</td><td>inline-flex px-2 py-px rounded-sm text-[10px] font-bold uppercase tracking-widest bg-journey-tint text-journey border border-journey-border mb-2</td><td>über dem Titel; nicht für STORY</td></tr>
|
<tr><td>Journey-Badge</td><td>inline-flex px-2 py-px rounded-sm text-[10px] font-bold uppercase tracking-widest bg-journey-tint text-journey border border-journey-border mb-2</td><td>über dem Titel; nicht für STORY</td></tr>
|
||||||
<tr><td>Titel</td><td>font-serif text-3xl text-ink leading-tight mb-4</td><td>gleich wie Story</td></tr>
|
<tr><td>Titel</td><td>font-serif text-3xl text-ink leading-tight mb-4</td><td>gleich wie Story</td></tr>
|
||||||
<tr><td>Metabar</td><td>flex items-center gap-3 pb-4 border-b border-subtle mb-4</td><td>gleich wie Story</td></tr>
|
<tr><td>Metabar</td><td>flex items-center gap-3 pb-4 border-b border-subtle mb-4</td><td>gleich wie Story</td></tr>
|
||||||
<tr><td>Bearbeiten/Löschen</td><td>nur BLOG_WRITE; auf Mobile im ··· BottomSheet</td><td>gleich wie Story</td></tr>
|
<tr><td>Bearbeiten/Löschen</td><td>nur BLOG_WRITE; <s>auf Mobile im ··· BottomSheet</s> <em>(implementiert: inline in der Metazeile auf allen Breiten)</em></td><td>gleich wie Story</td></tr>
|
||||||
<tr class="grp"><td colspan="3">Intro-Absatz</td></tr>
|
<tr class="grp"><td colspan="3">Intro-Absatz</td></tr>
|
||||||
<tr><td>Intro (body)</td><td>font-serif text-lg text-ink-2 italic leading-relaxed mb-6 pb-4 border-b border-dashed border-subtle</td><td>nur rendern wenn body nicht leer; kein HTML-Rendering — plaintext</td></tr>
|
<tr><td>Intro (body)</td><td>font-serif text-lg text-ink-2 italic leading-relaxed mb-6 pb-4 border-b border-dashed border-subtle</td><td>nur rendern wenn body nicht leer; kein HTML-Rendering — plaintext</td></tr>
|
||||||
<tr class="grp"><td colspan="3">Dokument-Item</td></tr>
|
<tr class="grp"><td colspan="3">Dokument-Item</td></tr>
|
||||||
@@ -650,7 +650,7 @@
|
|||||||
<tr><td>Interlude-Block</td><td>pl-3 border-l-2 border-journey-border bg-journey-tint rounded-r-sm py-2 pr-3 my-4</td><td>item.document === null</td></tr>
|
<tr><td>Interlude-Block</td><td>pl-3 border-l-2 border-journey-border bg-journey-tint rounded-r-sm py-2 pr-3 my-4</td><td>item.document === null</td></tr>
|
||||||
<tr><td>Interlude-Text</td><td>text-base italic text-ink leading-relaxed</td><td>item.note; plaintext</td></tr>
|
<tr><td>Interlude-Text</td><td>text-base italic text-ink leading-relaxed</td><td>item.note; plaintext</td></tr>
|
||||||
<tr class="grp"><td colspan="3">Mobile</td></tr>
|
<tr class="grp"><td colspan="3">Mobile</td></tr>
|
||||||
<tr><td>··· Menü</td><td>ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen</td><td>BLOG_WRITE; gleich wie Story</td></tr>
|
<tr><td>··· Menü</td><td><s>ml-auto text-ink-3; öffnet BottomSheet mit Bearbeiten + Löschen</s> <em>(implementiert: kein BottomSheet — Aktionen inline)</em></td><td>BLOG_WRITE; gleich wie Story</td></tr>
|
||||||
<tr><td>Touch Target (Brief öffnen)</td><td>min-h-[44px] durch padding auf der Karte</td><td>WCAG 2.2 AA</td></tr>
|
<tr><td>Touch Target (Brief öffnen)</td><td>min-h-[44px] durch padding auf der Karte</td><td>WCAG 2.2 AA</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -711,7 +711,7 @@
|
|||||||
<h3>Berechtigungen</h3>
|
<h3>Berechtigungen</h3>
|
||||||
<ul>
|
<ul>
|
||||||
<li>„Bearbeiten" und „Löschen" nur für <code>currentUser.permissions.includes('BLOG_WRITE')</code> — gleich wie Story.</li>
|
<li>„Bearbeiten" und „Löschen" nur für <code>currentUser.permissions.includes('BLOG_WRITE')</code> — gleich wie Story.</li>
|
||||||
<li>Auf Mobile: Bearbeiten/Löschen im BottomSheet hinter ··· — gleich wie Story.</li>
|
<li><s>Auf Mobile: Bearbeiten/Löschen im BottomSheet hinter ··· — gleich wie Story.</s> <em>(implementiert: Aktionen bleiben inline in der Metazeile — h-11 Touch-Targets)</em></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h3>Barrierefreiheit</h3>
|
<h3>Barrierefreiheit</h3>
|
||||||
|
|||||||
@@ -23,16 +23,17 @@ Utilities: `utils.ts`.
|
|||||||
| `JourneyItemRow.svelte` | `JourneyEditor.svelte` | Item row: drag handle, move-up/down, note textarea (PATCH on blur), inline remove confirm |
|
| `JourneyItemRow.svelte` | `JourneyEditor.svelte` | Item row: drag handle, move-up/down, note textarea (PATCH on blur), inline remove confirm |
|
||||||
| `JourneyAddBar.svelte` | `JourneyEditor.svelte` | Two add buttons: document picker (`DocumentPickerDropdown`) and interlude draft form |
|
| `JourneyAddBar.svelte` | `JourneyEditor.svelte` | Two add buttons: document picker (`DocumentPickerDropdown`) and interlude draft form |
|
||||||
| `GeschichtenCard.svelte` | Person-detail sidebar | Sidebar card showing the 3 most recent stories linked to a person — NOT the list page |
|
| `GeschichtenCard.svelte` | Person-detail sidebar | Sidebar card showing the 3 most recent stories linked to a person — NOT the list page |
|
||||||
| `GeschichteListRow.svelte` | `/geschichten` (list) | Row component for the list; shows JOURNEY type badge (`text-xs` orange pill) |
|
| `GeschichteListRow.svelte` | `/geschichten` (list) | Editorial list row: meta column (avatar, author, date, REISE badge), title + excerpt content column |
|
||||||
| `StoryReader.svelte` | `/geschichten/[id]` (STORY branch) | Renders sanitised rich-text body, persons section, documents section, and author actions |
|
| `StoryReader.svelte` | `/geschichten/[id]` (STORY branch) | Renders sanitised rich-text body, persons section, documents section, and author actions |
|
||||||
| `JourneyReader.svelte` | `/geschichten/[id]` (JOURNEY branch) | Renders intro paragraph, ordered items list, empty-state; delegates to ItemCard/Interlude |
|
| `JourneyReader.svelte` | `/geschichten/[id]` (JOURNEY branch) | Renders intro paragraph, ordered items list, empty-state; delegates to ItemCard/Interlude |
|
||||||
| `JourneyItemCard.svelte` | `JourneyReader.svelte` | Whole-card `<a>` for a document item; dated/undated aria-label, ✎ annotation glyph |
|
| `JourneyItemCard.svelte` | `JourneyReader.svelte` | Card per document item: title, meta line (date · von X an Y), "Brief öffnen →" link, mint-border note |
|
||||||
| `JourneyInterlude.svelte` | `JourneyReader.svelte` | Typographic aside between letters; ❦ glyph, `aria-label="Kuratorennotiz"` |
|
| `JourneyInterlude.svelte` | `JourneyReader.svelte` | Left-accent interlude box between letters (mode-aware tokens); `aria-label="Kuratorennotiz"` |
|
||||||
|
|
||||||
## utils.ts
|
## utils.ts
|
||||||
|
|
||||||
`formatAuthorName(author)` — joins `firstName + lastName`, falls back to `email` (for list/summary shapes).
|
`formatAuthorName(author)` — joins `firstName + lastName`, falls back to the localized `person_unknown` key (for list/summary shapes; email is not exposed).
|
||||||
`formatAuthorDisplayName(author)` — returns `displayName` (for detail `AuthorView` shape).
|
`formatAuthorDisplayName(author)` — returns `displayName`, localizing the server's `[Unbekannt]` fallback (for detail `AuthorView` shape).
|
||||||
|
`formatDocumentMetaLine(doc)` — `"12.07.1938 · von Franz an Emma"`; shared by `JourneyItemCard`, `JourneyItemRow`, and the story doc-reference cards.
|
||||||
`formatPublishedAt(publishedAt, style)` — wraps `formatDate` with null check; `style` is `'short'` (list) or `'long'` (detail).
|
`formatPublishedAt(publishedAt, style)` — wraps `formatDate` with null check; `style` is `'short'` (list) or `'long'` (detail).
|
||||||
|
|
||||||
## Public list is PUBLISHED-only
|
## Public list is PUBLISHED-only
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
/**
|
/**
|
||||||
* **Not a sanitizer.** This module extracts visible text from a (presumed
|
* **Not a sanitizer.** This module extracts visible text from an HTML (or
|
||||||
* already-sanitised) HTML string for excerpt rendering. It is safe ONLY
|
* plain-text) string for excerpt rendering. The safety invariant is: the
|
||||||
* because the Geschichte body is sanitised against the OWASP allow-list
|
* OUTPUT must only ever be rendered via Svelte text interpolation — never
|
||||||
* on the server before persistence, and via DOMPurify on render.
|
* `{@html}`. The DOMParser document is inert (scripts don't execute), but
|
||||||
|
* the returned string is whatever text the input carried.
|
||||||
|
*
|
||||||
|
* Note on inputs: STORY bodies are additionally sanitised against the OWASP
|
||||||
|
* allow-list on the server; JOURNEY intros are stored VERBATIM (unsanitised
|
||||||
|
* by design — see GeschichteService.bodyForType) and arrive here untrusted.
|
||||||
*
|
*
|
||||||
* Do not use these helpers to defend against XSS — `safeHtml()` in
|
* Do not use these helpers to defend against XSS — `safeHtml()` in
|
||||||
* `./sanitize.ts` is the only sanitiser. Calling `extractText()` on
|
* `./sanitize.ts` is the only sanitiser. Calling `extractText()` on
|
||||||
* untrusted input that has not been sanitised does not protect against
|
* untrusted input does not protect against `javascript:` URLs,
|
||||||
* `javascript:` URLs, event-handler attributes, or `<svg/onload>` payloads.
|
* event-handler attributes, or `<svg/onload>` payloads.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user