feat: Rechtliche-Texte-Module auf 100% — Dead Code, RAG-Fallback, Fehler-UI
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 33s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 18s

Paket A:
- einwilligungen/page.tsx: mockRecords (80 Zeilen toter Code) entfernt
- consent/page.tsx: RAG-Suggest-Button im Create-Dialog (+handleRagSuggest)
- workflow/page.tsx: uploadError State + rotes Fehler-Banner statt alert()

Paket B:
- cookie-banner/page.tsx: mockCategories → DEFAULT_COOKIE_CATEGORIES (Bug-Fix)
  DB-Kategorien haben jetzt immer Vorrang — kein Mock-Überschreiben mehr
- test_einwilligungen_routes.py: +4 TestCookieBannerEmbedCode-Tests (36 gesamt)

Paket C:
- searchTemplates.ts: neue Hilfsdatei mit zwei-stufiger Suche
  1. KLAUSUR_SERVICE (5s Timeout), 2. RAG-Fallback via ai-compliance-sdk
- document-generator/page.tsx: ServiceMode State + UI-Badges (rag-only/offline)
- searchTemplates.test.ts: 3 Vitest-Tests (KLAUSUR ok / RAG-Fallback / offline)

flow-data.ts: alle 5 Rechtliche-Texte-Module auf completion: 100

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-03 22:27:13 +01:00
parent 30bccfa39a
commit 7a55955439
9 changed files with 354 additions and 333 deletions

View File

@@ -20,76 +20,10 @@ import { DocumentValidation } from './components/DocumentValidation'
import { generateAllPlaceholders } from '@/lib/sdk/document-generator/datapoint-helpers'
// =============================================================================
// API CLIENT
// API CLIENT (extracted to searchTemplates.ts for testability)
// =============================================================================
const KLAUSUR_SERVICE_URL = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
async function searchTemplates(params: {
query: string
templateType?: TemplateType
licenseTypes?: LicenseType[]
language?: 'de' | 'en'
jurisdiction?: Jurisdiction
limit?: number
}): Promise<LegalTemplateResult[]> {
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: params.query,
template_type: params.templateType,
license_types: params.licenseTypes,
language: params.language,
jurisdiction: params.jurisdiction,
limit: params.limit || 10,
}),
})
if (!response.ok) {
throw new Error('Search failed')
}
const data = await response.json()
return data.map((r: any) => ({
id: r.id,
score: r.score,
text: r.text,
documentTitle: r.document_title,
templateType: r.template_type,
clauseCategory: r.clause_category,
language: r.language,
jurisdiction: r.jurisdiction,
licenseId: r.license_id,
licenseName: r.license_name,
licenseUrl: r.license_url,
attributionRequired: r.attribution_required,
attributionText: r.attribution_text,
sourceName: r.source_name,
sourceUrl: r.source_url,
sourceRepo: r.source_repo,
placeholders: r.placeholders || [],
isCompleteDocument: r.is_complete_document,
isModular: r.is_modular,
requiresCustomization: r.requires_customization,
outputAllowed: r.output_allowed ?? true,
modificationAllowed: r.modification_allowed ?? true,
distortionProhibited: r.distortion_prohibited ?? false,
}))
}
async function getTemplatesStatus(): Promise<any> {
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/status`)
if (!response.ok) return null
return response.json()
}
async function getSources(): Promise<any[]> {
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/sources`)
if (!response.ok) return []
const data = await response.json()
return data.sources || []
}
import { searchTemplates, getTemplatesStatus, getSources, RAG_PROXY } from './searchTemplates'
// =============================================================================
// COMPONENTS
@@ -295,6 +229,8 @@ export default function DocumentGeneratorPage() {
const [status, setStatus] = useState<any>(null)
const [sources, setSources] = useState<any[]>([])
const [isLoading, setIsLoading] = useState(true)
type ServiceMode = 'loading' | 'full' | 'rag-only' | 'offline'
const [serviceMode, setServiceMode] = useState<ServiceMode>('loading')
// Search state
const [searchQuery, setSearchQuery] = useState('')
@@ -321,8 +257,11 @@ export default function DocumentGeneratorPage() {
])
setStatus(statusData)
setSources(sourcesData)
} catch (error) {
console.error('Failed to load status:', error)
const hasKlausur = statusData !== null
const hasRag = await fetch(`${RAG_PROXY}/regulations`).then(r => r.ok).catch(() => false)
setServiceMode(hasKlausur ? 'full' : hasRag ? 'rag-only' : 'offline')
} catch {
setServiceMode('offline')
} finally {
setIsLoading(false)
}
@@ -454,10 +393,15 @@ export default function DocumentGeneratorPage() {
</button>
</StepHeader>
{/* Service unreachable warning */}
{!isLoading && !status && (
<div className="bg-amber-50 border border-amber-200 rounded-xl p-4 text-sm text-amber-800">
<strong>Template-Service nicht erreichbar.</strong> Stellen Sie sicher, dass breakpilot-core läuft (<code>curl -sf http://macmini:8099/health</code>). Das Suchen und Zusammenstellen von Vorlagen ist erst nach Verbindung möglich.
{/* Service mode banners */}
{serviceMode === 'rag-only' && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-800">
KLAUSUR_SERVICE nicht verfügbar Suche läuft über RAG-Fallback
</div>
)}
{serviceMode === 'offline' && (
<div className="bg-red-50 border border-red-200 rounded-lg p-3 text-sm text-red-800">
Keine Template-Services erreichbar. Stellen Sie sicher, dass breakpilot-core oder ai-compliance-sdk läuft.
</div>
)}

View File

@@ -0,0 +1,72 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { searchTemplates } from './searchTemplates'
describe('searchTemplates', () => {
beforeEach(() => {
vi.restoreAllMocks()
})
it('gibt Ergebnisse zurück wenn KLAUSUR_SERVICE verfügbar', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve([{
id: 't1',
score: 0.9,
text: 'Inhalt',
document_title: 'DSE Template',
template_type: 'privacy_policy',
clause_category: null,
language: 'de',
jurisdiction: 'de',
license_id: 'mit',
license_name: 'MIT',
license_url: null,
attribution_required: false,
attribution_text: null,
source_name: 'Test',
source_url: null,
source_repo: null,
placeholders: [],
is_complete_document: true,
is_modular: false,
requires_customization: false,
output_allowed: true,
modification_allowed: true,
distortion_prohibited: false,
}]),
}))
const results = await searchTemplates({ query: 'Datenschutzerklärung' })
expect(results).toHaveLength(1)
expect(results[0].documentTitle).toBe('DSE Template')
})
it('fällt auf RAG zurück wenn KLAUSUR_SERVICE fehlschlägt', async () => {
vi.stubGlobal('fetch', vi.fn()
.mockRejectedValueOnce(new Error('KLAUSUR down'))
.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
results: [{
regulation_name: 'DSGVO Art. 13',
text: 'Informationspflichten',
regulation_code: 'DSGVO-13',
score: 0.8,
}],
}),
})
)
const results = await searchTemplates({ query: 'test' })
expect(results).toHaveLength(1)
expect(results[0].documentTitle).toBe('DSGVO Art. 13')
expect((results[0] as any).source).toBe('rag')
})
it('gibt [] zurück wenn beide Services down sind', async () => {
vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('all down')))
const results = await searchTemplates({ query: 'test' })
expect(results).toEqual([])
})
})

View File

@@ -0,0 +1,140 @@
/**
* Template search helpers for document-generator.
*
* Provides a two-tier search:
* 1. Primary: KLAUSUR_SERVICE (curated legal templates with full metadata)
* 2. Fallback: RAG proxy (ai-compliance-sdk regulation search)
*/
import type { LegalTemplateResult } from '@/lib/sdk/types'
export const KLAUSUR_SERVICE_URL =
process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
export const RAG_PROXY = '/api/sdk/v1/rag'
export interface TemplateSearchParams {
query: string
templateType?: string
licenseTypes?: string[]
language?: 'de' | 'en'
jurisdiction?: string
limit?: number
}
/**
* Search for legal templates with automatic RAG fallback.
*
* Tries KLAUSUR_SERVICE first (5 s timeout). If unavailable or returning an
* error, falls back to the RAG proxy served by ai-compliance-sdk.
* Returns an empty array if both services are down.
*/
export async function searchTemplates(
params: TemplateSearchParams
): Promise<LegalTemplateResult[]> {
// 1. Primary: KLAUSUR_SERVICE
try {
const res = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: params.query,
template_type: params.templateType,
license_types: params.licenseTypes,
language: params.language,
jurisdiction: params.jurisdiction,
limit: params.limit || 10,
}),
signal: AbortSignal.timeout(5000),
})
if (res.ok) {
const data = await res.json()
return data.map((r: any) => ({
id: r.id,
score: r.score,
text: r.text,
documentTitle: r.document_title,
templateType: r.template_type,
clauseCategory: r.clause_category,
language: r.language,
jurisdiction: r.jurisdiction,
licenseId: r.license_id,
licenseName: r.license_name,
licenseUrl: r.license_url,
attributionRequired: r.attribution_required,
attributionText: r.attribution_text,
sourceName: r.source_name,
sourceUrl: r.source_url,
sourceRepo: r.source_repo,
placeholders: r.placeholders || [],
isCompleteDocument: r.is_complete_document,
isModular: r.is_modular,
requiresCustomization: r.requires_customization,
outputAllowed: r.output_allowed ?? true,
modificationAllowed: r.modification_allowed ?? true,
distortionProhibited: r.distortion_prohibited ?? false,
}))
}
} catch {
// KLAUSUR_SERVICE not reachable — fall through to RAG
}
// 2. Fallback: RAG proxy
try {
const res = await fetch(`${RAG_PROXY}/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: params.query || '', limit: params.limit || 10 }),
})
if (res.ok) {
const data = await res.json()
return (data.results || []).map((r: any, i: number) => ({
id: r.regulation_code || `rag-${i}`,
score: r.score ?? 0.5,
text: r.text || '',
documentTitle: r.regulation_name || r.regulation_short || 'Dokument',
templateType: 'regulation',
clauseCategory: null,
language: 'de',
jurisdiction: 'eu',
licenseId: null,
licenseName: null,
licenseUrl: null,
attributionRequired: false,
attributionText: null,
sourceName: 'RAG',
sourceUrl: null,
sourceRepo: null,
placeholders: [],
isCompleteDocument: false,
isModular: true,
requiresCustomization: true,
outputAllowed: true,
modificationAllowed: true,
distortionProhibited: false,
source: 'rag' as const,
}))
}
} catch {
// both services failed
}
return []
}
export async function getTemplatesStatus(): Promise<any> {
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/status`, {
signal: AbortSignal.timeout(5000),
})
if (!response.ok) return null
return response.json()
}
export async function getSources(): Promise<any[]> {
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/sources`, {
signal: AbortSignal.timeout(5000),
})
if (!response.ok) return []
const data = await response.json()
return data.sources || []
}