This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

320 lines
13 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* Education Search Page
* Bildungsquellen und Crawler-Verwaltung
*/
import { useState, useCallback } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
import { Search, Database, RefreshCw, ExternalLink, FileText, BookOpen, FolderOpen } from 'lucide-react'
import { DokumenteTab } from '@/components/education/DokumenteTab'
interface DataSource {
id: string
name: string
type: 'api' | 'crawler' | 'manual'
status: 'active' | 'inactive' | 'error'
lastUpdate?: string
documentCount: number
url?: string
}
const DATA_SOURCES: DataSource[] = [
{
id: 'nibis',
name: 'NiBiS (Niedersachsen)',
type: 'crawler',
status: 'active',
lastUpdate: '2026-01-20',
documentCount: 1250,
url: 'https://nibis.de',
},
{
id: 'kmk',
name: 'KMK Beschluesse',
type: 'crawler',
status: 'active',
lastUpdate: '2026-01-10',
documentCount: 450,
url: 'https://kmk.org',
},
]
export default function EduSearchPage() {
const [searchQuery, setSearchQuery] = useState('')
const [activeTab, setActiveTab] = useState<'search' | 'documents' | 'sources' | 'crawler'>('search')
const [documentCount, setDocumentCount] = useState<number>(0)
const handleDocumentCountChange = useCallback((count: number) => {
setDocumentCount(count)
}, [])
return (
<div className="space-y-6">
<PagePurpose
title="Education Search"
purpose="Durchsuchen Sie Bildungsquellen und verwalten Sie Crawler fuer Lehrplaene, Erlasse und Schulinformationen. Zentraler Zugang zu bildungsrelevanten Dokumenten."
audience={['Content Manager', 'Entwickler', 'Bildungs-Admins']}
architecture={{
services: ['edu-search-service (Go)', 'OpenSearch'],
databases: ['OpenSearch (bp_documents_v1)', 'PostgreSQL'],
}}
collapsible={true}
defaultCollapsed={true}
/>
{/* Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-3xl font-bold text-blue-600">
{DATA_SOURCES.reduce((sum, s) => sum + s.documentCount, 0).toLocaleString()}
</div>
<div className="text-sm text-slate-500">Dokumente gesamt</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-3xl font-bold text-green-600">{DATA_SOURCES.length}</div>
<div className="text-sm text-slate-500">Datenquellen</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-3xl font-bold text-purple-600">
{DATA_SOURCES.filter(s => s.type === 'crawler').length}
</div>
<div className="text-sm text-slate-500">Aktive Crawler</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="text-3xl font-bold text-orange-600">16</div>
<div className="text-sm text-slate-500">Bundeslaender</div>
</div>
</div>
{/* Tabs */}
<div className="flex gap-2">
<button
onClick={() => setActiveTab('search')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
activeTab === 'search'
? 'bg-blue-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<Search className="w-4 h-4 inline mr-2" />
Suche
</button>
<button
onClick={() => setActiveTab('documents')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
activeTab === 'documents'
? 'bg-blue-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<FolderOpen className="w-4 h-4 inline mr-2" />
Dokumente
{documentCount > 0 && (
<span className="ml-2 px-1.5 py-0.5 bg-white/20 rounded text-xs">
{documentCount}
</span>
)}
</button>
<button
onClick={() => setActiveTab('sources')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
activeTab === 'sources'
? 'bg-blue-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<Database className="w-4 h-4 inline mr-2" />
Datenquellen
</button>
<button
onClick={() => setActiveTab('crawler')}
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
activeTab === 'crawler'
? 'bg-blue-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
<RefreshCw className="w-4 h-4 inline mr-2" />
Crawler
</button>
</div>
{/* Search Tab */}
{activeTab === 'search' && (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex gap-4 mb-6">
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Suche nach Lehrplaenen, Erlassen, Curricula..."
className="flex-1 px-4 py-3 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 text-lg"
/>
<button className="px-8 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 font-medium">
Suchen
</button>
</div>
<div className="flex flex-wrap gap-2 mb-6">
<span className="text-sm text-slate-500 mr-2">Schnellfilter:</span>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Lehrplaene
</button>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Erlasse
</button>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Kerncurricula
</button>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Abitur
</button>
<button className="px-3 py-1 bg-slate-100 text-slate-700 rounded-full text-sm hover:bg-slate-200">
Niedersachsen
</button>
</div>
<div className="text-center py-12 text-slate-500">
<BookOpen className="w-16 h-16 mx-auto mb-4 opacity-50" />
<p>Geben Sie einen Suchbegriff ein, um Bildungsdokumente zu durchsuchen</p>
<p className="text-sm mt-2">Die Suche durchsucht alle angebundenen Datenquellen</p>
</div>
</div>
)}
{/* Documents Tab */}
{activeTab === 'documents' && (
<DokumenteTab onDocumentCountChange={handleDocumentCountChange} />
)}
{/* Sources Tab */}
{activeTab === 'sources' && (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left px-4 py-3 font-medium text-slate-600">Datenquelle</th>
<th className="text-left px-4 py-3 font-medium text-slate-600">Typ</th>
<th className="text-center px-4 py-3 font-medium text-slate-600">Status</th>
<th className="text-right px-4 py-3 font-medium text-slate-600">Dokumente</th>
<th className="text-left px-4 py-3 font-medium text-slate-600">Letztes Update</th>
<th className="text-center px-4 py-3 font-medium text-slate-600">Aktion</th>
</tr>
</thead>
<tbody>
{DATA_SOURCES.map((source) => (
<tr key={source.id} className="border-b border-slate-100 hover:bg-slate-50">
<td className="px-4 py-3">
<div className="flex items-center gap-2">
<Database className="w-4 h-4 text-slate-400" />
<div className="font-medium text-slate-900">{source.name}</div>
</div>
</td>
<td className="px-4 py-3">
<span className={`px-2 py-0.5 rounded-full text-xs ${
source.type === 'api' ? 'bg-blue-100 text-blue-700' :
source.type === 'crawler' ? 'bg-purple-100 text-purple-700' :
'bg-slate-100 text-slate-700'
}`}>
{source.type.toUpperCase()}
</span>
</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-0.5 rounded-full text-xs ${
source.status === 'active' ? 'bg-green-100 text-green-700' :
source.status === 'error' ? 'bg-red-100 text-red-700' :
'bg-slate-100 text-slate-700'
}`}>
{source.status === 'active' ? 'Aktiv' : source.status === 'error' ? 'Fehler' : 'Inaktiv'}
</span>
</td>
<td className="px-4 py-3 text-right font-medium">
{source.documentCount.toLocaleString()}
</td>
<td className="px-4 py-3 text-slate-500">
{source.lastUpdate || '-'}
</td>
<td className="px-4 py-3 text-center">
{source.url && (
<a
href={source.url}
target="_blank"
rel="noopener noreferrer"
className="p-1 text-blue-600 hover:bg-blue-50 rounded inline-block"
title="Quelle oeffnen"
>
<ExternalLink className="w-4 h-4" />
</a>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{/* Crawler Tab */}
{activeTab === 'crawler' && (
<div className="space-y-6">
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Crawler-Verwaltung</h3>
<p className="text-sm text-slate-600 mb-4">
Hier koennen Sie die Crawler fuer verschiedene Bildungsquellen steuern.
Das System crawlt ausschliesslich oeffentliche Bildungsdokumente (Lehrplaene, Erlasse, Curricula). Keine Personendaten.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="border border-slate-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<FileText className="w-5 h-5 text-purple-600" />
<span className="font-medium">NiBiS Crawler</span>
</div>
<p className="text-sm text-slate-600 mb-3">
Crawlt Lehrplaene und Erlasse aus Niedersachsen
</p>
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm">
Crawl starten
</button>
</div>
<div className="border border-slate-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-2">
<FileText className="w-5 h-5 text-blue-600" />
<span className="font-medium">KMK Crawler</span>
</div>
<p className="text-sm text-slate-600 mb-3">
Crawlt Beschluesse der Kultusministerkonferenz
</p>
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 text-sm">
Crawl starten
</button>
</div>
</div>
</div>
</div>
)}
{/* Info Box */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-6">
<h3 className="font-semibold text-blue-800 flex items-center gap-2">
<span></span>
Verwandte Module
</h3>
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-4">
<a href="/education/zeugnisse-crawler" className="block p-3 bg-white rounded-lg hover:shadow-md transition-shadow">
<div className="font-medium text-slate-900">Zeugnisse-Crawler</div>
<div className="text-sm text-slate-500">Zeugnis-Strukturen verwalten</div>
</a>
<a href="/ai/rag-pipeline" className="block p-3 bg-white rounded-lg hover:shadow-md transition-shadow">
<div className="font-medium text-slate-900">RAG Pipeline</div>
<div className="text-sm text-slate-500">Bildungsdokumente indexieren</div>
</a>
</div>
</div>
</div>
)
}