commit baee45b8610cc88e7ba2ce2af3c7e1f5d27cf662 Author: BreakPilot Dev Date: Sun Feb 8 21:31:35 2026 -0800 feat(ocr): Add Grid Detection v4 tests, docs, and SBOM update - Add comprehensive tests for grid_detection_service.py (31 tests) - mm coordinate conversion tests - Deskew calculation tests - Column detection tests - Integration tests for vocabulary tables - Add OCR-Compare documentation (OCR-Compare.md) - mm coordinate system documentation - Deskew correction documentation - Worksheet Editor integration guide - API endpoints documentation - Add TypeScript tests for ocr-integration.ts - mm to pixel conversion tests - OCR export format tests - localStorage operations tests - Update SBOM to v1.5.0 - Add OCR Grid Detection System section - Document Fabric.js (MIT) for Worksheet Editor - Document NumPy and OpenCV usage Co-Authored-By: Claude Opus 4.5 diff --git a/SBOM.md b/SBOM.md new file mode 100644 index 0000000..6302a25 --- /dev/null +++ b/SBOM.md @@ -0,0 +1,1204 @@ +# Software Bill of Materials (SBOM) +## BreakPilot PWA + +**Version:** 1.5.0 +**Letzte Aktualisierung:** 2026-02-08 +**Verantwortlicher:** BreakPilot Development Team + +--- + +## Übersicht + +Diese SBOM dokumentiert alle Open-Source-Komponenten, die in BreakPilot verwendet werden, einschließlich ihrer Lizenzen und Nutzungsbedingungen für den kommerziellen Einsatz. + +--- + +## 1. LibreChat (Chat-Interface) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | LibreChat | +| **Version** | latest (dev) | +| **Repository** | https://github.com/danny-avila/LibreChat | +| **Lizenz** | MIT License | +| **Copyright** | Copyright (c) 2025 LibreChat | +| **Kommerzielle Nutzung** | Erlaubt | +| **Attribution erforderlich** | Ja (Lizenztext beibehalten) | +| **Verwendungszweck** | Chat/Prompt-Oberfläche für Nutzer | + +### LibreChat Abhängigkeiten (Container) + +| Komponente | Version | Lizenz | Kommerzielle Nutzung | +|------------|---------|--------|----------------------| +| MongoDB | latest | SSPL-1.0 | Ja (mit Einschränkungen*) | +| Meilisearch | v1.12.3 | MIT | Ja | +| PostgreSQL (pgvector) | 0.8.0-pg15 | PostgreSQL License | Ja | +| LibreChat RAG API | latest (dev-lite) | MIT | Ja | + +> *MongoDB SSPL: Erlaubt kommerzielle Nutzung, solange MongoDB nicht als Service angeboten wird. + +--- + +## 2. BreakPilot Backend (Python/FastAPI) + +| Eigenschaft | Wert | +|-------------|------| +| **Framework** | FastAPI | +| **Lizenz** | MIT License | +| **Repository** | https://github.com/tiangolo/fastapi | +| **Kommerzielle Nutzung** | Erlaubt | + +### Python Dependencies + +| Paket | Version | Lizenz | Kommerzielle Nutzung | +|-------|---------|--------|----------------------| +| FastAPI | 0.123.9 | MIT | Ja | +| Uvicorn | 0.38.0 | BSD-3-Clause | Ja | +| Starlette | 0.49.3 | BSD-3-Clause | Ja | +| Pydantic | 2.12.5 | MIT | Ja | +| httpx | 0.28.1 | BSD-3-Clause | Ja | +| requests | 2.32.5 | Apache-2.0 | Ja | +| PyJWT | 2.10.1 | MIT | Ja | +| python-multipart | 0.0.20 | Apache-2.0 | Ja | +| Jinja2 | 3.1.6 | BSD-3-Clause | Ja | +| WeasyPrint | 66.0 | BSD-3-Clause | Ja | +| python-dateutil | 2.9.0 | Apache-2.0/BSD | Ja | +| python-docx | 1.2.0 | MIT | Ja | +| mammoth | 1.11.0 | BSD-2-Clause | Ja | +| Markdown | 3.9 | BSD-3-Clause | Ja | +| Pillow | 11.3.0 | HPND | Ja | +| opencv-python | 4.12.0 | Apache-2.0 | Ja | +| numpy | 2.0.2 | BSD-3-Clause | Ja | +| anthropic | 0.75.0 | MIT | Ja | +| email-validator | 2.3.0 | CC0-1.0 | Ja | +| PyJWKClient (PyJWT) | 2.10.1 | MIT | Ja | Keycloak JWKS Validierung | +| pytest | 8.4.2 | MIT | Ja | +| pytest-asyncio | 1.2.0 | Apache-2.0 | Ja | +| beautifulsoup4 | 4.12.3 | MIT | Ja | + +--- + +## 3. Consent Service (Go) + +| Eigenschaft | Wert | +|-------------|------| +| **Sprache** | Go 1.23+ | +| **Lizenz** | Proprietär (BreakPilot) | + +### Go Dependencies (Direkt) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | +|-------|---------|--------|----------------------| +| gin-gonic/gin | 1.11.0 | MIT | Ja | +| golang-jwt/jwt/v5 | 5.3.0 | MIT | Ja | +| google/uuid | 1.6.0 | BSD-3-Clause | Ja | +| jackc/pgx/v5 | 5.7.6 | MIT | Ja | +| joho/godotenv | 1.5.1 | MIT | Ja | +| skip2/go-qrcode | 0.0.0 | MIT | Ja | +| golang.org/x/crypto | 0.40.0 | BSD-3-Clause | Ja | + +### Go Dependencies (Indirekt/Transitiv) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | +|-------|---------|--------|----------------------| +| bytedance/sonic | 1.14.0 | Apache-2.0 | Ja | +| go-playground/validator/v10 | 10.27.0 | MIT | Ja | +| goccy/go-json | 0.10.2 | MIT | Ja | +| jackc/pgpassfile | 1.0.0 | MIT | Ja | +| jackc/pgservicefile | 1.0.0 | MIT | Ja | +| jackc/puddle/v2 | 2.2.2 | MIT | Ja | +| ugorji/go/codec | 1.3.0 | MIT | Ja | +| google.golang.org/protobuf | 1.36.9 | BSD-3-Clause | Ja | + +--- + +## 4. Matrix Synapse (Schulkommunikation) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | Matrix Synapse | +| **Version** | latest | +| **Repository** | https://github.com/element-hq/synapse | +| **Docker Image** | matrixdotorg/synapse:latest | +| **Lizenz** | AGPL-3.0 | +| **Copyright** | Copyright (c) 2014-2025 Element (formerly New Vector Ltd) | +| **Kommerzielle Nutzung** | Erlaubt (mit Bedingungen*) | +| **Attribution erforderlich** | Ja | +| **Verwendungszweck** | E2EE Messenger für Eltern-Lehrer-Kommunikation | + +> *AGPL-3.0: Kommerzielle Nutzung erlaubt. Wenn Sie Synapse modifizieren und als Service anbieten, müssen die Änderungen unter AGPL veröffentlicht werden. Bei reiner Nutzung ohne Modifikation keine zusätzlichen Pflichten. + +### Matrix Protocol + +| Komponente | Lizenz | Beschreibung | +|------------|--------|--------------| +| Matrix Protocol | Apache-2.0 | Offenes Kommunikationsprotokoll | +| Megolm (E2EE) | Apache-2.0 | Ende-zu-Ende-Verschlüsselung | + +### Matrix Service Dependencies (Go) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | +|-------|---------|--------|----------------------| +| google/uuid | 1.6.0 | BSD-3-Clause | Ja | +| net/http (stdlib) | Go 1.21+ | BSD-3-Clause | Ja | +| encoding/json (stdlib) | Go 1.21+ | BSD-3-Clause | Ja | + +--- + +## 5. Jitsi Meet (Videokonferenzen) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | Jitsi Meet | +| **Version** | stable-9823 | +| **Repository** | https://github.com/jitsi/jitsi-meet | +| **Docker Images** | jitsi/web, jitsi/prosody, jitsi/jicofo, jitsi/jvb | +| **Lizenz** | Apache-2.0 | +| **Copyright** | Copyright (c) 2013-2025 Atlassian Pty Ltd & Contributors | +| **Kommerzielle Nutzung** | Erlaubt | +| **Attribution erforderlich** | Ja (NOTICE-Datei beibehalten) | +| **Verwendungszweck** | Videokonferenzen für Schulungen, Elterngespräche | + +### Jitsi Komponenten + +| Komponente | Image | Lizenz | Beschreibung | +|------------|-------|--------|--------------| +| Jitsi Web | jitsi/web:stable-9823 | Apache-2.0 | Web-Frontend | +| Prosody | jitsi/prosody:stable-9823 | MIT | XMPP-Server | +| Jicofo | jitsi/jicofo:stable-9823 | Apache-2.0 | Conference Focus | +| Jitsi Videobridge | jitsi/jvb:stable-9823 | Apache-2.0 | WebRTC SFU | + +### Jitsi Abhängigkeiten + +| Komponente | Lizenz | Beschreibung | +|------------|--------|--------------| +| WebRTC | BSD-3-Clause | Echtzeit-Kommunikation | +| Olibs (Olm) | Apache-2.0 | Verschlüsselung | +| Ogg/Opus | BSD-3-Clause | Audio-Codec | +| VP8/VP9 | BSD-3-Clause | Video-Codec | + +### Jitsi Service Dependencies (Go) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | +|-------|---------|--------|----------------------| +| google/uuid | 1.6.0 | BSD-3-Clause | Ja | +| crypto/hmac (stdlib) | Go 1.21+ | BSD-3-Clause | Ja | +| encoding/base64 (stdlib) | Go 1.21+ | BSD-3-Clause | Ja | + +--- + +## 6. Datenbanken + +| Datenbank | Version | Lizenz | Kommerzielle Nutzung | Verwendung | +|-----------|---------|--------|----------------------|------------| +| PostgreSQL | 16-alpine | PostgreSQL License | Ja | Hauptdatenbank | +| PostgreSQL (Synapse) | 16-alpine | PostgreSQL License | Ja | Matrix Datenbank | +| pgvector Extension | 0.8.0 | PostgreSQL License | Ja | LibreChat RAG | +| MongoDB | latest | SSPL-1.0 | Ja* | LibreChat | +| Meilisearch | 1.12.3 | MIT | Ja | LibreChat Suche | + +--- + +## 7. Frontend (PWA) + +| Komponente | Lizenz | Kommerzielle Nutzung | +|------------|--------|----------------------| +| HTML5/CSS3/JS | N/A | N/A | +| Service Worker API | N/A | N/A | + +--- + +## 7a. Typografie & Schriftarten + +Diese Sektion dokumentiert alle verwendeten Schriftarten für Website, Marketing und E-Mails, um sicherzustellen, dass nur lizenzfreie Schriften verwendet werden. + +### Primäre Schriftart: Inter + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | Inter | +| **Designer** | Rasmus Andersson | +| **Version** | Variable Font | +| **Repository** | https://github.com/rsms/inter | +| **Lizenz** | SIL Open Font License 1.1 (OFL-1.1) | +| **Kommerzielle Nutzung** | Ja, uneingeschränkt | +| **Attribution erforderlich** | Nein | +| **Modifikation erlaubt** | Ja | + +### Verwendete Font-Weights + +| Weight | Name | Verwendung | +|--------|------|------------| +| 400 | Regular | Fließtext, Beschreibungen | +| 500 | Medium | Labels, Buttons | +| 600 | Semi-Bold | Überschriften H3-H6, Hervorhebungen | +| 700 | Bold | Überschriften H1-H2, CTAs | + +### Einbindungsmethode + +| Plattform | Methode | Datei | +|-----------|---------|-------| +| Website (Next.js) | Google Fonts CDN | `website/app/globals.css` | +| Admin Panel | Google Fonts CDN | `backend/frontend/static/css/studio.css` | +| Kundenportal | Google Fonts CDN | `backend/frontend/static/css/customer.css` | +| E-Mail Templates | System Font Fallback | Inline CSS | + +### Google Fonts Import + +```css +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); +``` + +### Font Stack (Fallback) + +```css +font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', Roboto, sans-serif; +``` + +### System-Fallback-Schriften + +| Betriebssystem | Fallback Font | Lizenz | +|----------------|---------------|--------| +| macOS/iOS | SF Pro Text | Apple Proprietary (vorinstalliert) | +| Windows | Segoe UI | Microsoft Proprietary (vorinstalliert) | +| Android | Roboto | Apache-2.0 (vorinstalliert) | +| Linux | Sans-Serif Default | Varies (system font) | + +### E-Mail-sichere Schriftarten + +Für E-Mail-Templates werden nur websichere Schriften verwendet: + +| Font | Fallback für | Lizenz | +|------|--------------|--------| +| Arial | Sans-Serif Fließtext | Monotype (lizenzfrei für E-Mail) | +| Helvetica | macOS/iOS | Apple (vorinstalliert) | +| Georgia | Serif (optional) | Microsoft (lizenzfrei für E-Mail) | + +### Icon-Fonts & Symbol-Sets + +| Name | Quelle | Lizenz | Verwendung | +|------|--------|--------|------------| +| Heroicons | heroicons.com | MIT | Admin Panel SVG Icons | +| System Emojis | OS-nativ | N/A | Feature-Icons, Bundesland-Marker | + +### Nicht verwendete / Ausgeschlossene Schriften + +| Schriftart | Grund für Ausschluss | +|------------|----------------------| +| Adobe Fonts (Typekit) | Erfordert Adobe-Lizenz | +| Google Fonts (proprietäre) | Nur OFL/Apache-lizenzierte | +| MyFonts/Linotype | Kommerzielle Lizenz erforderlich | +| Custom Brand Fonts | Lizenzkosten, keine Notwendigkeit | + +### Compliance-Checkliste (Typografie) + +- [x] Inter ist OFL-1.1 lizenziert (vollständig frei für kommerzielle Nutzung) +- [x] Keine proprietären Schriften eingebunden +- [x] System-Fallbacks sind auf allen Plattformen lizenzfrei +- [x] E-Mail-Templates verwenden nur websichere Schriften +- [x] SVG-Icons (Heroicons) sind MIT-lizenziert +- [x] Keine Schrift-Dateien (.woff/.ttf) mit unklarer Lizenz im Repository + +### Performance-Hinweis + +Google Fonts werden mit `display=swap` geladen, um FOUT (Flash of Unstyled Text) zu minimieren. Für Produktion kann eine lokale Einbindung über `/public/fonts/` erwogen werden. + +--- + +## Lizenz-Zusammenfassung + +### Erlaubte Lizenzen für kommerzielle Nutzung: + +| Lizenz | Bedingungen | +|--------|-------------| +| **MIT** | Copyright-Vermerk beibehalten | +| **BSD-2-Clause** | Copyright-Vermerk beibehalten | +| **BSD-3-Clause** | Copyright-Vermerk beibehalten, keine Endorsement-Nutzung | +| **Apache-2.0** | NOTICE-Datei beibehalten, Patent-Klausel beachten | +| **PostgreSQL License** | Copyright-Vermerk beibehalten | +| **SSPL-1.0** | Kein MongoDB-as-a-Service anbieten | +| **AGPL-3.0** | Bei Modifikation + Service: Quellcode veröffentlichen | + +--- + +## Compliance-Checkliste + +- [x] Alle Open-Source-Lizenzen erlauben kommerzielle Nutzung +- [x] MIT-Lizenztexte werden mit dem Produkt ausgeliefert +- [x] Copyright-Hinweise sind dokumentiert +- [x] SSPL-Einschränkungen für MongoDB werden eingehalten (kein DBaaS) +- [x] AGPL-3.0-Einschränkungen für Matrix Synapse beachtet (keine Modifikationen) +- [x] SBOM wird regelmäßig aktualisiert + +--- + +## 8. LLM Platform (geplant) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | BreakPilot LLM Platform | +| **Status** | In Planung | +| **Dokumentation** | [/docs/llm-platform/README.md](/docs/llm-platform/README.md) | + +### LLM Inference Stack + +| Komponente | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|------------|---------|--------|----------------------|--------------| +| vLLM | latest | Apache-2.0 | Ja | OpenAI-kompatible Inference Engine | +| Ollama | latest | MIT | Ja | Lokale Entwicklung & Fallback | + +### LLM Modelle + +| Modell | Lizenz | Kommerzielle Nutzung | Einschränkungen | +|--------|--------|----------------------|-----------------| +| Llama 3.1 (8B/70B) | Llama 3.1 Community License | Ja | >700M MAU erfordert Meta-Lizenzvereinbarung | +| Mistral 7B | Apache-2.0 | Ja | Keine | +| Mixtral 8x7B | Apache-2.0 | Ja | Keine | +| Claude API | Proprietär (Anthropic) | Ja | Pay-per-use, API Terms | + +> **Llama 3.1 License:** Die Llama 3.1 Community License erlaubt kommerzielle Nutzung. Bei mehr als 700 Millionen monatlich aktiven Nutzern ist eine separate Lizenzvereinbarung mit Meta erforderlich. Für BreakPilot (Schulkontext) ist dies nicht relevant. + +### LLM Gateway Dependencies (Python) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | +|-------|---------|--------|----------------------| +| sse-starlette | 2.x | BSD-3-Clause | Ja | +| feedparser | 6.x | BSD-2-Clause | Ja | +| simhash | 2.x | MIT | Ja | +| tavily-python | latest | MIT | Ja | + +### Hosting (Phase 1) + +| Anbieter | Dienst | Datenschutz | +|----------|--------|-------------| +| vast.ai | GPU-Mietung | US-basiert, nur für Inference (keine PII) | +| Bestehende Infra | Gateway, DB | DE-hosted | + +--- + +## 9. Education Search Service (edu-search-service) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | BreakPilot Education Search Service | +| **Status** | In Entwicklung (Phase 0 PoC) | +| **Sprache** | Go 1.21+ | +| **Lizenz** | Proprietär (BreakPilot) | +| **Verwendungszweck** | EU-gehostete Bildungsquellen-Suche als Tavily-Alternative | + +### Go Dependencies (Direkt) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|---------|--------|----------------------|--------------| +| gin-gonic/gin | 1.9.1 | MIT | Ja | HTTP Web Framework | +| opensearch-project/opensearch-go/v2 | 2.3.0 | Apache-2.0 | Ja | OpenSearch Client | +| google/uuid | 1.5.0 | BSD-3-Clause | Ja | UUID Generierung | +| gopkg.in/yaml.v3 | 3.0.1 | MIT | Ja | YAML Parser | +| PuerkitoBio/goquery | 1.8.1 | BSD-3-Clause | Ja | HTML Parsing/Scraping | +| ledongthuc/pdf | 0.0.0 | MIT | Ja | PDF Text Extraction | +| golang.org/x/net | 0.19.0 | BSD-3-Clause | Ja | Netzwerk-Utilities | +| golang.org/x/text | 0.14.0 | BSD-3-Clause | Ja | Text/Encoding | + +### Infrastructure Dependencies + +| Komponente | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|------------|---------|--------|----------------------|--------------| +| OpenSearch | 2.x | Apache-2.0 | Ja | Such-Engine & Index | + +> **Hinweis:** OpenSearch ist ein Fork von Elasticsearch unter Apache-2.0 Lizenz und damit uneingeschränkt kommerziell nutzbar. + +--- + +## 10. Legal Crawler (Backend Module) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | BreakPilot Legal Crawler | +| **Status** | Aktiv | +| **Sprache** | Python 3.10+ | +| **Lizenz** | Proprietär (BreakPilot) | +| **Verwendungszweck** | Crawlt Schulgesetze und rechtliche Inhalte aller 16 Bundesländer | + +### Python Dependencies (Legal Crawler spezifisch) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|---------|--------|----------------------|--------------| +| beautifulsoup4 | 4.12.3 | MIT | Ja | HTML Parsing | +| httpx | 0.28.1 | BSD-3-Clause | Ja | Async HTTP Client | + +### Gecrawlte Quellen (Legal Seeds) + +| Bundesland | Quelle | Typ | +|------------|--------|-----| +| Baden-Württemberg | landesrecht-bw.de | Schulgesetz | +| Bayern | gesetze-bayern.de | BayEUG | +| Berlin | gesetze.berlin.de | Schulgesetz | +| Brandenburg | bravors.brandenburg.de | BbgSchulG | +| Bremen | transparenz.bremen.de | BremSchulG | +| Hamburg | landesrecht-hamburg.de | HmbSG | +| Hessen | rv.hessenrecht.hessen.de | HSchG | +| Mecklenburg-Vorpommern | landesrecht-mv.de | SchulG M-V | +| Niedersachsen | voris.niedersachsen.de | NSchG | +| Nordrhein-Westfalen | bass.schule.nrw | SchulG NRW | +| Rheinland-Pfalz | landesrecht.rlp.de | SchulG | +| Saarland | sl.juris.de | SchoG | +| Sachsen | revosax.sachsen.de | SächsSchulG | +| Sachsen-Anhalt | landesrecht.sachsen-anhalt.de | SchulG LSA | +| Schleswig-Holstein | gesetze-rechtsprechung.sh.juris.de | SchulG SH | +| Thüringen | landesrecht.thueringen.de | ThürSchulG | + +> **Hinweis:** Alle gecrawlten Inhalte sind öffentlich zugängliche Rechtstexte. Der Crawler respektiert robots.txt und Rate-Limiting. + +--- + +## 11. ERPNext (Enterprise Resource Planning) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | ERPNext | +| **Version** | latest (v15+) | +| **Framework** | Frappe Framework | +| **Repository** | https://github.com/frappe/erpnext | +| **Docker Image** | frappe/erpnext:latest | +| **Lizenz** | GNU General Public License v3.0 (GPLv3) | +| **Copyright** | Copyright (c) 2013-2025 Frappe Technologies Pvt. Ltd. | +| **Kommerzielle Nutzung** | Erlaubt (mit Bedingungen*) | +| **Attribution erforderlich** | Ja | +| **Verwendungszweck** | Vollständiges ERP-System für Buchhaltung, HR, Billing & Projekte | + +> *GPLv3: Kommerzielle Nutzung erlaubt. Wenn Sie ERPNext modifizieren und verteilen, müssen die Änderungen unter GPLv3 veröffentlicht werden. Bei reiner Nutzung ohne Modifikation keine zusätzlichen Pflichten. + +### ERPNext Features + +| Feature | Beschreibung | Verwendung in BreakPilot | +|---------|--------------|--------------------------| +| **Accounting** | Double-Entry Buchhaltung | Finanzmanagement für Schulen | +| **Invoicing** | Rechnungsstellung + Abos | Automatische Billing für Services | +| **HR & Payroll** | Personalverwaltung + Lohnabrechnung | Lehrerverträge (zukünftig) | +| **Project Management** | Projekte & Dienstleistungen | Bildungsprojekte verwalten | +| **Banking** | Payables, Receivables | Zahlungsverkehr | +| **Expenses** | Spesen & Reisekosten | Kostenverwaltung | + +### Frappe Framework (ERPNext Basis) + +| Eigenschaft | Wert | +|-------------|------| +| **Framework** | Frappe Framework | +| **Sprache** | Python 3.10+ | +| **Repository** | https://github.com/frappe/frappe | +| **Lizenz** | MIT License | +| **Kommerzielle Nutzung** | Ja | + +### ERPNext Service Stack + +| Service | Image/Version | Lizenz | Beschreibung | +|---------|--------------|--------|--------------| +| ERPNext Application | frappe/erpnext:latest | GPLv3 | Hauptanwendung | +| Frappe Framework | frappe/erpnext:latest | MIT | Python Framework | +| Nginx | (embedded) | BSD-2-Clause | Web Server | +| Node.js | (embedded) | MIT | WebSocket Server | +| MariaDB | 10.6 | GPL-2.0 | Datenbank | +| Redis | alpine | BSD-3-Clause | Cache & Queue | + +### Python Dependencies (ERPNext/Frappe) + +| Paket | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|--------|----------------------|--------------| +| Werkzeug | BSD-3-Clause | Ja | WSGI Utilities | +| Jinja2 | BSD-3-Clause | Ja | Template Engine | +| SQLAlchemy | MIT | Ja | ORM | +| Babel | BSD-3-Clause | Ja | Internationalisierung | +| Pillow | HPND | Ja | Image Processing | +| PyMySQL | MIT | Ja | MySQL Client | +| gevent | MIT | Ja | Async Networking | +| gunicorn | MIT | Ja | WSGI Server | + +### JavaScript Dependencies (ERPNext Frontend) + +| Paket | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|--------|----------------------|--------------| +| Vue.js | MIT | Ja | Frontend Framework | +| Chart.js | MIT | Ja | Visualisierung | +| Socket.IO | MIT | Ja | Real-time Communication | +| Frappe UI | MIT | Ja | UI Components | + +### ERPNext Deployment + +| Komponente | Container | Port | Beschreibung | +|------------|-----------|------|--------------| +| Frontend | breakpilot-pwa-erpnext-frontend | 8090 | Nginx Reverse Proxy | +| Backend | breakpilot-pwa-erpnext-backend | 8000 (intern) | Python/Frappe App | +| WebSocket | breakpilot-pwa-erpnext-websocket | 9000 (intern) | Real-time Updates | +| Scheduler | breakpilot-pwa-erpnext-scheduler | - | Background Jobs | +| Worker (Long) | breakpilot-pwa-erpnext-worker-long | - | Long-running Tasks | +| Worker (Short) | breakpilot-pwa-erpnext-worker-short | - | Short Tasks | +| MariaDB | breakpilot-pwa-erpnext-db | 3306 (intern) | Database | +| Redis Queue | breakpilot-pwa-erpnext-redis-queue | 6379 (intern) | Job Queue | +| Redis Cache | breakpilot-pwa-erpnext-redis-cache | 6379 (intern) | App Cache | + +> **Lizenz-Compliance:** ERPNext steht unter GPLv3. BreakPilot nutzt ERPNext als eigenständigen Service ohne Modifikationen. Die GPLv3-Lizenz erlaubt dies ohne zusätzliche Verpflichtungen, solange ERPNext nicht modifiziert und redistribuiert wird. + +--- + +## 12. Keycloak Integration (Authentifizierung) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | Keycloak (Optional) | +| **Version** | 24.x+ | +| **Repository** | https://github.com/keycloak/keycloak | +| **Lizenz** | Apache-2.0 | +| **Copyright** | Copyright (c) Red Hat, Inc. | +| **Kommerzielle Nutzung** | Erlaubt | +| **Verwendungszweck** | Identity & Access Management (IAM) fuer Produktion | +| **Status** | Optional (Fallback: Lokales JWT) | + +### Architektur: Hybrid-Authentifizierung + +BreakPilot verwendet einen Hybrid-Ansatz: + +| Modus | Beschreibung | Empfohlen fuer | +|-------|--------------|----------------| +| **Lokales JWT** | Eigene JWT-Tokens mit HS256 | Entwicklung, Tests | +| **Keycloak** | JWKS-validierte RS256-Tokens | Produktion | + +### Keycloak Komponenten (wenn aktiviert) + +| Komponente | Beschreibung | Lizenz | +|------------|--------------|--------| +| Keycloak Server | IAM Server | Apache-2.0 | +| JWKS Endpoint | Public Key Distribution | Apache-2.0 | +| Realm Configuration | Multi-Tenant Support | Apache-2.0 | + +### Python Dependencies (Keycloak) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|---------|--------|----------------------|--------------| +| PyJWT | 2.10.1 | MIT | Ja | JWT Decoding & Validation | +| httpx | 0.28.1 | BSD-3-Clause | Ja | Async HTTP Client fuer JWKS | +| cryptography | latest | Apache-2.0/BSD | Ja | RSA Key Verification | + +### Rollentrennung + +| Schicht | Verantwortung | Implementierung | +|---------|---------------|-----------------| +| **Authentifizierung** | "Wer bist du?" | Keycloak (Produktion) oder lokales JWT | +| **Autorisierung** | "Was darfst du?" | Eigenes rbac.py (domaenenspezifisch) | + +> **Compliance Note:** Keycloak steht unter Apache-2.0 Lizenz und ist vollstaendig fuer kommerzielle Nutzung freigegeben. Die Integration ist optional - ohne Keycloak-Konfiguration verwendet BreakPilot automatisch lokale JWT-Authentifizierung. + +--- + +## 13. Tool-Integrationen (geplant) + +| Tool | Anbieter | Lizenz/API | Kommerzielle Nutzung | Beschreibung | +|------|----------|------------|----------------------|--------------| +| Tavily Search | Tavily AI | Proprietär (API) | Ja (Pay-per-use) | Web Search für LLM Tool-Calling | + +> **Datenschutz-Hinweis:** Alle Anfragen an externe Tools (Tavily) durchlaufen einen PII-Redaction-Filter. Keine personenbezogenen Daten werden an externe Dienste übermittelt. + +--- + +## 13. Content Service (Educational Content Platform) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | BreakPilot Content Service | +| **Version** | 1.0.0 | +| **Status** | Produktionsbereit | +| **Sprache** | Python 3.11 + Node.js 20 | +| **Lizenz** | Proprietär (BreakPilot) | +| **Verwendungszweck** | Educational Content Management mit Creative Commons Licensing | + +### Content Service Stack + +| Service | Port | Framework | Beschreibung | +|---------|------|-----------|--------------| +| Content Service API | 8002 | FastAPI | Content CRUD, Rating, Analytics | +| H5P Service | 8003 | Express | Interactive Content Editor & Player | +| AI Content Generator | 8004 | FastAPI | KI-gestützte H5P Content-Generierung | +| MinIO Storage | 9000-9001 | MinIO | S3-compatible Object Storage | +| Content Database | 5433 | PostgreSQL 16 | Metadata Storage | + +### 13.1 Content Service API (FastAPI) + +| Eigenschaft | Wert | +|-------------|------| +| **Framework** | FastAPI | +| **Base Image** | python:3.11-slim | +| **Lizenz** | MIT License | +| **Port** | 8002 | + +#### Python Dependencies (Content Service) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|---------|--------|----------------------|--------------| +| fastapi | ^0.109.0 | MIT | Ja | Web Framework | +| uvicorn[standard] | ^0.27.0 | BSD-3-Clause | Ja | ASGI Server | +| sqlalchemy | ^2.0.25 | MIT | Ja | Database ORM | +| psycopg2-binary | ^2.9.9 | LGPL-3.0 | Ja | PostgreSQL Driver | +| pydantic | ^2.5.3 | MIT | Ja | Data Validation | +| minio | ^7.2.3 | Apache-2.0 | Ja | S3 Storage Client | +| python-multipart | ^0.0.6 | Apache-2.0 | Ja | Form Data Parsing | +| python-jose[cryptography] | ^3.3.0 | MIT | Ja | JWT Handling | +| passlib[bcrypt] | ^1.7.4 | BSD | Ja | Password Hashing | +| matrix-nio | ^0.24.0 | ISC | Ja | Matrix Client for Feed Publishing | + +### 13.2 H5P Service (Node.js) - Simplified Implementation + +| Eigenschaft | Wert | +|-------------|------| +| **Framework** | Express (Simplified) | +| **Base Image** | node:20-alpine | +| **Lizenz** | MIT (Proprietäre Editoren) | +| **Port** | 8003 (8080 internal) | +| **Status** | Produktionsbereit (8 Content-Typen) | + +#### Node.js Dependencies (H5P Service) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|---------|--------|----------------------|--------------| +| express | ^4.18.2 | MIT | Ja | Web Framework | +| cors | ^2.8.5 | MIT | Ja | CORS Middleware | + +> **Implementation Note:** Vereinfachter H5P Service ohne externe GPL-3.0 Libraries. Alle Editoren und Player sind proprietäre HTML/CSS/JS Implementierungen unter MIT-kompatibler Lizenz. + +#### H5P Content Types (Proprietäre Implementation) + +| Content Type | Status | Beschreibung | +|--------------|--------|--------------| +| Quiz (Question Set) | ✅ | Multiple-Choice Tests mit Feedback | +| Interactive Video | ✅ | Videos mit zeitbasierten Interaktionen (YouTube, Vimeo, MP4) | +| Course Presentation | ✅ | Multi-Slide Präsentationen mit Navigation | +| Flashcards | ✅ | Lernkarten zum Wiederholen | +| Timeline | ✅ | Chronologische Zeitstrahle | +| Drag and Drop | ✅ | Zuordnungsaufgaben mit HTML5 Drag API | +| Fill in the Blanks | ✅ | Lückentexte mit automatischer Korrektur | +| Memory Game | ✅ | Klassisches Memory-Spiel | + +#### H5P Service Architecture + +| Komponente | Technologie | Lizenz | +|------------|-------------|--------| +| Server | Express.js | MIT | +| Editors | HTML5/CSS3/Vanilla JS | Proprietär (BreakPilot) | +| Players | HTML5/CSS3/Vanilla JS | Proprietär (BreakPilot) | +| Storage | LocalStorage (Browser) | N/A | +| Video Integration | YouTube/Vimeo iFrame API | Proprietär (API Terms) | + +> **Compliance Note:** Keine GPL-3.0 Dependencies. Vollständig kommerziell nutzbar ohne Copyleft-Verpflichtungen. + +### 13.3 AI Content Generator (Python/FastAPI) + +| Eigenschaft | Wert | +|-------------|------| +| **Framework** | FastAPI | +| **Base Image** | python:3.11-slim | +| **Lizenz** | MIT License (Dependencies) / Proprietär (Code) | +| **Port** | 8004 | +| **Status** | Produktionsbereit | + +#### Hauptfunktion +Automatische Generierung aller 8 H5P Content-Typen aus hochgeladenen Lernmaterialien mittels Claude AI und YouTube-Integration. + +#### Python Dependencies (AI Content Generator) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|---------|--------|----------------------|--------------| +| fastapi | ^0.115.6 | MIT | Ja | Web Framework | +| uvicorn | ^0.34.0 | BSD-3-Clause | Ja | ASGI Server | +| anthropic | ^0.42.0 | MIT | Ja | Claude API Client | +| youtube-transcript-api | ^0.6.3 | MIT | Ja | YouTube Transkript-Extraktion | +| PyPDF2 | ^3.0.1 | BSD-3-Clause | Ja | PDF Text-Extraktion | +| Pillow | ^11.0.0 | HPND | Ja | Image Processing | +| pytesseract | ^0.3.14 | Apache-2.0 | Ja | OCR Text Recognition | +| python-docx | ^1.1.2 | MIT | Ja | Word Document Processing | +| mammoth | ^1.8.0 | BSD-2-Clause | Ja | DOCX to Text Conversion | +| python-multipart | ^0.0.20 | Apache-2.0 | Ja | File Upload Handling | + +#### System Dependencies + +| Komponente | Lizenz | Kommerzielle Nutzung | Beschreibung | +|------------|--------|----------------------|--------------| +| Tesseract OCR | Apache-2.0 | Ja | Optical Character Recognition Engine | +| Poppler Utils | GPL-2.0 | Ja (nicht modifiziert) | PDF Rendering Library | + +#### Externe APIs (Cloud Services) + +| Service | Verwendung | Kosten | Lizenz/Terms | +|---------|------------|--------|--------------| +| Anthropic Claude API | Content-Generierung | Pay-per-use | [Anthropic Terms](https://www.anthropic.com/legal/commercial-terms) | +| YouTube Data API (optional) | Video-Suche | Kostenlos (Quota) | [YouTube API Terms](https://developers.google.com/youtube/terms) | +| YouTube Transcript API | Transkript-Abruf | Kostenlos | Public API | + +#### Generierte Content-Typen + +| Content Type | Input | AI-Technologie | +|--------------|-------|----------------| +| Quiz | Lernmaterialien | Claude (Sonnet 4.5) | +| Interactive Video | YouTube URL + Transkript | Claude + Transcript API | +| Course Presentation | Lernmaterialien | Claude (Sonnet 4.5) | +| Flashcards | Lernmaterialien | Claude (Sonnet 4.5) | +| Timeline | Lernmaterialien | Claude (Sonnet 4.5) | +| Drag and Drop | Lernmaterialien | Claude (Sonnet 4.5) | +| Fill in the Blanks | Lernmaterialien | Claude (Sonnet 4.5) | +| Memory Game | Lernmaterialien | Claude (Sonnet 4.5) | + +#### Material-Analyse Capabilities + +| Dateityp | Processing Library | Extraction Method | +|----------|-------------------|-------------------| +| PDF | PyPDF2 | Text Extraction | +| PNG/JPG | Pillow + Tesseract | OCR (Optical Character Recognition) | +| DOCX | python-docx / mammoth | Document Parsing | +| TXT | Python stdlib | Plain Text Reading | + +> **Compliance Note:** Alle Dependencies sind MIT, BSD oder Apache-2.0 lizenziert. Tesseract und Poppler werden als unveränderter Binary verwendet (GPL-Compliance gegeben). Anthropic Claude API erfordert kommerzielle Lizenz. + +> **Cost Note:** Anthropic Claude API ist kostenpflichtig (Pay-per-token). YouTube Data API hat kostenlose Quota-Limits. + +### 13.4 MinIO S3 Storage + +| Eigenschaft | Wert | +|-------------|------| +| **Image** | minio/minio:latest | +| **Ports** | 9000 (API), 9001 (Console) | +| **Lizenz** | AGPL-3.0 | +| **Verwendung** | Media File Storage (Videos, PDFs, Images) | + +> **AGPL-3.0**: Kommerzielle Nutzung erlaubt. Wenn MinIO modifiziert und als Service angeboten wird, müssen Änderungen unter AGPL veröffentlicht werden. + +### 13.5 Content Database (PostgreSQL) + +| Eigenschaft | Wert | +|-------------|------| +| **Image** | postgres:16-alpine | +| **Port** | 5433 (external), 5432 (internal) | +| **Lizenz** | PostgreSQL License | +| **Database** | breakpilot_content | + +### Content Platform Features + +| Feature | Status | Beschreibung | +|---------|--------|--------------| +| **Content CRUD** | ✅ | Create, Read, Update, Delete für Educational Content | +| **Creative Commons Licensing** | ✅ | CC-BY, CC-BY-SA, CC-BY-NC, CC-BY-NC-SA, CC0 | +| **H5P Interactive Content** | ✅ | Self-hosted H5P Editor & Player | +| **Matrix Feed Integration** | ✅ | Automatisches Publishing zu Matrix Spaces | +| **Rating System** | ✅ | 5-Star Ratings mit Kommentaren | +| **Download Tracking** | ✅ | Analytics & Impact Scoring | +| **DSGVO Compliance** | ✅ | Data Minimization, IP Anonymization | +| **S3 File Storage** | ✅ | MinIO für Videos, PDFs, Bilder | + +### Supported Content Types + +| Content Type | Format | Beschreibung | +|--------------|--------|--------------| +| VIDEO | MP4, WebM | Video Content | +| PDF | PDF | PDF Documents | +| IMAGE_GALLERY | JPG, PNG, WebP | Image Collections | +| MARKDOWN | .md | Markdown Documents | +| AUDIO | MP3, OGG, WebM | Audio Files | +| H5P | .h5p | Interactive H5P Packages | + +### Creative Commons Licenses + +| License | Beschreibung | Commercial Use | +|---------|--------------|----------------| +| CC-BY-4.0 | Attribution | ✅ | +| CC-BY-SA-4.0 | Attribution + ShareAlike | ✅ (Recommended) | +| CC-BY-NC-4.0 | Attribution + NonCommercial | ⚠️ | +| CC-BY-NC-SA-4.0 | Attribution + NonCommercial + ShareAlike | ⚠️ | +| CC0-1.0 | Public Domain | ✅ | + +### Content Categories + +| Category | Beschreibung | +|----------|--------------| +| MOVEMENT | Bewegungspausen & Physical Activities | +| MATH | Mathematik-Übungen | +| STEAM | Science, Technology, Engineering, Arts, Math | +| LANGUAGE | Sprachlernen | +| ARTS | Kreative Künste | +| SOCIAL | Social-Emotional Learning | +| MINDFULNESS | Achtsamkeit & Meditation | + +### Security & Privacy (Content Service) + +| Feature | Status | Beschreibung | +|---------|--------|--------------| +| **Data Minimization** | ✅ | Nur notwendige Metadaten gespeichert | +| **IP Anonymization** | ✅ | IP-Adressen nach 7 Tagen anonymisiert | +| **User Data Export** | ✅ | DSGVO-konformer Datenexport | +| **Account Deletion** | ✅ | Vollständige Datenlöschung möglich | +| **No Student Data** | ✅ | Keine Schülerdaten erfasst | +| **JWT Authentication** | 🔜 | OAuth2 via consent-service (pending) | +| **HTTPS/TLS** | 🔜 | Production requirement | + +### Docker Volumes + +| Volume | Verwendung | +|--------|------------| +| minio_data | Object Storage für Media Files | +| content_db_data | PostgreSQL Database | +| h5p_content | H5P Content Files | + +> **Lizenz-Compliance:** Die Content Service Plattform nutzt hauptsächlich MIT/Apache-2.0 lizenzierte Komponenten. H5P Libraries sind unter GPL-3.0, was kommerzielle Nutzung erlaubt. + +--- + +## Aktualisierungsprotokoll + +| Datum | Änderung | Verantwortlich | +|-------|----------|----------------| +| 2025-12-14 | Initiale SBOM erstellt | Claude Code | +| 2025-12-14 | LibreChat hinzugefügt | Claude Code | +| 2025-12-15 | Matrix Synapse hinzugefügt (AGPL-3.0) | Claude Code | +| 2025-12-15 | PostgreSQL für Synapse-DB hinzugefügt | Claude Code | +| 2025-12-15 | Jitsi Meet hinzugefügt (Apache-2.0) | Claude Code | +| 2025-12-15 | Go Dependencies aktualisiert (gin-gonic/gin, jackc/pgx) | Claude Code | +| 2025-12-15 | Python Dependencies aktualisiert (mammoth, python-docx für DSR) | Claude Code | +| 2025-12-15 | LLM Platform Komponenten hinzugefügt (vLLM, Ollama, Llama 3.1, Mistral) | Claude Code | +| 2025-12-15 | Tool-Integrationen Sektion hinzugefügt (Tavily) | Claude Code | +| 2025-12-16 | Education Search Service hinzugefügt (OpenSearch, goquery, pdf) | Claude Code | +| 2025-12-17 | Legal Crawler Modul hinzugefügt (beautifulsoup4) | Claude Code | +| 2025-12-17 | Alle 16 Bundesländer Schulgesetz-Quellen dokumentiert | Claude Code | +| 2025-12-29 | ERPNext hinzugefügt (GPLv3, Frappe Framework) | Claude Code | +| 2025-12-29 | MariaDB 10.6 für ERPNext hinzugefügt (GPL-2.0) | Claude Code | +| 2025-12-29 | Redis Cache & Queue für ERPNext hinzugefügt | Claude Code | +| 2025-12-30 | Content Service hinzugefügt (FastAPI, H5P, MinIO) | Claude Code | +| 2025-12-30 | H5P Service vereinfacht: Proprietäre Editoren/Players ohne GPL-3.0 | Claude Code | +| 2025-12-30 | Alle 8 H5P Content-Typen implementiert (Quiz, Video, Presentation, etc.) | Claude Code | +| 2025-12-30 | Creative Commons Licensing System dokumentiert | Claude Code | +| 2025-12-30 | Matrix Feed Integration für Content Publishing | Claude Code | +| 2025-12-30 | AI Content Generator Service hinzugefügt (Claude API, YouTube Transcript) | Claude Code | +| 2025-12-30 | Material-Analyse implementiert (PDF, DOCX, Images mit OCR) | Claude Code | +| 2025-12-30 | Automatische H5P-Generierung für alle 8 Content-Typen | Claude Code | +| 2026-01-09 | Keycloak-Integration (Hybrid-Auth) hinzugefügt | Claude Code | +| 2026-01-09 | PyJWKClient für JWKS-Validierung dokumentiert | Claude Code | +| 2026-01-09 | HashiCorp Vault für Secrets-Management hinzugefügt | Claude Code | +| 2026-01-09 | Sicherheitsaudit: Hardcodierte Secrets entfernt | Claude Code | +| 2026-01-09 | DevSecOps-Stack integriert (Gitleaks, Semgrep, Trivy, Syft/Grype) | Claude Code | +| 2026-01-09 | Pre-Commit Security Hooks aktualisiert (Bandit, Semgrep) | Claude Code | +| 2026-01-10 | Sektion 7a: Typografie & Schriftarten hinzugefügt | Claude Code | +| 2026-01-10 | Inter Font (OFL-1.1) als primäre Schrift dokumentiert | Claude Code | +| 2026-01-10 | E-Mail-sichere Schriften und Icon-Fonts dokumentiert | Claude Code | +| 2026-01-10 | Compliance-Checkliste für Typografie erstellt | Claude Code | +| 2026-02-08 | Sektion 15: Klausur-Service RAG System hinzugefügt | Claude Code | +| 2026-02-08 | Model Cards für BAAI/bge-m3 und bge-reranker-v2-m3 dokumentiert | Claude Code | +| 2026-02-08 | MS MARCO Modelle als nicht empfohlen markiert (Lizenzproblem) | Claude Code | +| 2026-02-08 | PyMuPDF aus Default-Build entfernt (AGPL-3.0) | Claude Code | +| 2026-02-08 | HyDE/Self-RAG Datenschutz-Warnungen dokumentiert | Claude Code | +| 2026-02-08 | RAG Feature-Status-Tabelle hinzugefügt (implementiert vs. aktiv vs. extern) | Claude Code | + +--- + +## 13. HashiCorp Vault (Secrets Management) + +### Vault Server + +| Komponente | Version | Lizenz | Verwendung | +|------------|---------|--------|------------| +| **HashiCorp Vault** | 1.15 | [BSL 1.1](https://github.com/hashicorp/vault/blob/main/LICENSE) | Secrets-Management | +| **hvac** (Python Client) | 2.1.0 | [Apache-2.0](https://github.com/hvac/hvac/blob/main/LICENSE.txt) | Python Vault Client | + +### Lizenz-Hinweis + +HashiCorp Vault ist unter der **Business Source License 1.1 (BSL 1.1)** lizenziert: +- **Kostenlose Nutzung** für alle Zwecke (einschließlich kommerzielle) erlaubt +- **Einzige Einschränkung**: Vault darf nicht als Hosted/Managed Service angeboten werden +- Nach 4 Jahren wechselt die Lizenz automatisch zu Open Source (MPL 2.0) + +Für BreakPilot ist die Nutzung vollständig konform, da Vault nur intern verwendet wird. + +### Konfiguration + +| Feature | Status | Beschreibung | +|---------|--------|--------------| +| **KV v2 Engine** | ✅ | Key-Value Secrets mit Versionierung | +| **AppRole Auth** | ✅ | Service-to-Service Authentication | +| **Token Auth** | ✅ | Development Mode | +| **Kubernetes Auth** | 🔜 | Für K8s Deployments | +| **TLS/mTLS** | 🔜 | Production Requirement | + +### Gespeicherte Secrets + +| Pfad | Typ | Beschreibung | +|------|-----|--------------| +| `secret/breakpilot/api_keys/anthropic` | API Key | Anthropic Claude API | +| `secret/breakpilot/api_keys/vast` | API Key | vast.ai GPU | +| `secret/breakpilot/api_keys/stripe` | API Key | Stripe Payments | +| `secret/breakpilot/database/postgres` | Credentials | PostgreSQL | +| `secret/breakpilot/auth/jwt` | Secret | JWT Signing Keys | +| `secret/breakpilot/auth/keycloak` | Secret | Keycloak Client | +| `secret/breakpilot/communication/matrix` | Token | Matrix Access Token | +| `secret/breakpilot/communication/jitsi` | Secret | Jitsi Auth Secrets | +| `secret/breakpilot/storage/minio` | Credentials | MinIO Object Storage | + +### Docker Volumes + +| Volume | Verwendung | +|--------|------------| +| vault_data | Vault Storage Backend | +| vault_logs | Audit Logs | + +### Sicherheitsfeatures + +| Feature | Status | Beschreibung | +|---------|--------|--------------| +| **Audit Logging** | ✅ | Alle Zugriffe werden geloggt | +| **Secret Rotation** | 🔜 | Automatische Key-Rotation | +| **Dynamic Secrets** | 🔜 | Kurzlebige Database Credentials | +| **Seal/Unseal** | ✅ | Verschlüsselung im Ruhezustand | + +> **Wichtig**: In Produktion müssen alle Placeholder-Secrets durch echte Werte ersetzt werden! +> Secrets niemals in Git committen! + +--- + +## 14. DevSecOps Tools (Security Scanning) + +BreakPilot verwendet einen umfassenden DevSecOps-Stack fuer kontinuierliche Security-Pruefungen. + +### Secrets Detection + +| Tool | Version | Lizenz | Verwendung | +|------|---------|--------|------------| +| **Gitleaks** | 8.18.x | [MIT](https://github.com/gitleaks/gitleaks/blob/master/LICENSE) | Pre-commit, CI/CD Secrets Detection | +| **detect-secrets** | 1.4.x | [Apache-2.0](https://github.com/Yelp/detect-secrets/blob/master/LICENSE) | Baseline Secrets Detection | + +### Static Application Security Testing (SAST) + +| Tool | Version | Lizenz | Verwendung | +|------|---------|--------|------------| +| **Semgrep** | 1.52.x | [LGPL-2.1](https://github.com/returntocorp/semgrep/blob/develop/LICENSE) | Multi-Language SAST | +| **Bandit** | 1.7.x | [Apache-2.0](https://github.com/PyCQA/bandit/blob/main/LICENSE) | Python Security Linting | + +### Software Composition Analysis (SCA) + +| Tool | Version | Lizenz | Verwendung | +|------|---------|--------|------------| +| **Trivy** | 0.48.x | [Apache-2.0](https://github.com/aquasecurity/trivy/blob/main/LICENSE) | Vulnerability & Misconfiguration Scanning | +| **Grype** | 0.74.x | [Apache-2.0](https://github.com/anchore/grype/blob/main/LICENSE) | Vulnerability Scanner | +| **OWASP Dependency-Check** | 9.x | [Apache-2.0](https://github.com/jeremylong/DependencyCheck/blob/main/LICENSE.txt) | CVE/NVD Dependency Check | + +### SBOM Generation + +| Tool | Version | Lizenz | Verwendung | +|------|---------|--------|------------| +| **Syft** | 0.100.x | [Apache-2.0](https://github.com/anchore/syft/blob/main/LICENSE) | SBOM Generation (CycloneDX, SPDX) | + +### Dynamic Application Security Testing (DAST) + +| Tool | Version | Lizenz | Verwendung | +|------|---------|--------|------------| +| **OWASP ZAP** | 2.14.x | [Apache-2.0](https://github.com/zaproxy/zaproxy/blob/main/LICENSE) | Web Application Scanner | + +### Lizenz-Compliance + +Alle DevSecOps-Tools sind unter permissiven Open-Source-Lizenzen veroeffentlicht: + +| Lizenz | Tools | Verpflichtungen | +|--------|-------|-----------------| +| **MIT** | Gitleaks | Copyright-Hinweis beibehalten | +| **Apache-2.0** | Trivy, Grype, Syft, Bandit, ZAP, Dependency-Check, detect-secrets | Lizenz/Copyright beibehalten, Aenderungen dokumentieren | +| **LGPL-2.1** | Semgrep | Bei Aenderungen am Semgrep-Code: Source bereitstellen | + +### Konfigurationsdateien + +| Datei | Beschreibung | +|-------|--------------| +| `.gitleaks.toml` | Gitleaks Regeln & Allowlists | +| `.semgrep.yml` | Custom SAST Rules | +| `.trivy.yaml` | Trivy Scan-Konfiguration | +| `.trivyignore` | Akzeptierte Vulnerabilities | +| `.pre-commit-config.yaml` | Pre-Commit Hook Definitionen | +| `scripts/security-scan.sh` | Manuelles Security-Scan Script | + +### Dokumentation + +Siehe: [docs/architecture/devsecops.md](docs/architecture/devsecops.md) + +--- + +## 15. Klausur-Service RAG System (Educational Document Retrieval) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | BreakPilot Klausur-Service RAG | +| **Version** | 2.1.0 | +| **Status** | Produktionsbereit | +| **Sprache** | Python 3.11+ | +| **Lizenz** | Proprietär (BreakPilot) | +| **Verwendungszweck** | RAG für Abitur-Erwartungshorizonte (Niedersachsen) | + +### 15.1 ML Models (Model Cards) + +> **Wichtig:** Alle Standard-ML-Modelle sind für kommerzielle Nutzung freigegeben. + +#### Embedding Model + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | BAAI/bge-m3 | +| **Repository** | https://huggingface.co/BAAI/bge-m3 | +| **Lizenz** | MIT | +| **Dimensionen** | 1024 | +| **Max Token** | 8192 | +| **Sprachen** | Multilingual (inkl. Deutsch) | +| **Kommerzielle Nutzung** | ✅ Ja | +| **Status** | Default (aktiv) | + +#### Re-Ranking Model + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | BAAI/bge-reranker-v2-m3 | +| **Repository** | https://huggingface.co/BAAI/bge-reranker-v2-m3 | +| **Lizenz** | Apache-2.0 | +| **Kommerzielle Nutzung** | ✅ Ja | +| **Status** | Default (aktiv) | + +#### Alternatve Embedding Models (Optional) + +| Modell | Lizenz | Dimensionen | Empfehlung | +|--------|--------|-------------|------------| +| all-MiniLM-L6-v2 | Apache-2.0 | 384 | ✅ Fallback/Development | +| deepset/mxbai-embed-de-large-v1 | Apache-2.0 | 1024 | ✅ German-only deployments | +| jinaai/jina-embeddings-v2-base-de | Apache-2.0 | 768 | ✅ German/English | +| intfloat/multilingual-e5-large | MIT | 1024 | ✅ Multilingual | + +#### Nicht empfohlene Modelle (Lizenzprobleme) + +| Modell | Lizenz | Problem | +|--------|--------|---------| +| cross-encoder/ms-marco-* | Apache-2.0 | ⚠️ MS MARCO Trainingsdaten sind nur für nicht-kommerzielle Nutzung freigegeben | +| PyMuPDF/fitz | AGPL-3.0 | ⚠️ AGPL erfordert Open-Source-Veröffentlichung oder kommerzielle Lizenz | + +### 15.2 Python Dependencies (Klausur-Service) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|---------|--------|----------------------|--------------| +| sentence-transformers | >=2.2.0 | Apache-2.0 | Ja | Embedding & Re-Ranking | +| torch | >=2.0.0 | BSD-3-Clause | Ja | ML Framework (CPU-only) | +| pypdf | >=4.0.0 | BSD-3-Clause | Ja | PDF-Extraktion (Default) | +| unstructured | >=0.12.0 | Apache-2.0 | Ja | PDF-Extraktion (Tabellen) | +| qdrant-client | >=1.7.0 | Apache-2.0 | Ja | Vector Database Client | +| httpx | >=0.26.0 | BSD-3-Clause | Ja | Async HTTP Client | +| FastAPI | >=0.109.0 | MIT | Ja | Web Framework | +| cryptography | >=41.0.0 | Apache-2.0/BSD | Ja | AES-256-GCM Verschlüsselung | + +> **Hinweis:** PyMuPDF ist NICHT im Default-Build enthalten (AGPL-3.0). Falls benötigt, separat installieren und AGPL-Compliance sicherstellen. + +### 15.3 RAG Feature Status + +| Feature | Implementiert | Standardmäßig Aktiv | Sendet Daten extern | +|---------|--------------|---------------------|---------------------| +| **Local Embeddings** | ✅ | ✅ | ❌ Nein | +| **Local Re-Ranking** | ✅ | ✅ | ❌ Nein | +| **Semantic Chunking** | ✅ | ✅ | ❌ Nein | +| **Hybrid Search (BM25)** | ✅ | ✅ | ❌ Nein | +| **HyDE** | ✅ | ❌ | ⚠️ Ja (LLM APIs) | +| **Self-RAG** | ✅ | ❌ | ⚠️ Ja (OpenAI API) | +| **RAG Evaluation** | ✅ | ⚠️ Teilweise | ⚠️ Optional (LLM) | + +### 15.4 Datenschutz-Hinweise (Privacy Notes) + +| Kategorie | Status | Beschreibung | +|-----------|--------|--------------| +| **Embeddings** | ✅ Lokal | Keine Daten werden an externe Server gesendet | +| **Re-Ranking** | ✅ Lokal | Cross-Encoder läuft komplett lokal | +| **PDF-Extraktion** | ✅ Lokal | pypdf/unstructured laufen lokal | +| **Hybrid Search** | ✅ Lokal | BM25 läuft komplett lokal | +| **HyDE** | ⚠️ Opt-in | Bei Aktivierung: Queries an LLM APIs (OpenAI/Anthropic) | +| **Self-RAG** | ⚠️ Opt-in | Bei Aktivierung: Dokumente + Queries an OpenAI | +| **Indexierte Daten** | ✅ | Nur Erwartungshorizonte, keine Schülerdaten | + +### 15.5 Konfiguration (Environment Variables) + +| Variable | Default | Beschreibung | +|----------|---------|--------------| +| `EMBEDDING_BACKEND` | `local` | Embedding-Backend (local/openai) | +| `LOCAL_EMBEDDING_MODEL` | `BAAI/bge-m3` | Lokales Embedding-Modell | +| `RERANKER_BACKEND` | `local` | Re-Ranking Backend (local/cohere) | +| `LOCAL_RERANKER_MODEL` | `BAAI/bge-reranker-v2-m3` | Lokales Re-Ranking Modell | +| `PDF_EXTRACTION_BACKEND` | `auto` | PDF-Backend (auto/unstructured/pypdf) | +| `HYDE_ENABLED` | `false` | HyDE aktivieren (⚠️ sendet Daten extern) | +| `SELF_RAG_ENABLED` | `false` | Self-RAG aktivieren (⚠️ sendet Daten extern) | +| `HYBRID_SEARCH_ENABLED` | `true` | Hybrid Search aktivieren | +| `CHUNKING_STRATEGY` | `semantic` | Chunking-Strategie (semantic/recursive) | + +### 15.6 API Endpoints + +| Endpoint | Methode | Beschreibung | +|----------|---------|--------------| +| `/api/v1/admin/rag/system-info` | GET | System-Info mit Lizenzen und Feature-Status | +| `/api/v1/admin/nibis/search` | POST | RAG-Suche mit optionalem Re-Ranking | +| `/api/v1/admin/rag/collections` | GET | Liste aller RAG Collections | +| `/api/v1/admin/nibis/ingest` | POST | Dokument-Indexierung starten | +| `/api/v1/admin/rag/metrics` | GET | RAG-Qualitätsmetriken | + +### 15.7 Compliance-Checkliste (Klausur-Service RAG) + +- [x] Alle Standard-ML-Modelle sind MIT/Apache-2.0 lizenziert +- [x] MS MARCO-basierte Modelle wurden durch bge-reranker-v2-m3 ersetzt +- [x] PyMuPDF (AGPL) ist nicht im Default-Build enthalten +- [x] HyDE/Self-RAG sind standardmäßig deaktiviert (Datenschutz) +- [x] Alle Embedding-/Re-Ranking-Operationen laufen lokal +- [x] System-Info Endpoint dokumentiert Lizenzen und Feature-Status +- [x] Keine Schülerdaten werden indexiert + +--- + +## Kontakt + +Bei Fragen zur Lizenz-Compliance: +- E-Mail: legal@breakpilot.app +- Repository: https://github.com/breakpilot/breakpilot-pwa + +--- + +## 16. OCR Grid Detection System (klausur-service) + +| Eigenschaft | Wert | +|-------------|------| +| **Name** | BreakPilot OCR Grid Detection | +| **Version** | 4.0 | +| **Status** | Produktiv | +| **Sprache** | Python 3.11+ / TypeScript | +| **Lizenz** | Proprietär (BreakPilot) | +| **Verwendungszweck** | OCR-Analyse von Vokabeltabellen mit mm-Koordinaten | + +### Python Dependencies (Grid Detection) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|---------|--------|----------------------|--------------| +| NumPy | ≥2.0.0 | BSD-3-Clause | Ja | Deskew-Berechnung (polyfit/lineare Regression) | +| OpenCV | ≥4.8.0 | Apache-2.0 | Ja | Bildverarbeitung (optional) | + +### Frontend Dependencies (Worksheet Editor Integration) + +| Paket | Version | Lizenz | Kommerzielle Nutzung | Beschreibung | +|-------|---------|--------|----------------------|--------------| +| Fabric.js | 6.x | MIT | Ja | Canvas-Rendering für Wortpositionierung | + +### Features + +| Feature | Status | Beschreibung | +|---------|--------|--------------| +| **mm-Koordinatensystem** | ✅ | A4-Format (210x297mm) | +| **Deskew-Korrektur** | ✅ | Automatische Ausrichtung schiefer Scans | +| **1mm Column Margin** | ✅ | Spalten beginnen 1mm vor erstem Wort | +| **Spalten-Erkennung** | ✅ | Englisch/Deutsch/Beispiel automatisch erkannt | +| **Editor-Integration** | ✅ | Export/Import via localStorage | + +### Lizenz-Compliance + +- [x] NumPy ist BSD-3-Clause lizenziert (kommerziell nutzbar) +- [x] OpenCV ist Apache-2.0 lizenziert (kommerziell nutzbar) +- [x] Fabric.js ist MIT lizenziert (kommerziell nutzbar) +- [x] Alle Verarbeitung erfolgt lokal (keine Daten an externe Server) + +| 2026-02-08 | OCR Grid Detection System (v4) hinzugefügt | Claude Code | +| 2026-02-08 | Fabric.js für Worksheet Editor Integration dokumentiert | Claude Code | +| 2026-02-08 | Deskew-Korrektur und mm-Koordinatensystem dokumentiert | Claude Code | diff --git a/docs-src/services/klausur-service/OCR-Compare.md b/docs-src/services/klausur-service/OCR-Compare.md new file mode 100644 index 0000000..449f8d0 --- /dev/null +++ b/docs-src/services/klausur-service/OCR-Compare.md @@ -0,0 +1,366 @@ +# OCR Compare Tool - Dokumentation + +**Status:** Produktiv +**Version:** 4.0 +**Letzte Aktualisierung:** 2026-02-08 +**URL:** https://macmini:3002/ai/ocr-compare + +--- + +## Übersicht + +Das OCR Compare Tool ermöglicht die automatische Analyse von gescannten Vokabeltabellen mit: +- Grid-basierter OCR-Erkennung +- Automatischer Spalten-Erkennung (Englisch/Deutsch/Beispiel) +- mm-Koordinatensystem für präzise Positionierung +- Deskew-Korrektur für schiefe Scans +- Export zum Worksheet-Editor + +--- + +## Architektur + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Frontend (admin-v2) │ +│ /admin-v2/app/(admin)/ai/ocr-compare/page.tsx │ +│ - Bild-Upload │ +│ - Grid-Overlay Visualisierung │ +│ - Cell-Edit Popup │ +│ - Export zum Worksheet-Editor │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ klausur-service (FastAPI) │ +│ Port 8086 - /klausur-service/backend/ │ +│ - /api/v1/ocr/analyze-grid (Grid-Analyse) │ +│ - services/grid_detection_service.py (v4) │ +└─────────────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────┐ +│ PaddleOCR Service │ +│ Port 8088 - OCR-Erkennung │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Features (Version 4) + +### 1. mm-Koordinatensystem + +Alle Koordinaten werden im A4-Format (210x297mm) ausgegeben: + +| Feld | Beschreibung | +|------|--------------| +| `x_mm` | X-Position in mm (0-210) | +| `y_mm` | Y-Position in mm (0-297) | +| `width_mm` | Breite in mm | +| `height_mm` | Höhe in mm | + +**Konvertierung:** +```typescript +// Prozent zu mm +const x_mm = (x_percent / 100) * 210 +const y_mm = (y_percent / 100) * 297 + +// mm zu Pixel (für Canvas bei 96 DPI) +const MM_TO_PX = 3.7795275591 +const x_px = x_mm * MM_TO_PX +``` + +### 2. Deskew-Korrektur + +Automatische Ausrichtung schiefer Scans basierend auf der ersten Spalte: + +1. **Erkennung:** Alle Wörter in der ersten Spalte (x < 33%) werden analysiert +2. **Berechnung:** Lineare Regression auf den linken Kanten +3. **Korrektur:** Rotation aller Koordinaten um den berechneten Winkel +4. **Limitierung:** Maximal ±5° Korrektur + +```python +# Deskew-Winkel im Response +{ + "deskew_angle_deg": -1.2, # Negativer Wert = nach links geneigt + ... +} +``` + +### 3. Spalten-Erkennung mit 1mm Margin + +Spalten werden automatisch erkannt und beginnen 1mm vor dem ersten Wort: + +```json +{ + "detected_columns": [ + { + "column_type": "english", + "x_start": 9.52, // Prozent + "x_end": 35.0, + "x_start_mm": 20.0, // mm (1mm vor erstem Wort) + "x_end_mm": 73.5, + "word_count": 15 + }, + { + "column_type": "german", + "x_start_mm": 74.0, + "x_end_mm": 140.0, + "word_count": 15 + }, + { + "column_type": "example", + "x_start_mm": 141.0, + "x_end_mm": 200.0, + "word_count": 12 + } + ] +} +``` + +### 4. Zellen-Status + +| Status | Beschreibung | +|--------|--------------| +| `empty` | Keine OCR-Erkennung in dieser Zelle | +| `recognized` | Text erkannt mit Confidence ≥ 50% | +| `problematic` | Text erkannt mit Confidence < 50% | +| `manual` | Manuell korrigiert | + +--- + +## API-Endpoints + +### POST /api/v1/ocr/analyze-grid + +Analysiert ein Bild und erkennt die Vokabeltabellen-Struktur. + +**Request:** +```json +{ + "image_base64": "data:image/jpeg;base64,...", + "min_confidence": 0.5, + "padding": 2.0 +} +``` + +**Response:** +```json +{ + "cells": [ + [ + { + "row": 0, + "col": 0, + "x": 10.0, + "y": 15.0, + "width": 25.0, + "height": 3.0, + "x_mm": 21.0, + "y_mm": 44.55, + "width_mm": 52.5, + "height_mm": 8.91, + "text": "house", + "confidence": 0.95, + "status": "recognized", + "column_type": "english", + "logical_row": 0, + "logical_col": 0 + } + ] + ], + "detected_columns": [...], + "page_dimensions": { + "width_mm": 210.0, + "height_mm": 297.0, + "format": "A4" + }, + "deskew_angle_deg": -0.5, + "statistics": { + "total_cells": 45, + "recognized_cells": 42, + "problematic_cells": 3, + "empty_cells": 0 + } +} +``` + +--- + +## Frontend-Komponenten + +### GridOverlay.tsx + +Zeigt die erkannten Zellen als farbiges Overlay über dem Bild. + +**Props:** +```typescript +interface GridOverlayProps { + cells: GridCell[][] + imageWidth: number + imageHeight: number + showLabels?: boolean + onCellClick?: (cell: GridCell) => void +} +``` + +**Farbkodierung:** +- Grün: `recognized` (gut erkannt) +- Gelb: `problematic` (niedrige Confidence) +- Grau: `empty` +- Blau: `manual` (manuell korrigiert) + +### CellEditPopup.tsx + +Popup zum Bearbeiten einer Zelle. + +**Features:** +- Text bearbeiten +- Spaltentyp ändern (English/German/Example) +- Confidence anzeigen +- mm-Koordinaten anzeigen +- Keyboard-Shortcuts: Ctrl+Enter (Speichern), Esc (Abbrechen) + +--- + +## Worksheet-Editor Integration + +### Export + +Der "Zum Editor exportieren" Button speichert die OCR-Daten in localStorage: + +```typescript +interface OCRExportData { + version: '1.0' + source: 'ocr-compare' + exported_at: string + session_id: string + page_number: number + page_dimensions: { + width_mm: number + height_mm: number + format: string + } + words: OCRWord[] + detected_columns: DetectedColumn[] +} +``` + +**localStorage Keys:** +- `ocr_export_{session_id}_{page_number}`: Export-Daten +- `ocr_export_latest`: Referenz zum neuesten Export + +### Import im Worksheet-Editor + +1. Öffnen Sie den Worksheet-Editor: https://macmini/worksheet-editor +2. Klicken Sie auf den OCR-Import Button (grünes Icon) +3. Die Wörter werden auf dem Canvas platziert + +**Konvertierung mm → Pixel:** +```typescript +const MM_TO_PX = 3.7795275591 +const x_px = word.x_mm * MM_TO_PX +const y_px = word.y_mm * MM_TO_PX +``` + +--- + +## Dateien + +### Backend (klausur-service) + +| Datei | Beschreibung | +|-------|--------------| +| `services/grid_detection_service.py` | Grid-Erkennung v4 mit Deskew | +| `tests/test_grid_detection.py` | Unit Tests | + +### Frontend (admin-v2) + +| Datei | Beschreibung | +|-------|--------------| +| `app/(admin)/ai/ocr-compare/page.tsx` | Haupt-UI | +| `components/ocr/GridOverlay.tsx` | Grid-Visualisierung | +| `components/ocr/CellEditPopup.tsx` | Zellen-Editor | + +### Frontend (studio-v2) + +| Datei | Beschreibung | +|-------|--------------| +| `lib/worksheet-editor/ocr-integration.ts` | OCR Import/Export Utility | +| `app/worksheet-editor/page.tsx` | Editor mit OCR-Import | +| `components/worksheet-editor/EditorToolbar.tsx` | Toolbar mit OCR-Button | + +--- + +## Deployment + +```bash +# 1. Backend synchronisieren +scp grid_detection_service.py macmini:.../klausur-service/backend/services/ + +# 2. Tests synchronisieren +scp test_grid_detection.py macmini:.../klausur-service/backend/tests/ + +# 3. klausur-service neu bauen +ssh macmini "docker compose build --no-cache klausur-service" + +# 4. Container starten +ssh macmini "docker compose up -d klausur-service" + +# 5. Frontend (admin-v2) deployen +ssh macmini "docker compose build --no-cache admin-v2 && docker compose up -d admin-v2" +``` + +--- + +## Verwendete Open-Source-Bibliotheken + +| Bibliothek | Version | Lizenz | Verwendung | +|------------|---------|--------|------------| +| NumPy | ≥1.24 | BSD-3-Clause | Deskew-Berechnung (polyfit) | +| OpenCV | ≥4.8 | Apache-2.0 | Bildverarbeitung (optional) | +| PaddleOCR | 2.7 | Apache-2.0 | OCR-Erkennung | +| Fabric.js | 6.x | MIT | Canvas-Rendering (Frontend) | + +--- + +## Fehlerbehandlung + +### Häufige Probleme + +| Problem | Lösung | +|---------|--------| +| "Grid analysieren" lädt nicht | klausur-service Container prüfen | +| Keine Zellen erkannt | Min. Confidence reduzieren | +| Falsche Spalten-Zuordnung | Manuell im CellEditPopup korrigieren | +| Export funktioniert nicht | Browser-Console auf Fehler prüfen | + +### Logging + +```bash +# klausur-service Logs +docker logs breakpilot-pwa-klausur-service --tail=100 + +# Grid Detection spezifisch +docker logs breakpilot-pwa-klausur-service 2>&1 | grep "grid_detection" +``` + +--- + +## Änderungshistorie + +| Version | Datum | Änderungen | +|---------|-------|------------| +| 4.0 | 2026-02-08 | Deskew-Korrektur, 1mm Column Margin | +| 3.0 | 2026-02-07 | mm-Koordinatensystem | +| 2.0 | 2026-02-06 | Spalten-Erkennung | +| 1.0 | 2026-02-05 | Initiale Implementierung | + +--- + +## Referenzen + +- [Worksheet-Editor Architektur](Worksheet-Editor-Architecture.md) +- [OCR Labeling Spec](OCR-Labeling-Spec.md) +- [SBOM](/infrastructure/sbom) diff --git a/klausur-service/backend/tests/test_grid_detection.py b/klausur-service/backend/tests/test_grid_detection.py new file mode 100644 index 0000000..72e456a --- /dev/null +++ b/klausur-service/backend/tests/test_grid_detection.py @@ -0,0 +1,385 @@ +""" +Tests for Grid Detection Service v4 + +Tests cover: +- mm coordinate conversion +- Deskew calculation +- Column detection with 1mm margin +- Data class functionality + +Lizenz: Apache 2.0 (kommerziell nutzbar) +""" + +import pytest +import math +from typing import List + +# Import the service under test +import sys +sys.path.insert(0, '/app') + +from services.grid_detection_service import ( + GridDetectionService, + OCRRegion, + GridCell, + CellStatus, + ColumnType, + A4_WIDTH_MM, + A4_HEIGHT_MM, + COLUMN_MARGIN_MM, + COLUMN_MARGIN_PCT +) + + +class TestOCRRegionMMConversion: + """Test mm coordinate conversion for OCR regions.""" + + def test_x_mm_conversion(self): + """Test X coordinate conversion from percent to mm.""" + # 50% of A4 width = 105mm + region = OCRRegion(text="test", confidence=0.9, x=50.0, y=0.0, width=10.0, height=5.0) + assert region.x_mm == 105.0 + + def test_y_mm_conversion(self): + """Test Y coordinate conversion from percent to mm.""" + # 33.33% of A4 height = 99mm (approx) + region = OCRRegion(text="test", confidence=0.9, x=0.0, y=33.33, width=10.0, height=5.0) + assert abs(region.y_mm - 99.0) < 0.5 + + def test_width_mm_conversion(self): + """Test width conversion from percent to mm.""" + # 10% of A4 width = 21mm + region = OCRRegion(text="test", confidence=0.9, x=0.0, y=0.0, width=10.0, height=5.0) + assert region.width_mm == 21.0 + + def test_height_mm_conversion(self): + """Test height conversion from percent to mm.""" + # 5% of A4 height = 14.85mm + region = OCRRegion(text="test", confidence=0.9, x=0.0, y=0.0, width=10.0, height=5.0) + assert abs(region.height_mm - 14.85) < 0.01 + + def test_center_coordinates(self): + """Test center coordinate calculation.""" + region = OCRRegion(text="test", confidence=0.9, x=10.0, y=20.0, width=20.0, height=10.0) + assert region.center_x == 20.0 + assert region.center_y == 25.0 + + def test_right_bottom_edges(self): + """Test right and bottom edge calculation.""" + region = OCRRegion(text="test", confidence=0.9, x=10.0, y=20.0, width=30.0, height=15.0) + assert region.right == 40.0 + assert region.bottom == 35.0 + + +class TestGridCellMMConversion: + """Test mm coordinate conversion for grid cells.""" + + def test_cell_to_dict_includes_mm(self): + """Test that to_dict includes mm coordinates.""" + cell = GridCell(row=0, col=0, x=10.0, y=20.0, width=30.0, height=5.0, text="hello") + result = cell.to_dict() + + assert "x_mm" in result + assert "y_mm" in result + assert "width_mm" in result + assert "height_mm" in result + + # 10% of 210mm = 21mm + assert result["x_mm"] == 21.0 + # 20% of 297mm = 59.4mm + assert result["y_mm"] == 59.4 + + def test_cell_mm_coordinates(self): + """Test direct mm property access.""" + cell = GridCell(row=0, col=0, x=50.0, y=50.0, width=20.0, height=3.0) + + assert cell.x_mm == 105.0 # 50% of 210mm + assert cell.y_mm == 148.5 # 50% of 297mm + assert cell.width_mm == 42.0 # 20% of 210mm + assert abs(cell.height_mm - 8.91) < 0.01 # 3% of 297mm + + def test_cell_to_dict_includes_all_fields(self): + """Test that to_dict includes all expected fields.""" + cell = GridCell( + row=1, col=2, x=10.0, y=20.0, width=30.0, height=5.0, + text="test", confidence=0.95, status=CellStatus.RECOGNIZED, + column_type=ColumnType.ENGLISH, logical_row=0, logical_col=0, + is_continuation=False + ) + result = cell.to_dict() + + assert result["row"] == 1 + assert result["col"] == 2 + assert result["text"] == "test" + assert result["confidence"] == 0.95 + assert result["status"] == "recognized" + assert result["column_type"] == "english" + assert result["logical_row"] == 0 + assert result["logical_col"] == 0 + assert result["is_continuation"] == False + + +class TestA4Constants: + """Test A4 dimension constants.""" + + def test_a4_width_mm(self): + """Verify A4 width is 210mm.""" + assert A4_WIDTH_MM == 210.0 + + def test_a4_height_mm(self): + """Verify A4 height is 297mm.""" + assert A4_HEIGHT_MM == 297.0 + + def test_column_margin_mm(self): + """Verify column margin is 1mm.""" + assert COLUMN_MARGIN_MM == 1.0 + + def test_column_margin_percent(self): + """Verify column margin percentage calculation.""" + expected = (1.0 / 210.0) * 100 + assert abs(COLUMN_MARGIN_PCT - expected) < 0.001 + + +class TestGridDetectionServiceInit: + """Test GridDetectionService initialization.""" + + def test_init_with_defaults(self): + """Test service initializes with default parameters.""" + service = GridDetectionService() + assert service.y_tolerance_pct == 1.5 + assert service.padding_pct == 0.3 + assert service.column_margin_mm == COLUMN_MARGIN_MM + + def test_init_with_custom_params(self): + """Test service initializes with custom parameters.""" + service = GridDetectionService( + y_tolerance_pct=2.0, + padding_pct=0.5, + column_margin_mm=2.0 + ) + assert service.y_tolerance_pct == 2.0 + assert service.padding_pct == 0.5 + assert service.column_margin_mm == 2.0 + + +class TestDeskewCalculation: + """Test deskew angle calculation.""" + + def test_calculate_deskew_no_regions(self): + """Test deskew returns 0 for empty regions.""" + service = GridDetectionService() + angle = service.calculate_deskew_angle([]) + assert angle == 0.0 + + def test_calculate_deskew_few_regions(self): + """Test deskew returns 0 for too few regions.""" + service = GridDetectionService() + regions = [ + OCRRegion(text="a", confidence=0.9, x=10.0, y=10.0, width=5.0, height=2.0), + ] + angle = service.calculate_deskew_angle(regions) + assert angle == 0.0 + + def test_calculate_deskew_perfectly_aligned(self): + """Test deskew returns near-zero for perfectly aligned text.""" + service = GridDetectionService() + # Perfectly vertical alignment at x=10% + regions = [ + OCRRegion(text="a", confidence=0.9, x=10.0, y=10.0, width=5.0, height=2.0), + OCRRegion(text="b", confidence=0.9, x=10.0, y=20.0, width=5.0, height=2.0), + OCRRegion(text="c", confidence=0.9, x=10.0, y=30.0, width=5.0, height=2.0), + OCRRegion(text="d", confidence=0.9, x=10.0, y=40.0, width=5.0, height=2.0), + OCRRegion(text="e", confidence=0.9, x=10.0, y=50.0, width=5.0, height=2.0), + ] + angle = service.calculate_deskew_angle(regions) + assert abs(angle) < 0.5 # Should be very close to 0 + + def test_calculate_deskew_tilted_right(self): + """Test deskew detects right tilt.""" + service = GridDetectionService() + # Text tilts right as we go down (x increases with y) + regions = [ + OCRRegion(text="a", confidence=0.9, x=10.0, y=10.0, width=5.0, height=2.0), + OCRRegion(text="b", confidence=0.9, x=11.0, y=20.0, width=5.0, height=2.0), + OCRRegion(text="c", confidence=0.9, x=12.0, y=30.0, width=5.0, height=2.0), + OCRRegion(text="d", confidence=0.9, x=13.0, y=40.0, width=5.0, height=2.0), + OCRRegion(text="e", confidence=0.9, x=14.0, y=50.0, width=5.0, height=2.0), + ] + angle = service.calculate_deskew_angle(regions) + assert angle > 0 # Positive angle for right tilt + + def test_calculate_deskew_max_angle(self): + """Test deskew is clamped to max 5 degrees.""" + service = GridDetectionService() + # Extreme tilt + regions = [ + OCRRegion(text="a", confidence=0.9, x=5.0, y=10.0, width=5.0, height=2.0), + OCRRegion(text="b", confidence=0.9, x=15.0, y=20.0, width=5.0, height=2.0), + OCRRegion(text="c", confidence=0.9, x=25.0, y=30.0, width=5.0, height=2.0), + OCRRegion(text="d", confidence=0.9, x=35.0, y=40.0, width=5.0, height=2.0), + OCRRegion(text="e", confidence=0.9, x=45.0, y=50.0, width=5.0, height=2.0), + ] + angle = service.calculate_deskew_angle(regions) + assert abs(angle) <= 5.0 # Clamped to ±5° + + +class TestDeskewApplication: + """Test deskew coordinate transformation.""" + + def test_apply_deskew_zero_angle(self): + """Test no transformation for zero angle.""" + service = GridDetectionService() + regions = [ + OCRRegion(text="a", confidence=0.9, x=10.0, y=20.0, width=5.0, height=2.0), + ] + result = service.apply_deskew_to_regions(regions, 0.0) + + assert len(result) == 1 + assert result[0].x == 10.0 + assert result[0].y == 20.0 + + def test_apply_deskew_preserves_text(self): + """Test deskew preserves text and confidence.""" + service = GridDetectionService() + regions = [ + OCRRegion(text="hello", confidence=0.95, x=10.0, y=20.0, width=5.0, height=2.0), + ] + result = service.apply_deskew_to_regions(regions, 2.0) + + assert result[0].text == "hello" + assert result[0].confidence == 0.95 + + +class TestCellStatus: + """Test cell status classification.""" + + def test_cell_status_empty(self): + """Test empty cell status.""" + cell = GridCell(row=0, col=0, x=0, y=0, width=10, height=5, text="") + assert cell.status == CellStatus.EMPTY + + def test_cell_status_recognized(self): + """Test recognized cell status.""" + cell = GridCell( + row=0, col=0, x=0, y=0, width=10, height=5, + text="hello", confidence=0.9, status=CellStatus.RECOGNIZED + ) + assert cell.status == CellStatus.RECOGNIZED + + def test_cell_status_problematic(self): + """Test problematic cell (low confidence).""" + cell = GridCell( + row=0, col=0, x=0, y=0, width=10, height=5, + text="hello", confidence=0.3, status=CellStatus.PROBLEMATIC + ) + assert cell.status == CellStatus.PROBLEMATIC + + +class TestColumnType: + """Test column type enum.""" + + def test_column_type_values(self): + """Test column type enum values.""" + assert ColumnType.ENGLISH.value == "english" + assert ColumnType.GERMAN.value == "german" + assert ColumnType.EXAMPLE.value == "example" + assert ColumnType.UNKNOWN.value == "unknown" + + +class TestDetectGrid: + """Test grid detection functionality.""" + + def test_detect_grid_empty_regions(self): + """Test grid detection with empty regions.""" + service = GridDetectionService() + result = service.detect_grid([]) + + assert result.rows == 0 + assert result.columns == 0 + assert len(result.cells) == 0 + + def test_detect_grid_single_word(self): + """Test grid detection with single word.""" + service = GridDetectionService() + regions = [ + OCRRegion(text="house", confidence=0.9, x=10.0, y=10.0, width=10.0, height=2.0), + ] + result = service.detect_grid(regions) + + assert result.rows >= 1 + assert result.columns >= 1 + + def test_detect_grid_result_has_page_dimensions(self): + """Test that result includes page dimensions.""" + service = GridDetectionService() + regions = [ + OCRRegion(text="house", confidence=0.9, x=10.0, y=10.0, width=10.0, height=2.0), + ] + result = service.detect_grid(regions) + result_dict = result.to_dict() + + assert "page_dimensions" in result_dict + assert result_dict["page_dimensions"]["width_mm"] == 210.0 + assert result_dict["page_dimensions"]["height_mm"] == 297.0 + assert result_dict["page_dimensions"]["format"] == "A4" + + def test_detect_grid_result_has_stats(self): + """Test that result includes stats.""" + service = GridDetectionService() + regions = [ + OCRRegion(text="house", confidence=0.9, x=10.0, y=10.0, width=10.0, height=2.0), + OCRRegion(text="Haus", confidence=0.8, x=50.0, y=10.0, width=8.0, height=2.0), + ] + result = service.detect_grid(regions) + result_dict = result.to_dict() + + assert "stats" in result_dict + assert "recognized" in result_dict["stats"] + assert "coverage" in result_dict["stats"] + + +class TestIntegration: + """Integration tests for full analysis pipeline.""" + + def test_full_vocabulary_table_analysis(self): + """Test analysis of a typical vocabulary table.""" + service = GridDetectionService() + + # Simulate a vocabulary table with 3 columns + regions = [ + # Row 1 + OCRRegion(text="house", confidence=0.95, x=10.0, y=15.0, width=12.0, height=2.5), + OCRRegion(text="Haus", confidence=0.92, x=45.0, y=15.0, width=8.0, height=2.5), + OCRRegion(text="This is a house.", confidence=0.88, x=70.0, y=15.0, width=25.0, height=2.5), + # Row 2 + OCRRegion(text="car", confidence=0.94, x=10.0, y=22.0, width=8.0, height=2.5), + OCRRegion(text="Auto", confidence=0.91, x=45.0, y=22.0, width=9.0, height=2.5), + OCRRegion(text="I drive a car.", confidence=0.85, x=70.0, y=22.0, width=22.0, height=2.5), + # Row 3 + OCRRegion(text="tree", confidence=0.96, x=10.0, y=29.0, width=9.0, height=2.5), + OCRRegion(text="Baum", confidence=0.93, x=45.0, y=29.0, width=10.0, height=2.5), + OCRRegion(text="The tree is tall.", confidence=0.87, x=70.0, y=29.0, width=24.0, height=2.5), + ] + + result = service.detect_grid(regions) + result_dict = result.to_dict() + + # Verify structure + assert "cells" in result_dict + assert "page_dimensions" in result_dict + assert "stats" in result_dict + + # Verify page dimensions + assert result_dict["page_dimensions"]["format"] == "A4" + + # Verify cells have mm coordinates + if len(result_dict["cells"]) > 0 and len(result_dict["cells"][0]) > 0: + cell = result_dict["cells"][0][0] + assert "x_mm" in cell + assert "y_mm" in cell + assert "width_mm" in cell + assert "height_mm" in cell + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/studio-v2/lib/worksheet-editor/ocr-integration.test.ts b/studio-v2/lib/worksheet-editor/ocr-integration.test.ts new file mode 100644 index 0000000..44d6035 --- /dev/null +++ b/studio-v2/lib/worksheet-editor/ocr-integration.test.ts @@ -0,0 +1,466 @@ +/** + * Tests for OCR Integration Utility + * + * Tests cover: + * - mm to pixel conversion + * - OCR data export format + * - LocalStorage operations + * - Canvas integration + */ + +import { + MM_TO_PX, + A4_WIDTH_MM, + A4_HEIGHT_MM, + A4_WIDTH_PX, + A4_HEIGHT_PX, + mmToPixel, + pixelToMm, + getColumnColor, + createTextProps, + exportOCRData, + saveOCRExportToStorage, + loadLatestOCRExport, + loadOCRExport, + clearOCRExports, + type OCRWord, + type OCRExportData, + type ColumnType, +} from './ocr-integration' + +// Mock localStorage +const localStorageMock = (() => { + let store: Record = {} + return { + getItem: jest.fn((key: string) => store[key] || null), + setItem: jest.fn((key: string, value: string) => { + store[key] = value + }), + removeItem: jest.fn((key: string) => { + delete store[key] + }), + clear: jest.fn(() => { + store = {} + }), + keys: () => Object.keys(store), + } +})() + +Object.defineProperty(window, 'localStorage', { value: localStorageMock }) + +describe('Constants', () => { + test('MM_TO_PX is correct for 96 DPI', () => { + // 1 inch = 25.4mm, 96 DPI = 96 pixels per inch + // 96 / 25.4 = 3.7795275591 + expect(MM_TO_PX).toBeCloseTo(3.7795275591, 8) + }) + + test('A4 dimensions in mm are correct', () => { + expect(A4_WIDTH_MM).toBe(210) + expect(A4_HEIGHT_MM).toBe(297) + }) + + test('A4 dimensions in pixels are calculated correctly', () => { + expect(A4_WIDTH_PX).toBe(Math.round(210 * MM_TO_PX)) // ~794 + expect(A4_HEIGHT_PX).toBe(Math.round(297 * MM_TO_PX)) // ~1123 + }) +}) + +describe('mmToPixel', () => { + test('converts 0mm to 0px', () => { + expect(mmToPixel(0)).toBe(0) + }) + + test('converts 1mm correctly', () => { + expect(mmToPixel(1)).toBeCloseTo(3.7795275591, 8) + }) + + test('converts 100mm correctly', () => { + expect(mmToPixel(100)).toBeCloseTo(377.95275591, 6) + }) + + test('converts A4 width correctly', () => { + expect(mmToPixel(210)).toBeCloseTo(793.7, 1) + }) +}) + +describe('pixelToMm', () => { + test('converts 0px to 0mm', () => { + expect(pixelToMm(0)).toBe(0) + }) + + test('converts 100px correctly', () => { + expect(pixelToMm(100)).toBeCloseTo(26.458, 2) + }) + + test('round-trip conversion is accurate', () => { + const original = 50 + const pixels = mmToPixel(original) + const backToMm = pixelToMm(pixels) + expect(backToMm).toBeCloseTo(original, 8) + }) +}) + +describe('getColumnColor', () => { + test('returns blue for english column', () => { + expect(getColumnColor('english')).toBe('#1e40af') + }) + + test('returns green for german column', () => { + expect(getColumnColor('german')).toBe('#166534') + }) + + test('returns purple for example column', () => { + expect(getColumnColor('example')).toBe('#6b21a8') + }) + + test('returns gray for unknown column', () => { + expect(getColumnColor('unknown')).toBe('#374151') + }) + + test('uses custom colors from options', () => { + const options = { englishColor: '#ff0000' } + expect(getColumnColor('english', options)).toBe('#ff0000') + }) +}) + +describe('createTextProps', () => { + const mockWord: OCRWord = { + text: 'house', + x_mm: 21.0, + y_mm: 44.55, + width_mm: 52.5, + height_mm: 8.91, + column_type: 'english', + logical_row: 0, + } + + test('creates correct type', () => { + const props = createTextProps(mockWord) + expect(props.type).toBe('i-text') + }) + + test('converts mm to pixels for left position', () => { + const props = createTextProps(mockWord) + expect(props.left).toBeCloseTo(21.0 * MM_TO_PX, 2) + }) + + test('converts mm to pixels for top position', () => { + const props = createTextProps(mockWord) + expect(props.top).toBeCloseTo(44.55 * MM_TO_PX, 2) + }) + + test('applies offset correctly', () => { + const props = createTextProps(mockWord, { offsetX: 5, offsetY: 10 }) + expect(props.left).toBeCloseTo((21.0 + 5) * MM_TO_PX, 2) + expect(props.top).toBeCloseTo((44.55 + 10) * MM_TO_PX, 2) + }) + + test('sets fill color based on column type', () => { + const props = createTextProps(mockWord) + expect(props.fill).toBe('#1e40af') // English blue + }) + + test('includes OCR metadata', () => { + const props = createTextProps(mockWord) + expect(props.ocrMetadata).toBeDefined() + expect((props.ocrMetadata as any).x_mm).toBe(21.0) + expect((props.ocrMetadata as any).column_type).toBe('english') + expect((props.ocrMetadata as any).logical_row).toBe(0) + }) + + test('uses custom font family', () => { + const props = createTextProps(mockWord, { fontFamily: 'Times New Roman' }) + expect(props.fontFamily).toBe('Times New Roman') + }) + + test('uses custom font size', () => { + const props = createTextProps(mockWord, { fontSize: 16 }) + expect(props.fontSize).toBe(16) + }) +}) + +describe('exportOCRData', () => { + const mockGridData = { + cells: [ + [ + { + text: 'house', + x_mm: 21.0, + y_mm: 44.55, + width_mm: 52.5, + height_mm: 8.91, + column_type: 'english' as ColumnType, + logical_row: 0, + status: 'recognized', + }, + { + text: 'Haus', + x_mm: 80.0, + y_mm: 44.55, + width_mm: 40.0, + height_mm: 8.91, + column_type: 'german' as ColumnType, + logical_row: 0, + status: 'recognized', + }, + ], + ], + detected_columns: [ + { column_type: 'english', x_start_mm: 20.0, x_end_mm: 73.5 }, + { column_type: 'german', x_start_mm: 74.0, x_end_mm: 140.0 }, + ], + page_dimensions: { + width_mm: 210, + height_mm: 297, + format: 'A4', + }, + } + + test('creates correct version', () => { + const result = exportOCRData(mockGridData, 'session-123', 1) + expect(result.version).toBe('1.0') + }) + + test('sets correct source', () => { + const result = exportOCRData(mockGridData, 'session-123', 1) + expect(result.source).toBe('ocr-compare') + }) + + test('includes session ID and page number', () => { + const result = exportOCRData(mockGridData, 'session-123', 1) + expect(result.session_id).toBe('session-123') + expect(result.page_number).toBe(1) + }) + + test('includes page dimensions', () => { + const result = exportOCRData(mockGridData, 'session-123', 1) + expect(result.page_dimensions.width_mm).toBe(210) + expect(result.page_dimensions.height_mm).toBe(297) + expect(result.page_dimensions.format).toBe('A4') + }) + + test('converts cells to words', () => { + const result = exportOCRData(mockGridData, 'session-123', 1) + expect(result.words).toHaveLength(2) + expect(result.words[0].text).toBe('house') + expect(result.words[0].column_type).toBe('english') + }) + + test('filters empty cells', () => { + const dataWithEmpty = { + ...mockGridData, + cells: [ + [ + ...mockGridData.cells[0], + { text: '', status: 'empty' }, // Empty cell + ], + ], + } + const result = exportOCRData(dataWithEmpty, 'session-123', 1) + expect(result.words).toHaveLength(2) // Empty cell excluded + }) + + test('includes detected columns', () => { + const result = exportOCRData(mockGridData, 'session-123', 1) + expect(result.detected_columns).toHaveLength(2) + expect(result.detected_columns[0].column_type).toBe('english') + }) + + test('sets exported_at timestamp', () => { + const before = new Date().toISOString() + const result = exportOCRData(mockGridData, 'session-123', 1) + const after = new Date().toISOString() + + expect(result.exported_at >= before).toBe(true) + expect(result.exported_at <= after).toBe(true) + }) +}) + +describe('localStorage operations', () => { + beforeEach(() => { + localStorageMock.clear() + }) + + const mockExportData: OCRExportData = { + version: '1.0', + source: 'ocr-compare', + exported_at: '2026-02-08T12:00:00Z', + session_id: 'session-123', + page_number: 1, + page_dimensions: { + width_mm: 210, + height_mm: 297, + format: 'A4', + }, + words: [ + { + text: 'house', + x_mm: 21.0, + y_mm: 44.55, + width_mm: 52.5, + height_mm: 8.91, + column_type: 'english', + logical_row: 0, + }, + ], + detected_columns: [], + } + + describe('saveOCRExportToStorage', () => { + test('saves data to localStorage', () => { + saveOCRExportToStorage(mockExportData) + + expect(localStorageMock.setItem).toHaveBeenCalledWith( + 'ocr_export_session-123_1', + expect.any(String) + ) + }) + + test('sets latest export key', () => { + saveOCRExportToStorage(mockExportData) + + expect(localStorageMock.setItem).toHaveBeenCalledWith( + 'ocr_export_latest', + 'ocr_export_session-123_1' + ) + }) + }) + + describe('loadLatestOCRExport', () => { + test('returns null when no export exists', () => { + const result = loadLatestOCRExport() + expect(result).toBeNull() + }) + + test('loads latest export data', () => { + // Manually set up the mock + localStorageMock.setItem( + 'ocr_export_session-123_1', + JSON.stringify(mockExportData) + ) + localStorageMock.setItem('ocr_export_latest', 'ocr_export_session-123_1') + + // Reset the mock to return correct values + localStorageMock.getItem.mockImplementation((key: string) => { + if (key === 'ocr_export_latest') return 'ocr_export_session-123_1' + if (key === 'ocr_export_session-123_1') + return JSON.stringify(mockExportData) + return null + }) + + const result = loadLatestOCRExport() + expect(result).not.toBeNull() + expect(result?.session_id).toBe('session-123') + }) + }) + + describe('loadOCRExport', () => { + test('returns null for non-existent session', () => { + const result = loadOCRExport('nonexistent', 1) + expect(result).toBeNull() + }) + + test('loads specific export by session and page', () => { + localStorageMock.getItem.mockImplementation((key: string) => { + if (key === 'ocr_export_session-123_1') + return JSON.stringify(mockExportData) + return null + }) + + const result = loadOCRExport('session-123', 1) + expect(result).not.toBeNull() + expect(result?.page_number).toBe(1) + }) + + test('handles JSON parse errors gracefully', () => { + localStorageMock.getItem.mockImplementation((key: string) => { + if (key === 'ocr_export_session-123_1') return 'invalid json' + return null + }) + + const result = loadOCRExport('session-123', 1) + expect(result).toBeNull() + }) + }) + + describe('clearOCRExports', () => { + test('removes all OCR export keys', () => { + // Set up mock to return keys + Object.defineProperty(localStorageMock, 'keys', { + value: () => [ + 'ocr_export_session-1_1', + 'ocr_export_session-2_1', + 'ocr_export_latest', + 'other_key', + ], + }) + + // Mock Object.keys(localStorage) + const originalKeys = Object.keys + Object.keys = jest.fn((obj) => { + if (obj === localStorage) { + return [ + 'ocr_export_session-1_1', + 'ocr_export_session-2_1', + 'ocr_export_latest', + 'other_key', + ] + } + return originalKeys(obj) + }) + + clearOCRExports() + + expect(localStorageMock.removeItem).toHaveBeenCalledWith( + 'ocr_export_session-1_1' + ) + expect(localStorageMock.removeItem).toHaveBeenCalledWith( + 'ocr_export_session-2_1' + ) + expect(localStorageMock.removeItem).toHaveBeenCalledWith( + 'ocr_export_latest' + ) + + // Restore Object.keys + Object.keys = originalKeys + }) + }) +}) + +describe('Edge Cases', () => { + test('handles negative mm values', () => { + const pixels = mmToPixel(-10) + expect(pixels).toBeCloseTo(-37.795, 2) + }) + + test('handles very large mm values', () => { + const pixels = mmToPixel(10000) + expect(pixels).toBeCloseTo(37795.275591, 2) + }) + + test('handles word with missing optional fields', () => { + const word: OCRWord = { + text: 'test', + x_mm: 0, + y_mm: 0, + width_mm: 10, + height_mm: 5, + column_type: 'unknown', + logical_row: 0, + } + const props = createTextProps(word) + expect(props).toBeDefined() + expect(props.text).toBe('test') + }) + + test('handles empty words array in export', () => { + const gridData = { + cells: [], + detected_columns: [], + page_dimensions: { width_mm: 210, height_mm: 297, format: 'A4' }, + } + const result = exportOCRData(gridData, 'session', 1) + expect(result.words).toHaveLength(0) + }) +})