SRC-04: such-oberflaeche-mit-hervorhebung
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 <mark>-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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
9748307f12
commit
d23438d3d0
@@ -0,0 +1,80 @@
|
||||
// SRC-04: Backend-for-Frontend-Route für die Such-Oberfläche. Spricht
|
||||
// direkt mit Manticore (dieselbe Instanz wie mail/internal/search, SRC-01/
|
||||
// SRC-03) — bewusst KEINE Kopie der vollständigen Go-Suchlogik, sondern nur
|
||||
// der für die Trefferliste + Snippet-Hervorhebung nötige minimale
|
||||
// Ausschnitt ("Bereite höchstens die Schnittstelle dafür vor", INT-01
|
||||
// baut später die vollständige, allgemeine REST-API v1 für Mail-Zugriff).
|
||||
//
|
||||
// Statische Feld-/Indexnamen, kein Sprintf/Join-artiger Klauselbau (gleiche
|
||||
// Konvention wie mail/internal/search/fields.go).
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { HIGHLIGHT_AFTER, HIGHLIGHT_BEFORE } from "../../../lib/highlight";
|
||||
|
||||
const INDEX_NAME = "mail_documents";
|
||||
const FIELD_TENANT_SLUG = "tenant_slug";
|
||||
|
||||
function manticoreURL(): string {
|
||||
const base = process.env.MANTICORE_URL;
|
||||
if (!base) {
|
||||
throw new Error("MANTICORE_URL ist nicht gesetzt (Umgebungsvariable erforderlich)");
|
||||
}
|
||||
return base.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
interface ManticoreHit {
|
||||
_score: number;
|
||||
_source: { message_id: string };
|
||||
highlight?: { subject?: string[]; body?: string[] };
|
||||
}
|
||||
|
||||
interface ManticoreSearchResponse {
|
||||
hits?: { hits?: ManticoreHit[] };
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const tenantSlug = request.nextUrl.searchParams.get("tenant");
|
||||
const query = request.nextUrl.searchParams.get("q");
|
||||
if (!tenantSlug || !query) {
|
||||
return NextResponse.json({ error: "'tenant' und 'q' sind erforderlich" }, { status: 400 });
|
||||
}
|
||||
|
||||
const manticorePayload = {
|
||||
index: INDEX_NAME,
|
||||
query: {
|
||||
bool: {
|
||||
must: [{ equals: { [FIELD_TENANT_SLUG]: tenantSlug } }, { query_string: query }],
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
fields: { subject: {}, body: {} },
|
||||
before_match: HIGHLIGHT_BEFORE,
|
||||
after_match: HIGHLIGHT_AFTER,
|
||||
},
|
||||
};
|
||||
|
||||
let manticoreResponse: Response;
|
||||
try {
|
||||
manticoreResponse = await fetch(`${manticoreURL()}/search`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(manticorePayload),
|
||||
});
|
||||
} catch (err) {
|
||||
return NextResponse.json({ error: `Suche nicht erreichbar: ${(err as Error).message}` }, { status: 502 });
|
||||
}
|
||||
|
||||
const parsed: ManticoreSearchResponse = await manticoreResponse.json().catch(() => ({}));
|
||||
if (!manticoreResponse.ok || parsed.error) {
|
||||
return NextResponse.json({ error: parsed.error ?? "Suche fehlgeschlagen" }, { status: 502 });
|
||||
}
|
||||
|
||||
const hits = (parsed.hits?.hits ?? []).map((hit) => ({
|
||||
messageId: hit._source.message_id,
|
||||
subjectSnippet: hit.highlight?.subject?.[0] ?? "",
|
||||
bodySnippet: hit.highlight?.body?.[0] ?? "",
|
||||
score: hit._score,
|
||||
}));
|
||||
|
||||
return NextResponse.json({ hits });
|
||||
}
|
||||
Reference in New Issue
Block a user