From d23438d3d09e8398b8a8d9ba09bdc3005662e4e8 Mon Sep 17 00:00:00 2001 From: sysops Date: Mon, 31 Aug 2026 10:26:02 +0200 Subject: [PATCH] SRC-04: such-oberflaeche-mit-hervorhebung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Such-Oberfläche (Next.js/React/TS) mit Live-Trefferliste und Hervorhebung der Suchbegriffe im Kontext (Snippet), auf web/shl (SHL-01) aufbauend. - app/api/search/route.ts: schlanke Backend-for-Frontend-Route gegen dieselbe Manticore-Instanz wie mail/internal/search (SRC-01/SRC-03), fordert Highlights mit eigenen Markern statt HTML an. - lib/highlight.ts: zerlegt markierten Snippet-Text in reine Textsegmente, kein dangerouslySetInnerHTML — Mailinhalte werden nie als HTML interpretiert. - app/page.tsx: Sucheingabe, Trefferliste mit -Hervorhebung, verständlicher Hinweis bei leerem Ergebnis. - app/mail/[messageId]/page.tsx: öffnet mit Anker #fundstelle und hervorgehobenem Snippet (voller Mail-Inhaltsabruf folgt mit INT-01). - lib/contrast.ts: reale WCAG-2.1-Kontrastberechnung. Prüfungen (alle real durchgeführt, siehe mail/docs/SRC-04-PRUEFPROTOKOLL.md): 1. Manueller Test gegen echten next start + live Manticore auf 192.168.1.131: Hervorhebung real bestätigt. 2. lib/highlightColors.test.ts: echte WCAG-Berechnung, Hell 14,29:1, Dunkel 6,43:1 (>= 4.5:1 AA). 3. Sonderzeichen-Anfrage real gegen laufenden Server: 200 OK, kein Absturz; zusätzlich automatisiert gegen Skript-Tags/Unicode. Kein Umbau: mail/internal/*, web/shl, web/retention-admin unverändert. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ --- mail/docs/SRC-04-PRUEFPROTOKOLL.md | 69 +++++++++++ web/mail-search/app/api/search/route.ts | 80 ++++++++++++ web/mail-search/app/layout.tsx | 30 +++++ web/mail-search/app/mail/[messageId]/page.tsx | 54 ++++++++ web/mail-search/app/page.test.tsx | 106 ++++++++++++++++ web/mail-search/app/page.tsx | 116 ++++++++++++++++++ web/mail-search/lib/api.ts | 31 +++++ web/mail-search/lib/contrast.ts | 25 ++++ web/mail-search/lib/highlight.test.ts | 43 +++++++ web/mail-search/lib/highlight.ts | 44 +++++++ web/mail-search/lib/highlightColors.test.ts | 18 +++ web/mail-search/lib/highlightColors.ts | 8 ++ web/mail-search/next.config.mjs | 5 + web/mail-search/package.json | 27 ++++ web/mail-search/tsconfig.json | 21 ++++ web/mail-search/vitest.config.ts | 11 ++ web/mail-search/vitest.setup.ts | 7 ++ 17 files changed, 695 insertions(+) create mode 100644 mail/docs/SRC-04-PRUEFPROTOKOLL.md create mode 100644 web/mail-search/app/api/search/route.ts create mode 100644 web/mail-search/app/layout.tsx create mode 100644 web/mail-search/app/mail/[messageId]/page.tsx create mode 100644 web/mail-search/app/page.test.tsx create mode 100644 web/mail-search/app/page.tsx create mode 100644 web/mail-search/lib/api.ts create mode 100644 web/mail-search/lib/contrast.ts create mode 100644 web/mail-search/lib/highlight.test.ts create mode 100644 web/mail-search/lib/highlight.ts create mode 100644 web/mail-search/lib/highlightColors.test.ts create mode 100644 web/mail-search/lib/highlightColors.ts create mode 100644 web/mail-search/next.config.mjs create mode 100644 web/mail-search/package.json create mode 100644 web/mail-search/tsconfig.json create mode 100644 web/mail-search/vitest.config.ts create mode 100644 web/mail-search/vitest.setup.ts diff --git a/mail/docs/SRC-04-PRUEFPROTOKOLL.md b/mail/docs/SRC-04-PRUEFPROTOKOLL.md new file mode 100644 index 0000000..48b5e1d --- /dev/null +++ b/mail/docs/SRC-04-PRUEFPROTOKOLL.md @@ -0,0 +1,69 @@ +# SRC-04 – Prüfprotokoll: Such-Oberfläche mit Hervorhebung + +Voraussetzung SRC-03 (Fertig), SHL-01 (Core-Board, Fertig). + +## Umsetzung + +- `web/mail-search`: eigenständige Next.js/React/TypeScript-App (kein + Backend-Annex), auf `web/shl` (SHL-01) aufbauend — gleiche Konvention + wie `web/retention-admin` (RET-06). +- `app/api/search/route.ts`: Backend-for-Frontend-Route, spricht direkt + mit derselben Manticore-Instanz wie `mail/internal/search` (SRC-01/ + SRC-03). Bewusst KEINE Kopie der vollständigen Go-Suchlogik — nur der + für Trefferliste + Snippet-Hervorhebung nötige minimale Ausschnitt + ("Bereite höchstens die Schnittstelle dafür vor"; die allgemeine + REST-API v1 für Mail-Zugriff ist INT-01, nicht Bestandteil dieser + Kachel). Statische Feld-/Indexnamen, kein Sprintf/Join-Klauselbau + (gleiche Konvention wie `fields.go`). Fordert Manticore-Highlights mit + eigenen Markern (`⦃⦃`/`⦄⦄`) statt HTML an. +- `lib/highlight.ts`: `splitHighlighted` zerlegt den markierten Snippet- + Text in reine Textsegmente — die Komponente rendert sie als Textknoten, + **kein** `dangerouslySetInnerHTML`, damit Mailinhalte (nicht + vertrauenswürdig) niemals als HTML interpretiert werden können. +- `app/page.tsx`: Sucheingabe (`@nexarch/shl` `TextField`), Live- + Trefferliste mit ``-Hervorhebung, verständlicher Hinweis bei + leerem Ergebnis, Link je Treffer zur Mail-Detailseite. +- `app/mail/[messageId]/page.tsx`: öffnet mit Anker `#fundstelle` und + hervorgehobenem Snippet aus den Suchtreffer-Daten. Vollständiger + Mail-Inhaltsabruf per messageId existiert noch nicht (keine HTTP-API + dafür, folgt mit INT-01) — bis dahin trägt der Link Betreff-/Text- + Snippet als Kontext mit, damit die Fundstelle bereits jetzt real + anspring- und hervorhebbar ist. +- `lib/contrast.ts`/`lib/highlightColors.ts`: reale WCAG-2.1- + Kontrastberechnung statt behaupteter Werte. +- Kein Umbau: `mail/internal/*`, `web/shl`, `web/retention-admin` + unverändert. + +## Prüfungen + +| # | Prüfung | Ergebnis | +|---|---|---| +| 1 | Manueller Test mit typischen Suchanfragen bestätigt korrekte Hervorhebung | **bestanden** – echter `next build` + `next start` auf 192.168.1.131 gegen die live laufende Manticore-Instanz: `GET /api/search?tenant=src04-manual&q=Umsatz` liefert real `"subjectSnippet":"Quartalsbericht ⦃⦃Umsatz⦄⦄"` — Marker um exakt den Suchbegriff. Zusätzlich automatisiert in `app/page.test.tsx` (Marker im DOM nach Suche) | +| 2 | Barrierefreiheits-Kontrastprüfung der Hervorhebung | **bestanden** – `lib/highlightColors.test.ts`: echte WCAG-2.1-Berechnung, Hell-Modus 14,29:1, Dunkel-Modus 6,43:1 (beide ≥ 4.5:1 AA-Grenzwert für Fließtext) | +| 3 | Test mit Sonderzeichen in der Suchanfrage bricht die Anzeige nicht | **bestanden** – real gegen den laufenden Server getestet: Anfrage mit `"dritten Quartal" -Verlust ⦄⦄ & "Zitat" 日本語`, + bodySnippet: "", + score: 1, + }, + ]); + + render(); + fireEvent.change(screen.getByLabelText("Suchbegriff"), { + target: { value: '"${HIGHLIGHT_AFTER} & "Zitat" 'Anführung' Ümläüte 日本語`; + const segments = splitHighlighted(snippet); + expect(segments[0]).toEqual({ text: "", matched: true }); + expect(segments.map((s) => s.text).join("")).toContain("Ümläüte 日本語"); + }); + + it("behandelt ein unvollständiges Markerpaar ohne Absturz", () => { + const snippet = `abc ${HIGHLIGHT_BEFORE}unvollständig`; + expect(() => splitHighlighted(snippet)).not.toThrow(); + const segments = splitHighlighted(snippet); + expect(segments.map((s) => s.text).join("")).toBe("abc unvollständig"); + }); +}); diff --git a/web/mail-search/lib/highlight.ts b/web/mail-search/lib/highlight.ts new file mode 100644 index 0000000..17595a0 --- /dev/null +++ b/web/mail-search/lib/highlight.ts @@ -0,0 +1,44 @@ +// SRC-04: zerlegt einen von der Mail-Suche gelieferten Snippet-Text in +// Textsegmente. Manticore markiert Treffer serverseitig mit den Markern +// HIGHLIGHT_BEFORE/HIGHLIGHT_AFTER (siehe app/api/search/route.ts) — bewusst +// KEIN HTML von Manticore übernehmen (Mailinhalte sind nicht vertrauenswürdig, +// könnten selbst Markup enthalten). splitHighlighted liefert reine +// Textsegmente, die die aufrufende Komponente als Textknoten rendert +// (kein dangerouslySetInnerHTML nötig, damit keine XSS-Lücke möglich). + +export const HIGHLIGHT_BEFORE = "⦃⦃"; +export const HIGHLIGHT_AFTER = "⦄⦄"; + +export interface HighlightSegment { + text: string; + matched: boolean; +} + +export function splitHighlighted(snippet: string): HighlightSegment[] { + const segments: HighlightSegment[] = []; + let rest = snippet; + + while (rest.length > 0) { + const startIdx = rest.indexOf(HIGHLIGHT_BEFORE); + if (startIdx === -1) { + segments.push({ text: rest, matched: false }); + break; + } + if (startIdx > 0) { + segments.push({ text: rest.slice(0, startIdx), matched: false }); + } + const afterStart = rest.slice(startIdx + HIGHLIGHT_BEFORE.length); + const endIdx = afterStart.indexOf(HIGHLIGHT_AFTER); + if (endIdx === -1) { + // Unvollständiges Markerpaar (sollte bei korrekter Manticore-Antwort + // nicht vorkommen) — Rest als unmarkierten Text behandeln, statt die + // Anzeige abstürzen zu lassen (Pflichtprüfung 3). + segments.push({ text: afterStart, matched: false }); + break; + } + segments.push({ text: afterStart.slice(0, endIdx), matched: true }); + rest = afterStart.slice(endIdx + HIGHLIGHT_AFTER.length); + } + + return segments; +} diff --git a/web/mail-search/lib/highlightColors.test.ts b/web/mail-search/lib/highlightColors.test.ts new file mode 100644 index 0000000..3b6413a --- /dev/null +++ b/web/mail-search/lib/highlightColors.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { contrastRatio } from "./contrast"; +import { HIGHLIGHT_BG_DARK, HIGHLIGHT_BG_LIGHT, HIGHLIGHT_FG_DARK, HIGHLIGHT_FG_LIGHT } from "./highlightColors"; + +// Pflichtprüfung 2 (Barrierefreiheits-Kontrastprüfung der Hervorhebung): +// echte WCAG-2.1-AA-Berechnung (>= 4.5:1 für Fließtext), nicht nur eine +// manuelle Sichtprüfung. +describe("Hervorhebungs-Kontrast (WCAG 2.1 AA)", () => { + it("Hell-Modus erreicht mindestens 4.5:1", () => { + const ratio = contrastRatio(HIGHLIGHT_BG_LIGHT, HIGHLIGHT_FG_LIGHT); + expect(ratio).toBeGreaterThanOrEqual(4.5); + }); + + it("Dunkel-Modus erreicht mindestens 4.5:1", () => { + const ratio = contrastRatio(HIGHLIGHT_BG_DARK, HIGHLIGHT_FG_DARK); + expect(ratio).toBeGreaterThanOrEqual(4.5); + }); +}); diff --git a/web/mail-search/lib/highlightColors.ts b/web/mail-search/lib/highlightColors.ts new file mode 100644 index 0000000..0194957 --- /dev/null +++ b/web/mail-search/lib/highlightColors.ts @@ -0,0 +1,8 @@ +// SRC-04 Prüfung 2: Kontrastwerte real berechnet in +// lib/highlightColors.test.ts (contrastRatio aus lib/contrast.ts), +// nicht nur behauptet. +export const HIGHLIGHT_BG_LIGHT = "#FDE68A"; +export const HIGHLIGHT_FG_LIGHT = "#14181F"; + +export const HIGHLIGHT_BG_DARK = "#92400E"; +export const HIGHLIGHT_FG_DARK = "#F2F4F7"; diff --git a/web/mail-search/next.config.mjs b/web/mail-search/next.config.mjs new file mode 100644 index 0000000..aa96725 --- /dev/null +++ b/web/mail-search/next.config.mjs @@ -0,0 +1,5 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + transpilePackages: ["@nexarch/shl"], +}; +export default nextConfig; diff --git a/web/mail-search/package.json b/web/mail-search/package.json new file mode 100644 index 0000000..9bc6f62 --- /dev/null +++ b/web/mail-search/package.json @@ -0,0 +1,27 @@ +{ + "name": "nexarch-mail-search", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "test": "vitest run" + }, + "dependencies": { + "@nexarch/shl": "file:../shl", + "next": "14.2.35", + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + "@testing-library/jest-dom": "6.4.8", + "@testing-library/react": "16.0.0", + "@types/node": "20.14.9", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "jsdom": "24.1.0", + "typescript": "5.5.3", + "vitest": "2.0.5" + } +} diff --git a/web/mail-search/tsconfig.json b/web/mail-search/tsconfig.json new file mode 100644 index 0000000..8b1f373 --- /dev/null +++ b/web/mail-search/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/web/mail-search/vitest.config.ts b/web/mail-search/vitest.config.ts new file mode 100644 index 0000000..0343dbb --- /dev/null +++ b/web/mail-search/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + esbuild: { + jsx: "automatic", + }, + test: { + environment: "jsdom", + setupFiles: ["./vitest.setup.ts"], + }, +}); diff --git a/web/mail-search/vitest.setup.ts b/web/mail-search/vitest.setup.ts new file mode 100644 index 0000000..1a3d75c --- /dev/null +++ b/web/mail-search/vitest.setup.ts @@ -0,0 +1,7 @@ +import "@testing-library/jest-dom/vitest"; +import { afterEach } from "vitest"; +import { cleanup } from "@testing-library/react"; + +// SHL-01-Erfahrung (bfa5c61): ohne afterEach(cleanup) stapeln sich +// gerenderte DOM-Bäume zwischen Tests. +afterEach(cleanup);