From 0ac23089f472c1c49ba26eb44cd30ca9b43b5751 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 25 Feb 2026 23:09:41 +0100 Subject: [PATCH 01/23] docs: update CLAUDE.md for direct MacBook development workflow Remove rsync-based workflow, document git push + Mac Mini pull workflow. --- .claude/CLAUDE.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 480caa2..2138d88 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -6,19 +6,26 @@ | Geraet | Rolle | Aufgaben | |--------|-------|----------| -| **MacBook** | Client | Claude Terminal, Browser (Frontend-Tests) | -| **Mac Mini** | Server | Docker, alle Services, Code-Ausfuehrung, Tests, Git | +| **MacBook** | Entwicklung | Claude Terminal, Code-Entwicklung, Browser (Frontend-Tests) | +| **Mac Mini** | Server | Docker, alle Services, Tests, Builds, Deployment | -**WICHTIG:** Die Entwicklung findet vollstaendig auf dem **Mac Mini** statt! +**WICHTIG:** Code wird direkt auf dem MacBook in diesem Repo bearbeitet. Docker und Services laufen auf dem Mac Mini. -### SSH-Verbindung +### Entwicklungsworkflow ```bash -ssh macmini -# Projektverzeichnis: -cd /Users/benjaminadmin/Projekte/breakpilot-core +# 1. Code auf MacBook bearbeiten (dieses Verzeichnis) +# 2. Committen und pushen: +git push origin main && git push gitea main -# Einzelbefehle (BEVORZUGT): +# 3. Auf Mac Mini pullen und Container neu bauen: +ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-core && git pull --no-rebase origin main" +ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-core && /usr/local/bin/docker compose build --no-cache && /usr/local/bin/docker compose up -d " +``` + +### SSH-Verbindung (fuer Docker/Tests) + +```bash ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-core && " ``` From 13ba1457b0df847dae8831e050fb0f1c25ac7462 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 26 Feb 2026 23:24:47 +0100 Subject: [PATCH 02/23] Fix embedding client endpoint paths The embedding-service exposes endpoints at root level (/chunk, /embed, /extract-pdf, /rerank) not under /api/v1/. Fix the RAG service's embedding client to use the correct paths. Co-Authored-By: Claude Opus 4.6 --- rag-service/embedding_client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rag-service/embedding_client.py b/rag-service/embedding_client.py index 2ab14c1..fe0bf0d 100644 --- a/rag-service/embedding_client.py +++ b/rag-service/embedding_client.py @@ -30,7 +30,7 @@ class EmbeddingClient: """ async with httpx.AsyncClient(timeout=_TIMEOUT) as client: response = await client.post( - self._url("/api/v1/embeddings"), + self._url("/embed"), json={"texts": texts}, ) response.raise_for_status() @@ -60,7 +60,7 @@ class EmbeddingClient: """ async with httpx.AsyncClient(timeout=_TIMEOUT) as client: response = await client.post( - self._url("/api/v1/rerank"), + self._url("/rerank"), json={ "query": query, "documents": documents, @@ -88,7 +88,7 @@ class EmbeddingClient: """ async with httpx.AsyncClient(timeout=_TIMEOUT) as client: response = await client.post( - self._url("/api/v1/chunk"), + self._url("/chunk"), json={ "text": text, "strategy": strategy, @@ -111,7 +111,7 @@ class EmbeddingClient: """ async with httpx.AsyncClient(timeout=_TIMEOUT) as client: response = await client.post( - self._url("/api/v1/extract-pdf"), + self._url("/extract-pdf"), files={"file": ("document.pdf", pdf_bytes, "application/pdf")}, ) response.raise_for_status() From d7cc6bfbc7d0853dbc82ab7438ee92fb6cd44a9c Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 26 Feb 2026 23:29:23 +0100 Subject: [PATCH 03/23] Switch embedding model to bge-m3 (1024-dim) The Qdrant collections use 1024-dim vectors (bge-m3) but the embedding-service was configured with all-MiniLM-L6-v2 (384-dim). Also increase memory limit to 8G for the larger model. Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 8449106..1fecb6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -414,7 +414,7 @@ services: - embedding_models:/root/.cache/huggingface environment: EMBEDDING_BACKEND: ${EMBEDDING_BACKEND:-local} - LOCAL_EMBEDDING_MODEL: ${LOCAL_EMBEDDING_MODEL:-sentence-transformers/all-MiniLM-L6-v2} + LOCAL_EMBEDDING_MODEL: ${LOCAL_EMBEDDING_MODEL:-BAAI/bge-m3} LOCAL_RERANKER_MODEL: ${LOCAL_RERANKER_MODEL:-cross-encoder/ms-marco-MiniLM-L-6-v2} PDF_EXTRACTION_BACKEND: ${PDF_EXTRACTION_BACKEND:-pymupdf} OPENAI_API_KEY: ${OPENAI_API_KEY:-} @@ -423,7 +423,7 @@ services: deploy: resources: limits: - memory: 4G + memory: 8G healthcheck: test: ["CMD", "python", "-c", "import httpx; r=httpx.get('http://127.0.0.1:8087/health'); r.raise_for_status()"] interval: 30s From 92ca5b7ba57c31282aecf232aaa824d6bdece241 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 27 Feb 2026 07:46:57 +0100 Subject: [PATCH 04/23] feat(rag): use Ollama for embeddings instead of embedding-service Switch to Ollama's bge-m3 model (1024-dim) for generating embeddings, solving the dimension mismatch with Qdrant collections. Embedding-service still used for chunking, reranking, and PDF extraction. Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 4 ++ rag-service/embedding_client.py | 83 ++++++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 22 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 1fecb6b..3da1012 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -385,8 +385,12 @@ services: MINIO_BUCKET: ${MINIO_BUCKET:-breakpilot-rag} MINIO_SECURE: "false" EMBEDDING_SERVICE_URL: http://embedding-service:8087 + OLLAMA_URL: ${OLLAMA_URL:-http://host.docker.internal:11434} + OLLAMA_EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-bge-m3} JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} ENVIRONMENT: ${ENVIRONMENT:-development} + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: qdrant: condition: service_healthy diff --git a/rag-service/embedding_client.py b/rag-service/embedding_client.py index fe0bf0d..a1d5125 100644 --- a/rag-service/embedding_client.py +++ b/rag-service/embedding_client.py @@ -1,4 +1,5 @@ import logging +import os from typing import Optional import httpx @@ -8,44 +9,82 @@ from config import settings logger = logging.getLogger("rag-service.embedding") _TIMEOUT = httpx.Timeout(timeout=120.0, connect=10.0) +_EMBED_TIMEOUT = httpx.Timeout(timeout=300.0, connect=10.0) + +# Ollama config for embeddings (bge-m3, 1024-dim) +_OLLAMA_URL = os.getenv("OLLAMA_URL", "http://ollama:11434") +_OLLAMA_EMBED_MODEL = os.getenv("OLLAMA_EMBED_MODEL", "bge-m3") + +# Batch size for Ollama embedding requests +_EMBED_BATCH_SIZE = int(os.getenv("EMBED_BATCH_SIZE", "32")) class EmbeddingClient: - """HTTP client for the embedding-service (port 8087).""" + """ + Hybrid client: + - Embeddings via Ollama (bge-m3, 1024-dim) for Qdrant compatibility + - Chunking + PDF extraction via embedding-service (port 8087) + """ def __init__(self) -> None: - self._base_url: str = settings.EMBEDDING_SERVICE_URL.rstrip("/") + self._embed_svc_url: str = settings.EMBEDDING_SERVICE_URL.rstrip("/") + self._ollama_url: str = _OLLAMA_URL.rstrip("/") + self._embed_model: str = _OLLAMA_EMBED_MODEL - def _url(self, path: str) -> str: - return f"{self._base_url}{path}" + def _svc_url(self, path: str) -> str: + return f"{self._embed_svc_url}{path}" # ------------------------------------------------------------------ - # Embeddings + # Embeddings (via Ollama) # ------------------------------------------------------------------ async def generate_embeddings(self, texts: list[str]) -> list[list[float]]: """ - Send a batch of texts to the embedding service and return a list of - embedding vectors. + Generate embeddings via Ollama's bge-m3 model. + Processes in batches to avoid timeout on large uploads. """ - async with httpx.AsyncClient(timeout=_TIMEOUT) as client: - response = await client.post( - self._url("/embed"), - json={"texts": texts}, - ) - response.raise_for_status() - data = response.json() - return data.get("embeddings", []) + all_embeddings: list[list[float]] = [] + + for i in range(0, len(texts), _EMBED_BATCH_SIZE): + batch = texts[i : i + _EMBED_BATCH_SIZE] + batch_embeddings = [] + + async with httpx.AsyncClient(timeout=_EMBED_TIMEOUT) as client: + for text in batch: + response = await client.post( + f"{self._ollama_url}/api/embeddings", + json={ + "model": self._embed_model, + "prompt": text, + }, + ) + response.raise_for_status() + data = response.json() + embedding = data.get("embedding", []) + if not embedding: + raise ValueError( + f"Ollama returned empty embedding for model {self._embed_model}" + ) + batch_embeddings.append(embedding) + + all_embeddings.extend(batch_embeddings) + + if i + _EMBED_BATCH_SIZE < len(texts): + logger.info( + "Embedding progress: %d/%d", len(all_embeddings), len(texts) + ) + + return all_embeddings async def generate_single_embedding(self, text: str) -> list[float]: """Convenience wrapper for a single text.""" results = await self.generate_embeddings([text]) if not results: - raise ValueError("Embedding service returned empty result") + raise ValueError("Ollama returned empty result") return results[0] # ------------------------------------------------------------------ - # Reranking + # Reranking (via embedding-service) # ------------------------------------------------------------------ async def rerank_documents( @@ -60,7 +99,7 @@ class EmbeddingClient: """ async with httpx.AsyncClient(timeout=_TIMEOUT) as client: response = await client.post( - self._url("/rerank"), + self._svc_url("/rerank"), json={ "query": query, "documents": documents, @@ -72,7 +111,7 @@ class EmbeddingClient: return data.get("results", []) # ------------------------------------------------------------------ - # Chunking + # Chunking (via embedding-service) # ------------------------------------------------------------------ async def chunk_text( @@ -88,7 +127,7 @@ class EmbeddingClient: """ async with httpx.AsyncClient(timeout=_TIMEOUT) as client: response = await client.post( - self._url("/chunk"), + self._svc_url("/chunk"), json={ "text": text, "strategy": strategy, @@ -101,7 +140,7 @@ class EmbeddingClient: return data.get("chunks", []) # ------------------------------------------------------------------ - # PDF extraction + # PDF extraction (via embedding-service) # ------------------------------------------------------------------ async def extract_pdf(self, pdf_bytes: bytes) -> str: @@ -111,7 +150,7 @@ class EmbeddingClient: """ async with httpx.AsyncClient(timeout=_TIMEOUT) as client: response = await client.post( - self._url("/extract-pdf"), + self._svc_url("/extract-pdf"), files={"file": ("document.pdf", pdf_bytes, "application/pdf")}, ) response.raise_for_status() From 5c8307f58a5d56c6167e93004bcd536e73186864 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Fri, 27 Feb 2026 07:51:12 +0100 Subject: [PATCH 05/23] fix(rag): use query_points instead of deprecated search method qdrant-client 1.17.0 removed the search() method in favor of query_points(). Update the wrapper to use the new API. Co-Authored-By: Claude Opus 4.6 --- rag-service/qdrant_client_wrapper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rag-service/qdrant_client_wrapper.py b/rag-service/qdrant_client_wrapper.py index 2678497..bfeafc0 100644 --- a/rag-service/qdrant_client_wrapper.py +++ b/rag-service/qdrant_client_wrapper.py @@ -167,12 +167,13 @@ class QdrantClientWrapper: ) qdrant_filter = qmodels.Filter(must=must_conditions) - results = self.client.search( + results = self.client.query_points( collection_name=collection, - query_vector=query_vector, + query=query_vector, limit=limit, query_filter=qdrant_filter, score_threshold=score_threshold, + with_payload=True, ) return [ @@ -181,7 +182,7 @@ class QdrantClientWrapper: "score": hit.score, "payload": hit.payload or {}, } - for hit in results + for hit in results.points ] # ------------------------------------------------------------------ From 403cb5b85d3b88571574876c91473fed30a703b3 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 28 Feb 2026 09:07:03 +0100 Subject: [PATCH 06/23] fix: increase RAG service proxy timeout to 600s - Increase proxy_read_timeout from 300s to 600s for large PDF uploads - Add proxy_send_timeout 600s (was defaulting to 60s) - Fixes 504 Gateway Timeout when uploading 7.5MB+ IFRS PDFs Co-Authored-By: Claude Opus 4.6 --- nginx/conf.d/default.conf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf index 8a84f76..70f9711 100644 --- a/nginx/conf.d/default.conf +++ b/nginx/conf.d/default.conf @@ -564,7 +564,8 @@ server { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; - proxy_read_timeout 300s; + proxy_read_timeout 600s; + proxy_send_timeout 600s; } } From 1c8f528c7a16dc40846d0434eec72392ae289e08 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 28 Feb 2026 17:46:13 +0100 Subject: [PATCH 07/23] feat(nginx): add /rag-originals/ location for QA PDF serving Serves original regulation PDFs from ~/rag-originals/ on port 3002 for the RAG QA Split-View Chunk-Browser. Adds volume mount to nginx. Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 1 + nginx/conf.d/default.conf | 9 +++++++++ 2 files changed, 10 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 3da1012..3cf8269 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,6 +76,7 @@ services: - ./nginx/conf.d:/etc/nginx/conf.d:ro - vault_certs:/etc/nginx/certs:ro - ./nginx/html:/usr/share/nginx/html/portal:ro + - /Users/benjaminadmin/rag-originals:/data/rag-originals:ro depends_on: vault-agent: condition: service_started diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf index 70f9711..4487746 100644 --- a/nginx/conf.d/default.conf +++ b/nginx/conf.d/default.conf @@ -248,6 +248,15 @@ server { proxy_set_header X-Forwarded-Proto https; } + # RAG Original-PDFs fuer QA Split-View + location /rag-originals/ { + alias /data/rag-originals/; + autoindex off; + types { application/pdf pdf; } + add_header Cache-Control "public, max-age=86400"; + add_header X-Content-Type-Options nosniff; + } + # Admin Lehrer Frontend (fallback for everything else) location / { set $upstream_admin_lehrer bp-lehrer-admin:3000; From 72e0f18d083499c2acbaa4efec35e56cc255ba38 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Tue, 3 Mar 2026 18:42:53 +0100 Subject: [PATCH 08/23] =?UTF-8?q?feat(sbom):=20OCR-=20und=20HTR-Pakete=20f?= =?UTF-8?q?=C3=BCr=20klausur-service=20erg=C3=A4nzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Neue Python-Pakete dokumentiert: - pyspellchecker 0.8.1+ (MIT) – OCR-Regelkorrektur Step 6 - pytesseract 0.3.10+ (Apache-2.0) – Tesseract OCR Wrapper - opencv-python-headless 4.8+ (Apache-2.0) – Bildverarbeitung/Inpainting - rapidocr-onnxruntime (Apache-2.0) – Schnelles OCR ARM64 - onnxruntime (MIT) – ONNX-Inferenz für RapidOCR - eng-to-ipa (MIT) – IPA-Lautschrift-Lookup - sentence-transformers 2.2+ (Apache-2.0) – Lokale Embeddings - torch 2.0+ (BSD-3-Clause) – ML-Framework CPU/MPS - transformers 4.x (Apache-2.0) – TrOCR/HTR-Modelle Co-Authored-By: Claude Sonnet 4.6 --- admin-core/app/(admin)/infrastructure/sbom/page.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/admin-core/app/(admin)/infrastructure/sbom/page.tsx b/admin-core/app/(admin)/infrastructure/sbom/page.tsx index 7146e4e..f605201 100644 --- a/admin-core/app/(admin)/infrastructure/sbom/page.tsx +++ b/admin-core/app/(admin)/infrastructure/sbom/page.tsx @@ -193,6 +193,15 @@ const PYTHON_PACKAGES: Component[] = [ { type: 'library', name: 'scipy', version: '1.14+', category: 'python', description: 'Signal Processing (Audio)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/scipy/scipy' }, { type: 'library', name: 'redis', version: '5.x', category: 'python', description: 'Valkey/Redis Client (Voice Sessions)', license: 'MIT', sourceUrl: 'https://github.com/redis/redis-py' }, { type: 'library', name: 'pydantic-settings', version: '2.x', category: 'python', description: 'Settings Management (Voice Config)', license: 'MIT', sourceUrl: 'https://github.com/pydantic/pydantic-settings' }, + { type: 'library', name: 'pyspellchecker', version: '0.8.1+', category: 'python', description: 'Regel-basierte OCR-Korrektur (klausur-service Schritt 6)', license: 'MIT', sourceUrl: 'https://github.com/barrust/pyspellchecker' }, + { type: 'library', name: 'pytesseract', version: '0.3.10+', category: 'python', description: 'Tesseract OCR Engine Wrapper (klausur-service)', license: 'Apache-2.0', sourceUrl: 'https://github.com/madmaze/pytesseract' }, + { type: 'library', name: 'opencv-python-headless', version: '4.8+', category: 'python', description: 'Bildverarbeitung, Projektionsprofile, Inpainting (klausur-service)', license: 'Apache-2.0', sourceUrl: 'https://github.com/opencv/opencv-python' }, + { type: 'library', name: 'rapidocr-onnxruntime', version: 'latest', category: 'python', description: 'Schnelles OCR ARM64 via ONNX (klausur-service)', license: 'Apache-2.0', sourceUrl: 'https://github.com/RapidAI/RapidOCR' }, + { type: 'library', name: 'onnxruntime', version: 'latest', category: 'python', description: 'ONNX-Inferenz für RapidOCR (klausur-service)', license: 'MIT', sourceUrl: 'https://github.com/microsoft/onnxruntime' }, + { type: 'library', name: 'eng-to-ipa', version: 'latest', category: 'python', description: 'IPA-Lautschrift-Lookup (klausur-service Vokabel-Pipeline)', license: 'MIT', sourceUrl: 'https://github.com/mphilli/English-to-IPA' }, + { type: 'library', name: 'sentence-transformers', version: '2.2+', category: 'python', description: 'Lokale Embeddings (klausur-service, rag-service)', license: 'Apache-2.0', sourceUrl: 'https://github.com/UKPLab/sentence-transformers' }, + { type: 'library', name: 'torch', version: '2.0+', category: 'python', description: 'ML-Framework CPU/MPS (TrOCR, klausur-service)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/pytorch/pytorch' }, + { type: 'library', name: 'transformers', version: '4.x', category: 'python', description: 'HuggingFace Transformers (TrOCR, Handschrift-HTR)', license: 'Apache-2.0', sourceUrl: 'https://github.com/huggingface/transformers' }, ] // Key Go modules (from go.mod files) From 85df14c5529a3fe25a0a9139c3917695f3d8a5f4 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 4 Mar 2026 12:23:57 +0100 Subject: [PATCH 09/23] feat: HTTPS-Proxy fuer Compliance MkDocs auf Port 8011 Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 1 + nginx/conf.d/default.conf | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 3cf8269..f76e227 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,6 +72,7 @@ services: - "8443:8443" # Jitsi Meet - "3008:3008" # Admin Core - "3010:3010" # Portal Dashboard + - "8011:8011" # Compliance Docs (MkDocs) volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - vault_certs:/etc/nginx/certs:ro diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf index 4487746..3afb427 100644 --- a/nginx/conf.d/default.conf +++ b/nginx/conf.d/default.conf @@ -630,6 +630,31 @@ server { } } +# ========================================================= +# COMPLIANCE: Docs (MkDocs) on port 8011 +# ========================================================= +server { + listen 8011 ssl; + http2 on; + server_name macmini localhost; + + ssl_certificate /etc/nginx/certs/macmini.crt; + ssl_certificate_key /etc/nginx/certs/macmini.key; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers off; + + location / { + set $upstream_docs bp-compliance-docs:80; + proxy_pass http://$upstream_docs; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto https; + } +} + # ========================================================= # CORE: Jitsi Meet on port 8443 # ========================================================= From db1b3c40edc577ce3cfeb6ce7dff9705e1325e9f Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 4 Mar 2026 22:45:11 +0100 Subject: [PATCH 10/23] fix: Compliance Dashboard + Katalogverwaltung Kacheln vom Portal entfernt Beide verlinkten auf /dashboard und waren redundant zum SDK-Einstiegspunkt. Co-Authored-By: Claude Opus 4.6 --- nginx/html/index.html | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/nginx/html/index.html b/nginx/html/index.html index b63cf0e..1740ae7 100644 --- a/nginx/html/index.html +++ b/nginx/html/index.html @@ -679,24 +679,6 @@ - -
-
-

Compliance Dashboard

-

Kataloge, Statistiken, Verwaltung

-
macmini:3007/dashboard
-
-
- - -
-
-

Katalogverwaltung

-

SDK-Kataloge & Auswahltabellen

-
macmini:3007/dashboard
-
-
-
From 1527f4ffe7853415fdf8f6f5ac0774f96534d15c Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 5 Mar 2026 17:01:30 +0100 Subject: [PATCH 11/23] =?UTF-8?q?refactor:=20Camunda=20l=C3=B6schen,=20Jit?= =?UTF-8?q?si/Matrix/Voice=20nach=20Lehrer=20verschieben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Camunda war nie aktiv (nur Frontend-Stub ohne Backend) — komplett entfernt. Jitsi (5 Services), Synapse (2 Services) und Voice Service werden ausschließlich vom Lehrer-Stack genutzt und gehören nicht in Core. Nginx-Container-Namen auf bp-lehrer-jitsi-* aktualisiert (shared Network). Co-Authored-By: Claude Opus 4.6 --- docker-compose.yml | 298 +------------------------------------- nginx/conf.d/default.conf | 18 +-- 2 files changed, 10 insertions(+), 306 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index f76e227..6208432 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,17 +19,6 @@ volumes: valkey_data: qdrant_data: minio_data: - # Communication - synapse_data: - synapse_db_data: - jitsi_web_config: - jitsi_web_crontabs: - jitsi_transcripts: - jitsi_prosody_config: - jitsi_prosody_plugins: - jitsi_jicofo_config: - jitsi_jvb_config: - jibri_recordings: # CI/CD gitea_data: gitea_config: @@ -42,7 +31,6 @@ volumes: erpnext_sites: erpnext_logs: # Services - voice_session_data: embedding_models: services: @@ -195,26 +183,6 @@ services: networks: - breakpilot-network - synapse-db: - image: postgres:16-alpine - container_name: bp-core-synapse-db - profiles: [chat] - environment: - POSTGRES_USER: synapse - POSTGRES_PASSWORD: ${SYNAPSE_DB_PASSWORD:-synapse_secret} - POSTGRES_DB: synapse - POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --lc-collate=C --lc-ctype=C" - volumes: - - synapse_db_data:/var/lib/postgresql/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U synapse"] - interval: 5s - timeout: 5s - retries: 5 - restart: unless-stopped - networks: - - breakpilot-network - # ========================================================= # VECTOR DB & OBJECT STORAGE # ========================================================= @@ -453,7 +421,7 @@ services: - "8099:8099" environment: PORT: 8099 - CHECK_SERVICES: "postgres:5432,valkey:6379,qdrant:6333,minio:9000,backend-core:8000,rag-service:8097,embedding-service:8087,voice-service:8091" + CHECK_SERVICES: "postgres:5432,valkey:6379,qdrant:6333,minio:9000,backend-core:8000,rag-service:8097,embedding-service:8087" depends_on: postgres: condition: service_healthy @@ -466,199 +434,6 @@ services: networks: - breakpilot-network - # ========================================================= - # COMMUNICATION - # ========================================================= - synapse: - image: matrixdotorg/synapse:latest - container_name: bp-core-synapse - profiles: [chat] - ports: - - "8008:8008" - - "8448:8448" - volumes: - - synapse_data:/data - environment: - SYNAPSE_SERVER_NAME: ${SYNAPSE_SERVER_NAME:-macmini} - SYNAPSE_REPORT_STATS: "no" - SYNAPSE_NO_TLS: "true" - SYNAPSE_ENABLE_REGISTRATION: ${SYNAPSE_ENABLE_REGISTRATION:-true} - SYNAPSE_LOG_LEVEL: ${SYNAPSE_LOG_LEVEL:-WARNING} - UID: "1000" - GID: "1000" - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:8008/health"] - interval: 30s - timeout: 10s - start_period: 30s - retries: 3 - depends_on: - synapse-db: - condition: service_healthy - restart: unless-stopped - networks: - - breakpilot-network - - jitsi-web: - image: jitsi/web:stable-9823 - container_name: bp-core-jitsi-web - expose: - - "80" - volumes: - - jitsi_web_config:/config - - jitsi_web_crontabs:/var/spool/cron/crontabs - - jitsi_transcripts:/usr/share/jitsi-meet/transcripts - environment: - ENABLE_XMPP_WEBSOCKET: "true" - ENABLE_COLIBRI_WEBSOCKET: "true" - XMPP_DOMAIN: ${XMPP_DOMAIN:-meet.jitsi} - XMPP_BOSH_URL_BASE: http://jitsi-xmpp:5280 - XMPP_MUC_DOMAIN: ${XMPP_MUC_DOMAIN:-muc.meet.jitsi} - XMPP_GUEST_DOMAIN: ${XMPP_GUEST_DOMAIN:-guest.meet.jitsi} - TZ: ${TZ:-Europe/Berlin} - PUBLIC_URL: ${JITSI_PUBLIC_URL:-https://macmini:8443} - JICOFO_AUTH_USER: focus - ENABLE_AUTH: ${JITSI_ENABLE_AUTH:-false} - ENABLE_GUESTS: "true" - ENABLE_RECORDING: "true" - ENABLE_LIVESTREAMING: "false" - DISABLE_HTTPS: "true" - APP_NAME: "BreakPilot Meet" - NATIVE_APP_NAME: "BreakPilot Meet" - PROVIDER_NAME: "BreakPilot" - depends_on: - - jitsi-xmpp - networks: - breakpilot-network: - aliases: - - meet.jitsi - - jitsi-xmpp: - image: jitsi/prosody:stable-9823 - container_name: bp-core-jitsi-xmpp - volumes: - - jitsi_prosody_config:/config - - jitsi_prosody_plugins:/prosody-plugins-custom - environment: - XMPP_DOMAIN: ${XMPP_DOMAIN:-meet.jitsi} - XMPP_AUTH_DOMAIN: ${XMPP_AUTH_DOMAIN:-auth.meet.jitsi} - XMPP_MUC_DOMAIN: ${XMPP_MUC_DOMAIN:-muc.meet.jitsi} - XMPP_INTERNAL_MUC_DOMAIN: ${XMPP_INTERNAL_MUC_DOMAIN:-internal-muc.meet.jitsi} - XMPP_GUEST_DOMAIN: ${XMPP_GUEST_DOMAIN:-guest.meet.jitsi} - XMPP_RECORDER_DOMAIN: ${XMPP_RECORDER_DOMAIN:-recorder.meet.jitsi} - XMPP_CROSS_DOMAIN: "true" - TZ: ${TZ:-Europe/Berlin} - JICOFO_AUTH_USER: focus - JICOFO_AUTH_PASSWORD: ${JICOFO_AUTH_PASSWORD:-jicofo_secret} - JVB_AUTH_USER: jvb - JVB_AUTH_PASSWORD: ${JVB_AUTH_PASSWORD:-jvb_secret} - JIBRI_XMPP_USER: jibri - JIBRI_XMPP_PASSWORD: ${JIBRI_XMPP_PASSWORD:-jibri_secret} - JIBRI_RECORDER_USER: recorder - JIBRI_RECORDER_PASSWORD: ${JIBRI_RECORDER_PASSWORD:-recorder_secret} - LOG_LEVEL: ${XMPP_LOG_LEVEL:-warn} - PUBLIC_URL: ${JITSI_PUBLIC_URL:-https://macmini:8443} - ENABLE_AUTH: ${JITSI_ENABLE_AUTH:-false} - ENABLE_GUESTS: "true" - restart: unless-stopped - networks: - breakpilot-network: - aliases: - - xmpp.meet.jitsi - - jitsi-jicofo: - image: jitsi/jicofo:stable-9823 - container_name: bp-core-jitsi-jicofo - volumes: - - jitsi_jicofo_config:/config - environment: - XMPP_DOMAIN: ${XMPP_DOMAIN:-meet.jitsi} - XMPP_AUTH_DOMAIN: ${XMPP_AUTH_DOMAIN:-auth.meet.jitsi} - XMPP_MUC_DOMAIN: ${XMPP_MUC_DOMAIN:-muc.meet.jitsi} - XMPP_INTERNAL_MUC_DOMAIN: ${XMPP_INTERNAL_MUC_DOMAIN:-internal-muc.meet.jitsi} - XMPP_SERVER: jitsi-xmpp - JICOFO_AUTH_USER: focus - JICOFO_AUTH_PASSWORD: ${JICOFO_AUTH_PASSWORD:-jicofo_secret} - TZ: ${TZ:-Europe/Berlin} - ENABLE_AUTH: ${JITSI_ENABLE_AUTH:-false} - AUTH_TYPE: internal - ENABLE_AUTO_OWNER: "true" - depends_on: - - jitsi-xmpp - restart: unless-stopped - networks: - - breakpilot-network - - jitsi-jvb: - image: jitsi/jvb:stable-9823 - container_name: bp-core-jitsi-jvb - ports: - - "10000:10000/udp" - - "8080:8080" - volumes: - - jitsi_jvb_config:/config - environment: - XMPP_DOMAIN: ${XMPP_DOMAIN:-meet.jitsi} - XMPP_AUTH_DOMAIN: ${XMPP_AUTH_DOMAIN:-auth.meet.jitsi} - XMPP_INTERNAL_MUC_DOMAIN: ${XMPP_INTERNAL_MUC_DOMAIN:-internal-muc.meet.jitsi} - XMPP_SERVER: jitsi-xmpp - JVB_AUTH_USER: jvb - JVB_AUTH_PASSWORD: ${JVB_AUTH_PASSWORD:-jvb_secret} - JVB_PORT: 10000 - JVB_STUN_SERVERS: ${JVB_STUN_SERVERS:-stun.l.google.com:19302} - TZ: ${TZ:-Europe/Berlin} - PUBLIC_URL: ${JITSI_PUBLIC_URL:-https://macmini:8443} - COLIBRI_REST_ENABLED: "true" - ENABLE_COLIBRI_WEBSOCKET: "true" - depends_on: - - jitsi-xmpp - restart: unless-stopped - networks: - - breakpilot-network - - jibri: - build: - context: ./docker/jibri - dockerfile: Dockerfile - container_name: bp-core-jibri - volumes: - - jibri_recordings:/recordings - - /dev/shm:/dev/shm - shm_size: 2gb - cap_add: - - SYS_ADMIN - - NET_BIND_SERVICE - environment: - XMPP_DOMAIN: ${XMPP_DOMAIN:-meet.jitsi} - XMPP_AUTH_DOMAIN: ${XMPP_AUTH_DOMAIN:-auth.meet.jitsi} - XMPP_INTERNAL_MUC_DOMAIN: ${XMPP_INTERNAL_MUC_DOMAIN:-internal-muc.meet.jitsi} - XMPP_RECORDER_DOMAIN: ${XMPP_RECORDER_DOMAIN:-recorder.meet.jitsi} - XMPP_SERVER: jitsi-xmpp - XMPP_MUC_DOMAIN: ${XMPP_MUC_DOMAIN:-muc.meet.jitsi} - JIBRI_XMPP_USER: jibri - JIBRI_XMPP_PASSWORD: ${JIBRI_XMPP_PASSWORD:-jibri_secret} - JIBRI_RECORDER_USER: recorder - JIBRI_RECORDER_PASSWORD: ${JIBRI_RECORDER_PASSWORD:-recorder_secret} - JIBRI_BREWERY_MUC: JibriBrewery - JIBRI_RECORDING_DIR: /recordings - JIBRI_FINALIZE_SCRIPT: /finalize.sh - TZ: ${TZ:-Europe/Berlin} - DISPLAY: ":0" - RESOLUTION: "1920x1080" - MINIO_ENDPOINT: minio:9000 - MINIO_ACCESS_KEY: ${MINIO_ROOT_USER:-breakpilot} - MINIO_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-breakpilot123} - MINIO_BUCKET: ${MINIO_BUCKET:-breakpilot-recordings} - BACKEND_WEBHOOK_URL: http://backend-core:8000/api/recordings/webhook - depends_on: - - jitsi-xmpp - - minio - profiles: - - recording - restart: unless-stopped - networks: - - breakpilot-network - # ========================================================= # DEVOPS & CI/CD # ========================================================= @@ -780,38 +555,6 @@ services: networks: - breakpilot-network - # ========================================================= - # WORKFLOW ENGINE - # ========================================================= - camunda: - image: camunda/camunda-bpm-platform:7.21.0 - container_name: bp-core-camunda - ports: - - "8089:8080" - environment: - DB_DRIVER: org.postgresql.Driver - DB_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-breakpilot_db} - DB_USERNAME: ${POSTGRES_USER:-breakpilot} - DB_PASSWORD: ${POSTGRES_PASSWORD:-breakpilot123} - DB_VALIDATE_ON_BORROW: "true" - WAIT_FOR: postgres:5432 - CAMUNDA_BPM_ADMIN_USER_ID: ${CAMUNDA_ADMIN_USER:-admin} - CAMUNDA_BPM_ADMIN_USER_PASSWORD: ${CAMUNDA_ADMIN_PASSWORD:-admin} - depends_on: - postgres: - condition: service_healthy - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:8080/camunda/api/engine"] - interval: 30s - timeout: 10s - start_period: 60s - retries: 5 - profiles: - - bpmn - restart: unless-stopped - networks: - - breakpilot-network - # ========================================================= # DOCUMENTATION & UTILITIES # ========================================================= @@ -846,45 +589,6 @@ services: networks: - breakpilot-network - # ========================================================= - # VOICE SERVICE - # ========================================================= - voice-service: - build: - context: ./voice-service - dockerfile: Dockerfile - container_name: bp-core-voice-service - platform: linux/arm64 - expose: - - "8091" - volumes: - - voice_session_data:/app/data/sessions - environment: - PORT: 8091 - DATABASE_URL: postgresql://${POSTGRES_USER:-breakpilot}:${POSTGRES_PASSWORD:-breakpilot123}@postgres:5432/${POSTGRES_DB:-breakpilot_db} - VALKEY_URL: redis://valkey:6379/0 - KLAUSUR_SERVICE_URL: http://bp-lehrer-klausur-service:8086 - OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://host.docker.internal:11434} - OLLAMA_VOICE_MODEL: ${OLLAMA_VOICE_MODEL:-llama3.2} - ENVIRONMENT: ${ENVIRONMENT:-development} - JWT_SECRET: ${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} - extra_hosts: - - "host.docker.internal:host-gateway" - depends_on: - postgres: - condition: service_healthy - valkey: - condition: service_started - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:8091/health"] - interval: 30s - timeout: 10s - start_period: 60s - retries: 3 - restart: unless-stopped - networks: - - breakpilot-network - # ========================================================= # NIGHT SCHEDULER # ========================================================= diff --git a/nginx/conf.d/default.conf b/nginx/conf.d/default.conf index 3afb427..2742c6d 100644 --- a/nginx/conf.d/default.conf +++ b/nginx/conf.d/default.conf @@ -32,7 +32,7 @@ server { # Jitsi WebSocket endpoints location /xmpp-websocket { - set $upstream_jitsi bp-core-jitsi-web:80; + set $upstream_jitsi bp-lehrer-jitsi-web:80; proxy_pass http://$upstream_jitsi; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -46,7 +46,7 @@ server { } location /colibri-ws { - set $upstream_jvb bp-core-jitsi-jvb:9090; + set $upstream_jvb bp-lehrer-jitsi-jvb:9090; proxy_pass http://$upstream_jvb; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -60,7 +60,7 @@ server { } location /http-bind { - set $upstream_jitsi bp-core-jitsi-web:80; + set $upstream_jitsi bp-lehrer-jitsi-web:80; proxy_pass http://$upstream_jitsi; proxy_http_version 1.1; proxy_set_header Host $host; @@ -71,7 +71,7 @@ server { # Jitsi static assets location ~ ^/(css|images|fonts|sounds|static|libs|lang|connection_optimization)/ { - set $upstream_jitsi bp-core-jitsi-web:80; + set $upstream_jitsi bp-lehrer-jitsi-web:80; proxy_pass http://$upstream_jitsi; proxy_http_version 1.1; proxy_set_header Host $host; @@ -81,7 +81,7 @@ server { } location ~ ^/(config\.js|interface_config\.js|logging_config\.js|external_api\.js|external_api\.min\.js|favicon\.ico|robots\.txt|manifest\.json|pwa-worker\.js) { - set $upstream_jitsi bp-core-jitsi-web:80; + set $upstream_jitsi bp-lehrer-jitsi-web:80; proxy_pass http://$upstream_jitsi; proxy_http_version 1.1; proxy_set_header Host $host; @@ -91,7 +91,7 @@ server { } location /jitsi/ { - set $upstream_jitsi bp-core-jitsi-web:80; + set $upstream_jitsi bp-lehrer-jitsi-web:80; rewrite ^/jitsi(/.*)$ $1 break; proxy_pass http://$upstream_jitsi; proxy_http_version 1.1; @@ -670,7 +670,7 @@ server { ssl_prefer_server_ciphers off; location /xmpp-websocket { - set $upstream_jitsi bp-core-jitsi-web:80; + set $upstream_jitsi bp-lehrer-jitsi-web:80; proxy_pass http://$upstream_jitsi; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -684,7 +684,7 @@ server { } location /colibri-ws { - set $upstream_jvb bp-core-jitsi-jvb:9090; + set $upstream_jvb bp-lehrer-jitsi-jvb:9090; proxy_pass http://$upstream_jvb; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; @@ -698,7 +698,7 @@ server { } location / { - set $upstream_jitsi bp-core-jitsi-web:80; + set $upstream_jitsi bp-lehrer-jitsi-web:80; proxy_pass http://$upstream_jitsi; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; From c8cc8774db33382f41c695ae492cef7a94348dd4 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 5 Mar 2026 17:36:22 +0100 Subject: [PATCH 12/23] refactor: Video Chat, Voice Service, Alerts Seiten aus Core Admin entfernt - Kommunikation-Seiten nach Lehrer migriert - API-Routes, Health-Check, Navigation bereinigt - Screen-Flow, SBOM, Tests aktualisiert Co-Authored-By: Claude Opus 4.6 --- .../app/(admin)/communication/alerts/page.tsx | 912 ------------------ .../app/(admin)/communication/mail/page.tsx | 1 - .../app/(admin)/communication/matrix/page.tsx | 594 ------------ .../(admin)/communication/video-chat/page.tsx | 635 ------------ .../(admin)/development/screen-flow/page.tsx | 11 +- .../app/(admin)/infrastructure/gpu/page.tsx | 1 - .../app/(admin)/infrastructure/sbom/page.tsx | 5 - .../app/(admin)/infrastructure/tests/page.tsx | 3 - .../api/admin/communication/stats/route.ts | 210 ---- admin-core/app/api/admin/health/route.ts | 2 - admin-core/app/api/alerts/[...path]/route.ts | 172 ---- admin-core/lib/navigation.ts | 26 +- 12 files changed, 3 insertions(+), 2569 deletions(-) delete mode 100644 admin-core/app/(admin)/communication/alerts/page.tsx delete mode 100644 admin-core/app/(admin)/communication/matrix/page.tsx delete mode 100644 admin-core/app/(admin)/communication/video-chat/page.tsx delete mode 100644 admin-core/app/api/admin/communication/stats/route.ts delete mode 100644 admin-core/app/api/alerts/[...path]/route.ts diff --git a/admin-core/app/(admin)/communication/alerts/page.tsx b/admin-core/app/(admin)/communication/alerts/page.tsx deleted file mode 100644 index 3d73dd3..0000000 --- a/admin-core/app/(admin)/communication/alerts/page.tsx +++ /dev/null @@ -1,912 +0,0 @@ -'use client' - -/** - * Alerts Monitoring Admin Page (migrated from website/admin/alerts) - * - * Google Alerts & Feed-Ueberwachung Dashboard - * Provides inbox management, topic configuration, rule builder, and relevance profiles - */ - -import { useEffect, useState, useCallback } from 'react' -import { PagePurpose } from '@/components/common/PagePurpose' - -// Types -interface AlertItem { - id: string - title: string - url: string - snippet: string - topic_name: string - relevance_score: number | null - relevance_decision: string | null - status: string - fetched_at: string - published_at: string | null - matched_rule: string | null - tags: string[] -} - -interface Topic { - id: string - name: string - feed_url: string - feed_type: string - is_active: boolean - fetch_interval_minutes: number - last_fetched_at: string | null - alert_count: number -} - -interface Rule { - id: string - name: string - topic_id: string | null - conditions: Array<{ - field: string - operator: string - value: string | number - }> - action_type: string - action_config: Record - priority: number - is_active: boolean -} - -interface Profile { - priorities: string[] - exclusions: string[] - positive_examples: Array<{ title: string; url: string }> - negative_examples: Array<{ title: string; url: string }> - policies: { - keep_threshold: number - drop_threshold: number - } -} - -interface Stats { - total_alerts: number - new_alerts: number - kept_alerts: number - review_alerts: number - dropped_alerts: number - total_topics: number - active_topics: number - total_rules: number -} - -// Tab type -type TabId = 'dashboard' | 'inbox' | 'topics' | 'rules' | 'profile' | 'audit' | 'documentation' - -export default function AlertsPage() { - const [activeTab, setActiveTab] = useState('dashboard') - const [stats, setStats] = useState(null) - const [alerts, setAlerts] = useState([]) - const [topics, setTopics] = useState([]) - const [rules, setRules] = useState([]) - const [profile, setProfile] = useState(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(null) - const [inboxFilter, setInboxFilter] = useState('all') - - const API_BASE = '/api/alerts' - - const fetchData = useCallback(async () => { - try { - const [statsRes, alertsRes, topicsRes, rulesRes, profileRes] = await Promise.all([ - fetch(`${API_BASE}/stats`), - fetch(`${API_BASE}/inbox?limit=50`), - fetch(`${API_BASE}/topics`), - fetch(`${API_BASE}/rules`), - fetch(`${API_BASE}/profile`), - ]) - - if (statsRes.ok) setStats(await statsRes.json()) - if (alertsRes.ok) { - const data = await alertsRes.json() - setAlerts(data.items || []) - } - if (topicsRes.ok) { - const data = await topicsRes.json() - setTopics(data.topics || data.items || []) - } - if (rulesRes.ok) { - const data = await rulesRes.json() - setRules(data.rules || data.items || []) - } - if (profileRes.ok) setProfile(await profileRes.json()) - - setError(null) - } catch (err) { - setError(err instanceof Error ? err.message : 'Verbindungsfehler') - // Set demo data - setStats({ - total_alerts: 147, - new_alerts: 23, - kept_alerts: 89, - review_alerts: 12, - dropped_alerts: 23, - total_topics: 5, - active_topics: 4, - total_rules: 8, - }) - setAlerts([ - { - id: 'demo_1', - title: 'Neue Studie zur digitalen Bildung an Schulen', - url: 'https://example.com/artikel1', - snippet: 'Eine aktuelle Studie zeigt, dass digitale Lernmittel den Lernerfolg steigern koennen...', - topic_name: 'Digitale Bildung', - relevance_score: 0.85, - relevance_decision: 'KEEP', - status: 'new', - fetched_at: new Date().toISOString(), - published_at: null, - matched_rule: null, - tags: ['bildung', 'digital'], - }, - { - id: 'demo_2', - title: 'Inklusion: Fortbildungen fuer Lehrkraefte', - url: 'https://example.com/artikel2', - snippet: 'Das Kultusministerium bietet neue Fortbildungsangebote zum Thema Inklusion an...', - topic_name: 'Inklusion', - relevance_score: 0.72, - relevance_decision: 'KEEP', - status: 'new', - fetched_at: new Date(Date.now() - 3600000).toISOString(), - published_at: null, - matched_rule: null, - tags: ['inklusion'], - }, - ]) - setTopics([ - { - id: 'topic_1', - name: 'Digitale Bildung', - feed_url: 'https://google.com/alerts/feeds/123', - feed_type: 'rss', - is_active: true, - fetch_interval_minutes: 60, - last_fetched_at: new Date().toISOString(), - alert_count: 47, - }, - { - id: 'topic_2', - name: 'Inklusion', - feed_url: 'https://google.com/alerts/feeds/456', - feed_type: 'rss', - is_active: true, - fetch_interval_minutes: 60, - last_fetched_at: new Date(Date.now() - 1800000).toISOString(), - alert_count: 32, - }, - ]) - setRules([ - { - id: 'rule_1', - name: 'Stellenanzeigen ausschliessen', - topic_id: null, - conditions: [{ field: 'title', operator: 'contains', value: 'Stellenangebot' }], - action_type: 'drop', - action_config: {}, - priority: 10, - is_active: true, - }, - ]) - setProfile({ - priorities: ['Inklusion', 'digitale Bildung'], - exclusions: ['Stellenanzeigen', 'Werbung'], - positive_examples: [], - negative_examples: [], - policies: { keep_threshold: 0.7, drop_threshold: 0.3 }, - }) - } finally { - setLoading(false) - } - }, []) - - useEffect(() => { - fetchData() - }, [fetchData]) - - const formatTimeAgo = (dateStr: string | null) => { - if (!dateStr) return '-' - const date = new Date(dateStr) - const now = new Date() - const diffMs = now.getTime() - date.getTime() - const diffMins = Math.floor(diffMs / 60000) - - if (diffMins < 1) return 'gerade eben' - if (diffMins < 60) return `vor ${diffMins} Min.` - if (diffMins < 1440) return `vor ${Math.floor(diffMins / 60)} Std.` - return `vor ${Math.floor(diffMins / 1440)} Tagen` - } - - const getScoreBadge = (score: number | null) => { - if (score === null) return null - const pct = Math.round(score * 100) - let cls = 'bg-slate-100 text-slate-600' - if (pct >= 70) cls = 'bg-green-100 text-green-800' - else if (pct >= 40) cls = 'bg-amber-100 text-amber-800' - else cls = 'bg-red-100 text-red-800' - return {pct}% - } - - const getDecisionBadge = (decision: string | null) => { - if (!decision) return null - const styles: Record = { - KEEP: 'bg-green-100 text-green-800', - REVIEW: 'bg-amber-100 text-amber-800', - DROP: 'bg-red-100 text-red-800', - } - return ( - - {decision} - - ) - } - - const filteredAlerts = alerts.filter((alert) => { - if (inboxFilter === 'all') return true - if (inboxFilter === 'new') return alert.status === 'new' - if (inboxFilter === 'keep') return alert.relevance_decision === 'KEEP' - if (inboxFilter === 'review') return alert.relevance_decision === 'REVIEW' - return true - }) - - const tabs: { id: TabId; label: string; badge?: number }[] = [ - { id: 'dashboard', label: 'Dashboard' }, - { id: 'inbox', label: 'Inbox', badge: stats?.new_alerts || 0 }, - { id: 'topics', label: 'Topics' }, - { id: 'rules', label: 'Regeln' }, - { id: 'profile', label: 'Profil' }, - { id: 'audit', label: 'Audit' }, - { id: 'documentation', label: 'Dokumentation' }, - ] - - if (loading) { - return ( -
-
-
- ) - } - - return ( -
- {/* Page Purpose */} - - - {/* Stats Overview */} -
-
-
{stats?.total_alerts || 0}
-
Alerts gesamt
-
-
-
{stats?.new_alerts || 0}
-
Neue Alerts
-
-
-
{stats?.kept_alerts || 0}
-
Relevant
-
-
-
{stats?.review_alerts || 0}
-
Zur Pruefung
-
-
- - {/* Tab Navigation */} -
-
- -
- -
- {/* Dashboard Tab */} - {activeTab === 'dashboard' && ( -
- {/* Quick Actions */} -
-
-

Aktive Topics

-
- {topics.slice(0, 5).map((topic) => ( -
-
-
{topic.name}
-
{topic.alert_count} Alerts
-
- - {topic.is_active ? 'Aktiv' : 'Pausiert'} - -
- ))} - {topics.length === 0 && ( -
Keine Topics konfiguriert
- )} -
-
- -
-

Letzte Alerts

-
- {alerts.slice(0, 5).map((alert) => ( -
-
{alert.title}
-
- {alert.topic_name} - {getScoreBadge(alert.relevance_score)} -
-
- ))} - {alerts.length === 0 && ( -
Keine Alerts vorhanden
- )} -
-
-
- - {error && ( -
-

- Hinweis: API nicht erreichbar. Demo-Daten werden angezeigt. -

-
- )} -
- )} - - {/* Inbox Tab */} - {activeTab === 'inbox' && ( -
- {/* Filters */} -
- {['all', 'new', 'keep', 'review'].map((filter) => ( - - ))} -
- - {/* Alerts Table */} -
- - - - - - - - - - - - {filteredAlerts.map((alert) => ( - - - - - - - - ))} - {filteredAlerts.length === 0 && ( - - - - )} - -
AlertTopicScoreDecisionZeit
- - {alert.title} - -

{alert.snippet}

-
{alert.topic_name}{getScoreBadge(alert.relevance_score)}{getDecisionBadge(alert.relevance_decision)}{formatTimeAgo(alert.fetched_at)}
- Keine Alerts gefunden -
-
-
- )} - - {/* Topics Tab */} - {activeTab === 'topics' && ( -
-
-

Feed Topics

- -
- -
- {topics.map((topic) => ( -
-
-
- - - -
- - {topic.is_active ? 'Aktiv' : 'Pausiert'} - -
-

{topic.name}

-

{topic.feed_url}

-
-
- {topic.alert_count} - Alerts -
-
- {formatTimeAgo(topic.last_fetched_at)} -
-
-
- ))} - {topics.length === 0 && ( -
- Keine Topics konfiguriert -
- )} -
-
- )} - - {/* Rules Tab */} - {activeTab === 'rules' && ( -
-
-

Filterregeln

- -
- -
- {rules.map((rule) => ( -
-
- - - -
-
-
{rule.name}
-
- Wenn: {rule.conditions[0]?.field} {rule.conditions[0]?.operator} "{rule.conditions[0]?.value}" -
-
- - {rule.action_type} - -
-
-
-
- ))} - {rules.length === 0 && ( -
- Keine Regeln konfiguriert -
- )} -
-
- )} - - {/* Profile Tab */} - {activeTab === 'profile' && ( -
-
-

Relevanzprofil

- -
-
- -