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 && " ``` diff --git a/.env.example b/.env.example index cd027b4..ba66286 100644 --- a/.env.example +++ b/.env.example @@ -46,11 +46,6 @@ ERPNEXT_DB_ROOT_PASSWORD=erpnext_root ERPNEXT_DB_PASSWORD=erpnext_secret ERPNEXT_ADMIN_PASSWORD=admin -# Woodpecker CI -WOODPECKER_HOST=http://macmini:8090 -WOODPECKER_ADMIN=pilotadmin -WOODPECKER_AGENT_SECRET=woodpecker-secret - # Gitea Runner GITEA_RUNNER_TOKEN= diff --git a/.woodpecker/main.yml b/.woodpecker/main.yml deleted file mode 100644 index 3e8a8a9..0000000 --- a/.woodpecker/main.yml +++ /dev/null @@ -1,422 +0,0 @@ -# Woodpecker CI Main Pipeline -# BreakPilot Core - CI/CD Pipeline -# -# Plattform: ARM64 (Apple Silicon Mac Mini) -# -# Services: -# Go: consent-service -# Python: backend-core, voice-service (+ BQAS), embedding-service, night-scheduler -# Node.js: admin-core -# -# Strategie: -# - Lint bei PRs -# - Tests laufen bei JEDEM Push/PR -# - Test-Ergebnisse werden an Dashboard gesendet -# - Builds/Scans laufen nur bei Tags oder manuell -# - Deployment nur manuell (Sicherheit) - -when: - - event: [push, pull_request, manual, tag] - branch: [main, develop] - -clone: - git: - image: woodpeckerci/plugin-git - settings: - depth: 1 - extra_hosts: - - macmini:192.168.178.100 - -variables: - - &golang_image golang:1.23-alpine - - &python_image python:3.12-slim - - &nodejs_image node:20-alpine - - &docker_image docker:27-cli - -steps: - # ======================================== - # STAGE 1: Lint (nur bei PRs) - # ======================================== - - go-lint: - image: golangci/golangci-lint:v1.55-alpine - commands: - - cd consent-service && golangci-lint run --timeout 5m ./... - when: - event: pull_request - - python-lint: - image: *python_image - commands: - - pip install --quiet ruff - - | - for svc in backend-core voice-service night-scheduler embedding-service; do - if [ -d "$svc" ]; then - echo "=== Linting $svc ===" - ruff check "$svc/" --output-format=github || true - fi - done - when: - event: pull_request - - nodejs-lint: - image: *nodejs_image - commands: - - | - if [ -d "admin-core" ]; then - cd admin-core - npm ci --silent 2>/dev/null || npm install --silent - npx next lint || true - fi - when: - event: pull_request - - # ======================================== - # STAGE 2: Unit Tests mit JSON-Ausgabe - # Ergebnisse werden im Workspace gespeichert (.ci-results/) - # ======================================== - - test-go-consent: - image: *golang_image - environment: - CGO_ENABLED: "0" - commands: - - | - set -euo pipefail - apk add --no-cache jq bash - mkdir -p .ci-results - - if [ ! -d "consent-service" ]; then - echo '{"service":"consent-service","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-consent.json - echo "WARNUNG: consent-service Verzeichnis nicht gefunden" - exit 0 - fi - - cd consent-service - set +e - go test -v -json -coverprofile=coverage.out ./... 2>&1 | tee ../.ci-results/test-consent.json - TEST_EXIT=$? - set -e - - JSON_FILE="../.ci-results/test-consent.json" - if grep -q '^{' "$JSON_FILE" 2>/dev/null; then - TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length') - PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length') - FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length') - SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length') - else - echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)" - TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0 - fi - - COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0") - [ -z "$COVERAGE" ] && COVERAGE=0 - - echo "{\"service\":\"consent-service\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-consent.json - cat ../.ci-results/results-consent.json - - # Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter - if [ "$FAILED" -gt "0" ]; then - echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben" - fi - - test-python-voice: - image: *python_image - environment: - CI: "true" - commands: - - | - set -uo pipefail - mkdir -p .ci-results - - if [ ! -d "voice-service" ]; then - echo '{"service":"voice-service","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-voice.json - echo "WARNUNG: voice-service Verzeichnis nicht gefunden" - exit 0 - fi - - cd voice-service - export PYTHONPATH="$(pwd):${PYTHONPATH:-}" - pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true - pip install --quiet --no-cache-dir fastapi uvicorn pydantic pytest pytest-json-report - - set +e - python -m pytest tests/ -v --tb=short --ignore=tests/bqas --json-report --json-report-file=../.ci-results/test-voice.json - TEST_EXIT=$? - set -e - - if [ -f ../.ci-results/test-voice.json ]; then - TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0") - PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0") - FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0") - SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0") - else - TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0 - fi - - echo "{\"service\":\"voice-service\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-voice.json - cat ../.ci-results/results-voice.json - - if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi - - test-bqas-golden: - image: *python_image - commands: - - | - set -uo pipefail - mkdir -p .ci-results - - if [ ! -d "voice-service/tests/bqas" ]; then - echo '{"service":"bqas-golden","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-bqas-golden.json - echo "WARNUNG: voice-service/tests/bqas Verzeichnis nicht gefunden" - exit 0 - fi - - cd voice-service - export PYTHONPATH="$(pwd):${PYTHONPATH:-}" - pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true - pip install --quiet --no-cache-dir fastapi uvicorn pydantic pytest pytest-json-report pytest-asyncio - - set +e - python -m pytest tests/bqas/test_golden.py tests/bqas/test_regression.py tests/bqas/test_synthetic.py -v --tb=short --json-report --json-report-file=../.ci-results/test-bqas-golden.json - TEST_EXIT=$? - set -e - - if [ -f ../.ci-results/test-bqas-golden.json ]; then - TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0") - PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0") - FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0") - SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0") - else - TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0 - fi - - echo "{\"service\":\"bqas-golden\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-bqas-golden.json - cat ../.ci-results/results-bqas-golden.json - - # BQAS tests may skip if Ollama not available - don't fail pipeline - if [ "$FAILED" -gt "0" ]; then exit 1; fi - - test-bqas-rag: - image: *python_image - commands: - - | - set -uo pipefail - mkdir -p .ci-results - - if [ ! -d "voice-service/tests/bqas" ]; then - echo '{"service":"bqas-rag","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-bqas-rag.json - echo "WARNUNG: voice-service/tests/bqas Verzeichnis nicht gefunden" - exit 0 - fi - - cd voice-service - export PYTHONPATH="$(pwd):${PYTHONPATH:-}" - pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || true - pip install --quiet --no-cache-dir fastapi uvicorn pydantic pytest pytest-json-report pytest-asyncio - - set +e - python -m pytest tests/bqas/test_rag.py tests/bqas/test_notifier.py -v --tb=short --json-report --json-report-file=../.ci-results/test-bqas-rag.json - TEST_EXIT=$? - set -e - - if [ -f ../.ci-results/test-bqas-rag.json ]; then - TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0") - PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0") - FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0") - SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0") - else - TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0 - fi - - echo "{\"service\":\"bqas-rag\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-bqas-rag.json - cat ../.ci-results/results-bqas-rag.json - - # BQAS tests may skip if Ollama not available - don't fail pipeline - if [ "$FAILED" -gt "0" ]; then exit 1; fi - - # ======================================== - # STAGE 3: Test-Ergebnisse an Dashboard senden - # ======================================== - - report-test-results: - image: curlimages/curl:8.10.1 - commands: - - | - set -uo pipefail - echo "=== Sende Test-Ergebnisse an Dashboard ===" - echo "Pipeline Status: ${CI_PIPELINE_STATUS:-unknown}" - ls -la .ci-results/ || echo "Verzeichnis nicht gefunden" - - PIPELINE_STATUS="${CI_PIPELINE_STATUS:-unknown}" - - for f in .ci-results/results-*.json; do - [ -f "$f" ] || continue - echo "Sending: $f" - curl -f -sS -X POST "http://backend:8000/api/tests/ci-result" \ - -H "Content-Type: application/json" \ - -d "{ - \"pipeline_id\": \"${CI_PIPELINE_NUMBER}\", - \"commit\": \"${CI_COMMIT_SHA}\", - \"branch\": \"${CI_COMMIT_BRANCH}\", - \"repo\": \"breakpilot-core\", - \"status\": \"${PIPELINE_STATUS}\", - \"test_results\": $(cat "$f") - }" || echo "WARNUNG: Konnte $f nicht senden" - done - - echo "=== Test-Ergebnisse gesendet ===" - when: - status: [success, failure] - depends_on: - - test-go-consent - - test-python-voice - - test-bqas-golden - - test-bqas-rag - - # ======================================== - # STAGE 4: Build & Security (nur Tags/manuell) - # ======================================== - - build-consent-service: - image: *docker_image - commands: - - | - if [ -d ./consent-service ]; then - docker build -t breakpilot/consent-service:${CI_COMMIT_SHA:0:8} ./consent-service - docker tag breakpilot/consent-service:${CI_COMMIT_SHA:0:8} breakpilot/consent-service:latest - echo "Built breakpilot/consent-service:${CI_COMMIT_SHA:0:8}" - else - echo "consent-service Verzeichnis nicht gefunden - ueberspringe" - fi - when: - - event: tag - - event: manual - - build-backend-core: - image: *docker_image - commands: - - | - if [ -d ./backend-core ]; then - docker build -t breakpilot/backend-core:${CI_COMMIT_SHA:0:8} ./backend-core - docker tag breakpilot/backend-core:${CI_COMMIT_SHA:0:8} breakpilot/backend-core:latest - echo "Built breakpilot/backend-core:${CI_COMMIT_SHA:0:8}" - else - echo "backend-core Verzeichnis nicht gefunden - ueberspringe" - fi - when: - - event: tag - - event: manual - - build-admin-core: - image: *docker_image - commands: - - | - if [ -d ./admin-core ]; then - docker build -t breakpilot/admin-core:${CI_COMMIT_SHA:0:8} ./admin-core - docker tag breakpilot/admin-core:${CI_COMMIT_SHA:0:8} breakpilot/admin-core:latest - echo "Built breakpilot/admin-core:${CI_COMMIT_SHA:0:8}" - else - echo "admin-core Verzeichnis nicht gefunden - ueberspringe" - fi - when: - - event: tag - - event: manual - - build-voice-service: - image: *docker_image - commands: - - | - if [ -d ./voice-service ]; then - docker build -t breakpilot/voice-service:${CI_COMMIT_SHA:0:8} ./voice-service - docker tag breakpilot/voice-service:${CI_COMMIT_SHA:0:8} breakpilot/voice-service:latest - echo "Built breakpilot/voice-service:${CI_COMMIT_SHA:0:8}" - else - echo "voice-service Verzeichnis nicht gefunden - ueberspringe" - fi - when: - - event: tag - - event: manual - - build-embedding-service: - image: *docker_image - commands: - - | - if [ -d ./embedding-service ]; then - docker build -t breakpilot/embedding-service:${CI_COMMIT_SHA:0:8} ./embedding-service - docker tag breakpilot/embedding-service:${CI_COMMIT_SHA:0:8} breakpilot/embedding-service:latest - echo "Built breakpilot/embedding-service:${CI_COMMIT_SHA:0:8}" - else - echo "embedding-service Verzeichnis nicht gefunden - ueberspringe" - fi - when: - - event: tag - - event: manual - - build-night-scheduler: - image: *docker_image - commands: - - | - if [ -d ./night-scheduler ]; then - docker build -t breakpilot/night-scheduler:${CI_COMMIT_SHA:0:8} ./night-scheduler - docker tag breakpilot/night-scheduler:${CI_COMMIT_SHA:0:8} breakpilot/night-scheduler:latest - echo "Built breakpilot/night-scheduler:${CI_COMMIT_SHA:0:8}" - else - echo "night-scheduler Verzeichnis nicht gefunden - ueberspringe" - fi - when: - - event: tag - - event: manual - - generate-sbom: - image: *golang_image - commands: - - | - echo "Installing syft for ARM64..." - wget -qO- https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin - for svc in consent-service backend-core voice-service embedding-service night-scheduler; do - if [ -d "./$svc" ]; then - syft dir:./$svc -o cyclonedx-json > sbom-$svc.json - echo "SBOM generated for $svc" - fi - done - when: - - event: tag - - event: manual - - vulnerability-scan: - image: *golang_image - commands: - - | - echo "Installing grype for ARM64..." - wget -qO- https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin - for f in sbom-*.json; do - [ -f "$f" ] || continue - echo "=== Scanning $f ===" - grype sbom:"$f" -o table --fail-on critical || true - done - when: - - event: tag - - event: manual - depends_on: - - generate-sbom - - # ======================================== - # STAGE 5: Deploy (nur manuell) - # ======================================== - - deploy-production: - image: *docker_image - commands: - - echo "Deploying breakpilot-core to production..." - - docker compose -f docker-compose.yml pull || true - - docker compose -f docker-compose.yml up -d --remove-orphans || true - when: - event: manual - depends_on: - - build-consent-service - - build-backend-core - - build-admin-core - - build-voice-service - - build-embedding-service - - build-night-scheduler 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

- -
-
- -